summaryrefslogtreecommitdiff
path: root/apex
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2020-09-10 17:22:01 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2020-09-10 17:22:01 +0000
commit8ac6741e47c76bde065f868ea64d2f04541487b9 (patch)
tree1a679458fdbd8d370692d56791e2bf83acee35b5 /apex
parent3de940cc40b1e3fdf8224e18a8308a16768cbfa8 (diff)
parentc64112eb974e9aa7638aead998f07a868acfb5a7 (diff)
Merge "Merge Android R"
Diffstat (limited to 'apex')
-rw-r--r--apex/Android.bp20
-rw-r--r--apex/blobstore/OWNERS4
-rw-r--r--apex/blobstore/TEST_MAPPING18
-rw-r--r--apex/blobstore/framework/Android.bp40
-rw-r--r--apex/blobstore/framework/java/android/app/blob/BlobHandle.aidl19
-rw-r--r--apex/blobstore/framework/java/android/app/blob/BlobHandle.java305
-rw-r--r--apex/blobstore/framework/java/android/app/blob/BlobInfo.aidl19
-rw-r--r--apex/blobstore/framework/java/android/app/blob/BlobInfo.java127
-rw-r--r--apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java943
-rw-r--r--apex/blobstore/framework/java/android/app/blob/BlobStoreManagerFrameworkInitializer.java34
-rw-r--r--apex/blobstore/framework/java/android/app/blob/IBlobCommitCallback.aidl21
-rw-r--r--apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl43
-rw-r--r--apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl39
-rw-r--r--apex/blobstore/framework/java/android/app/blob/LeaseInfo.aidl19
-rw-r--r--apex/blobstore/framework/java/android/app/blob/LeaseInfo.java130
-rw-r--r--apex/blobstore/framework/java/android/app/blob/XmlTags.java58
-rw-r--r--apex/blobstore/service/Android.bp28
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java221
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java824
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java479
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java69
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerInternal.java28
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java1915
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java192
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java629
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobStoreUtils.java65
-rw-r--r--apex/extservices/Android.bp39
-rw-r--r--apex/extservices/apex_manifest.json4
-rw-r--r--apex/extservices/com.android.extservices.avbpubkeybin0 -> 1032 bytes
-rw-r--r--apex/extservices/com.android.extservices.pem51
-rw-r--r--apex/extservices/com.android.extservices.pk8bin0 -> 2376 bytes
-rw-r--r--apex/extservices/com.android.extservices.x509.pem36
-rw-r--r--apex/extservices/testing/Android.bp25
-rw-r--r--apex/extservices/testing/test_manifest.json4
-rw-r--r--apex/jobscheduler/OWNERS6
-rw-r--r--apex/jobscheduler/README_js-mainline.md20
-rw-r--r--apex/jobscheduler/framework/Android.bp30
-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.java1596
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobParameters.aidl19
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobParameters.java397
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobScheduler.java209
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java56
-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.java71
-rw-r--r--apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl52
-rw-r--r--apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java189
-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.java124
-rw-r--r--apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java168
-rw-r--r--apex/jobscheduler/service/Android.bp16
-rw-r--r--apex/jobscheduler/service/java/com/android/server/AnyMotionDetector.java520
-rw-r--r--apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java4638
-rw-r--r--apex/jobscheduler/service/java/com/android/server/TEST_MAPPING23
-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/TEST_MAPPING20
-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.java726
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java655
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java3492
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java496
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java876
-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.java52
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING45
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java250
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java294
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java702
-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.java159
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java2038
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java2911
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/RestrictingController.java41
-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.java238
-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.java820
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java2432
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING31
-rw-r--r--apex/media/OWNERS4
-rw-r--r--apex/media/aidl/Android.bp35
-rw-r--r--apex/media/aidl/private/android/media/Controller2Link.aidl19
-rw-r--r--apex/media/aidl/private/android/media/IMediaController2.aidl39
-rw-r--r--apex/media/aidl/private/android/media/IMediaSession2.aidl39
-rw-r--r--apex/media/aidl/private/android/media/IMediaSession2Service.aidl32
-rw-r--r--apex/media/aidl/private/android/media/Session2Command.aidl19
-rw-r--r--apex/media/aidl/stable/android/media/Session2Token.aidl19
-rw-r--r--apex/media/framework/Android.bp110
-rw-r--r--apex/media/framework/TEST_MAPPING7
-rw-r--r--apex/media/framework/api/current.txt226
-rw-r--r--apex/media/framework/api/module-lib-current.txt1
-rw-r--r--apex/media/framework/api/module-lib-removed.txt1
-rw-r--r--apex/media/framework/api/removed.txt1
-rw-r--r--apex/media/framework/api/system-current.txt1
-rw-r--r--apex/media/framework/api/system-removed.txt1
-rw-r--r--apex/media/framework/jarjar_rules.txt1
-rw-r--r--apex/media/framework/java/android/media/BufferingParams.java188
-rw-r--r--apex/media/framework/java/android/media/Controller2Link.java216
-rw-r--r--apex/media/framework/java/android/media/DataSourceCallback.java68
-rw-r--r--apex/media/framework/java/android/media/MediaConstants.java35
-rw-r--r--apex/media/framework/java/android/media/MediaController2.java638
-rw-r--r--apex/media/framework/java/android/media/MediaParser.java2130
-rw-r--r--apex/media/framework/java/android/media/MediaSession2.java907
-rw-r--r--apex/media/framework/java/android/media/MediaSession2Service.java452
-rw-r--r--apex/media/framework/java/android/media/ProxyDataSourceCallback.java68
-rw-r--r--apex/media/framework/java/android/media/RoutingDelegate.java48
-rw-r--r--apex/media/framework/java/android/media/Session2Command.java218
-rw-r--r--apex/media/framework/java/android/media/Session2CommandGroup.java197
-rw-r--r--apex/media/framework/java/android/media/Session2Link.java226
-rw-r--r--apex/media/framework/java/android/media/Session2Token.java272
-rw-r--r--apex/media/framework/updatable-media-proguard.flags2
-rw-r--r--apex/permission/Android.bp43
-rw-r--r--apex/permission/OWNERS6
-rw-r--r--apex/permission/TEST_MAPPING7
-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/permission/framework/Android.bp45
-rw-r--r--apex/permission/framework/api/current.txt1
-rw-r--r--apex/permission/framework/api/module-lib-current.txt1
-rw-r--r--apex/permission/framework/api/module-lib-removed.txt1
-rw-r--r--apex/permission/framework/api/removed.txt1
-rw-r--r--apex/permission/framework/api/system-current.txt1
-rw-r--r--apex/permission/framework/api/system-removed.txt1
-rw-r--r--apex/permission/framework/java/android/permission/PermissionState.java22
-rw-r--r--apex/permission/service/Android.bp42
-rw-r--r--apex/permission/service/api/current.txt1
-rw-r--r--apex/permission/service/api/removed.txt1
-rw-r--r--apex/permission/service/api/system-server-current.txt46
-rw-r--r--apex/permission/service/api/system-server-removed.txt1
-rw-r--r--apex/permission/service/java/com/android/permission/persistence/IoUtils.java40
-rw-r--r--apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistence.java74
-rw-r--r--apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistenceImpl.java265
-rw-r--r--apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsState.java222
-rw-r--r--apex/permission/service/java/com/android/role/persistence/RolesPersistence.java73
-rw-r--r--apex/permission/service/java/com/android/role/persistence/RolesPersistenceImpl.java218
-rw-r--r--apex/permission/service/java/com/android/role/persistence/RolesState.java115
-rw-r--r--apex/permission/testing/Android.bp25
-rw-r--r--apex/permission/testing/test_manifest.json4
-rw-r--r--apex/permission/tests/Android.bp37
-rw-r--r--apex/permission/tests/AndroidManifest.xml32
-rw-r--r--apex/permission/tests/java/com/android/permission/persistence/RuntimePermissionsPersistenceTest.kt110
-rw-r--r--apex/permission/tests/java/com/android/role/persistence/RolesPersistenceTest.kt101
-rw-r--r--apex/statsd/.clang-format17
-rw-r--r--apex/statsd/Android.bp83
-rw-r--r--apex/statsd/OWNERS9
-rw-r--r--apex/statsd/TEST_MAPPING10
-rw-r--r--apex/statsd/aidl/Android.bp51
-rw-r--r--apex/statsd/aidl/android/os/IPendingIntentRef.aidl46
-rw-r--r--apex/statsd/aidl/android/os/IPullAtomCallback.aidl31
-rw-r--r--apex/statsd/aidl/android/os/IPullAtomResultReceiver.aidl32
-rw-r--r--apex/statsd/aidl/android/os/IStatsCompanionService.aidl68
-rw-r--r--apex/statsd/aidl/android/os/IStatsManagerService.aidl136
-rw-r--r--apex/statsd/aidl/android/os/IStatsd.aidl237
-rw-r--r--apex/statsd/aidl/android/os/StatsDimensionsValueParcel.aidl21
-rw-r--r--apex/statsd/aidl/android/util/StatsEventParcel.aidl8
-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/framework/Android.bp81
-rw-r--r--apex/statsd/framework/api/current.txt12
-rw-r--r--apex/statsd/framework/api/module-lib-current.txt10
-rw-r--r--apex/statsd/framework/api/module-lib-removed.txt1
-rw-r--r--apex/statsd/framework/api/removed.txt1
-rw-r--r--apex/statsd/framework/api/system-current.txt111
-rw-r--r--apex/statsd/framework/api/system-removed.txt1
-rw-r--r--apex/statsd/framework/java/android/app/StatsManager.java725
-rw-r--r--apex/statsd/framework/java/android/os/StatsDimensionsValue.java317
-rw-r--r--apex/statsd/framework/java/android/os/StatsFrameworkInitializer.java77
-rw-r--r--apex/statsd/framework/java/android/util/StatsEvent.java879
-rw-r--r--apex/statsd/framework/java/android/util/StatsLog.java185
-rw-r--r--apex/statsd/framework/test/Android.bp36
-rw-r--r--apex/statsd/framework/test/AndroidManifest.xml26
-rw-r--r--apex/statsd/framework/test/AndroidTest.xml34
-rw-r--r--apex/statsd/framework/test/src/android/app/PullAtomMetadataTest.java85
-rw-r--r--apex/statsd/framework/test/src/android/os/StatsDimensionsValueTest.java115
-rw-r--r--apex/statsd/framework/test/src/android/util/StatsEventTest.java818
-rw-r--r--apex/statsd/jni/android_util_StatsLog.cpp90
-rw-r--r--apex/statsd/service/Android.bp35
-rw-r--r--apex/statsd/service/java/com/android/server/stats/StatsCompanion.java188
-rw-r--r--apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java817
-rw-r--r--apex/statsd/service/java/com/android/server/stats/StatsManagerService.java661
-rw-r--r--apex/statsd/statsd.rc20
-rw-r--r--apex/statsd/testing/Android.bp25
-rw-r--r--apex/statsd/testing/test_manifest.json4
-rw-r--r--apex/statsd/tests/libstatspull/Android.bp61
-rw-r--r--apex/statsd/tests/libstatspull/AndroidManifest.xml31
-rw-r--r--apex/statsd/tests/libstatspull/jni/stats_pull_helper.cpp70
-rw-r--r--apex/statsd/tests/libstatspull/protos/test_atoms.proto32
-rw-r--r--apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java287
-rw-r--r--apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java124
216 files changed, 50664 insertions, 1 deletions
diff --git a/apex/Android.bp b/apex/Android.bp
index 74db82825e6a..a5e2b4a5b707 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -106,7 +106,10 @@ java_defaults {
stubs_library_visibility: ["//visibility:public"],
// Hide impl library and stub sources
- impl_library_visibility: [":__package__"],
+ impl_library_visibility: [
+ ":__package__",
+ "//frameworks/base", // For framework-all
+ ],
stubs_source_visibility: ["//visibility:private"],
// Collates API usages from each module for further analysis.
@@ -158,6 +161,9 @@ stubs_defaults {
api_file: "api/current.txt",
removed_api_file: "api/removed.txt",
},
+ api_lint: {
+ enabled: true,
+ },
},
dist: {
targets: ["sdk", "win_sdk"],
@@ -181,6 +187,9 @@ stubs_defaults {
api_file: "api/system-current.txt",
removed_api_file: "api/system-removed.txt",
},
+ api_lint: {
+ enabled: true,
+ },
},
dist: {
targets: ["sdk", "win_sdk"],
@@ -193,6 +202,7 @@ java_defaults {
installable: false,
sdk_version: "module_current",
libs: [ "stub-annotations" ],
+ java_version: "1.8",
dist: {
targets: ["sdk", "win_sdk"],
dir: "apistubs/android/public",
@@ -204,6 +214,7 @@ java_defaults {
installable: false,
sdk_version: "module_current",
libs: [ "stub-annotations" ],
+ java_version: "1.8",
dist: {
targets: ["sdk", "win_sdk"],
dir: "apistubs/android/system",
@@ -215,6 +226,7 @@ java_defaults {
installable: false,
sdk_version: "module_current",
libs: [ "stub-annotations" ],
+ java_version: "1.8",
dist: {
targets: ["sdk", "win_sdk"],
dir: "apistubs/android/module-lib",
@@ -246,6 +258,9 @@ stubs_defaults {
api_file: "api/module-lib-current.txt",
removed_api_file: "api/module-lib-removed.txt",
},
+ api_lint: {
+ enabled: true,
+ },
},
dist: {
targets: ["sdk", "win_sdk"],
@@ -280,6 +295,9 @@ stubs_defaults {
api_file: "api/current.txt",
removed_api_file: "api/removed.txt",
},
+ api_lint: {
+ enabled: true,
+ },
},
dist: {
targets: ["sdk", "win_sdk"],
diff --git a/apex/blobstore/OWNERS b/apex/blobstore/OWNERS
new file mode 100644
index 000000000000..8e04399196e2
--- /dev/null
+++ b/apex/blobstore/OWNERS
@@ -0,0 +1,4 @@
+set noparent
+
+sudheersai@google.com
+yamasani@google.com
diff --git a/apex/blobstore/TEST_MAPPING b/apex/blobstore/TEST_MAPPING
new file mode 100644
index 000000000000..6d3c0d73f77f
--- /dev/null
+++ b/apex/blobstore/TEST_MAPPING
@@ -0,0 +1,18 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsBlobStoreTestCases"
+ },
+ {
+ "name": "CtsBlobStoreHostTestCases"
+ },
+ {
+ "name": "FrameworksMockingServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.blob"
+ }
+ ]
+ }
+ ]
+}
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/BlobHandle.aidl b/apex/blobstore/framework/java/android/app/blob/BlobHandle.aidl
new file mode 100644
index 000000000000..02d0740a2ce0
--- /dev/null
+++ b/apex/blobstore/framework/java/android/app/blob/BlobHandle.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 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} */
+parcelable BlobHandle; \ No newline at end of file
diff --git a/apex/blobstore/framework/java/android/app/blob/BlobHandle.java b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java
new file mode 100644
index 000000000000..113f8fe9e248
--- /dev/null
+++ b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2020 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 static android.app.blob.XmlTags.ATTR_ALGO;
+import static android.app.blob.XmlTags.ATTR_DIGEST;
+import static android.app.blob.XmlTags.ATTR_EXPIRY_TIME;
+import static android.app.blob.XmlTags.ATTR_LABEL;
+import static android.app.blob.XmlTags.ATTR_TAG;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Base64;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * An identifier to represent a blob.
+ */
+// TODO: use datagen tool?
+public final class BlobHandle implements Parcelable {
+ /** @hide */
+ public static final String ALGO_SHA_256 = "SHA-256";
+
+ private static final String[] SUPPORTED_ALGOS = {
+ ALGO_SHA_256
+ };
+
+ private static final int LIMIT_BLOB_TAG_LENGTH = 128; // characters
+ private static final int LIMIT_BLOB_LABEL_LENGTH = 100; // characters
+
+ /**
+ * Cyrptographically secure hash algorithm used to generate hash of the blob this handle is
+ * representing.
+ *
+ * @hide
+ */
+ @NonNull public final String algorithm;
+
+ /**
+ * Hash of the blob this handle is representing using {@link #algorithm}.
+ *
+ * @hide
+ */
+ @NonNull public final byte[] digest;
+
+ /**
+ * Label of the blob that can be surfaced to the user.
+ * @hide
+ */
+ @NonNull public final CharSequence label;
+
+ /**
+ * Time in milliseconds after which the blob should be invalidated and not
+ * allowed to be accessed by any other app, in {@link System#currentTimeMillis()} timebase.
+ *
+ * @hide
+ */
+ @CurrentTimeMillisLong public final long expiryTimeMillis;
+
+ /**
+ * An opaque {@link String} associated with the blob.
+ *
+ * @hide
+ */
+ @NonNull public final String tag;
+
+ private BlobHandle(String algorithm, byte[] digest, CharSequence label, long expiryTimeMillis,
+ String tag) {
+ this.algorithm = algorithm;
+ this.digest = digest;
+ this.label = label;
+ this.expiryTimeMillis = expiryTimeMillis;
+ this.tag = tag;
+ }
+
+ private BlobHandle(Parcel in) {
+ this.algorithm = in.readString();
+ this.digest = in.createByteArray();
+ this.label = in.readCharSequence();
+ this.expiryTimeMillis = in.readLong();
+ this.tag = in.readString();
+ }
+
+ /** @hide */
+ public static @NonNull BlobHandle create(@NonNull String algorithm, @NonNull byte[] digest,
+ @NonNull CharSequence label, @CurrentTimeMillisLong long expiryTimeMillis,
+ @NonNull String tag) {
+ final BlobHandle handle = new BlobHandle(algorithm, digest, label, expiryTimeMillis, tag);
+ handle.assertIsValid();
+ return handle;
+ }
+
+ /**
+ * Create a new blob identifier.
+ *
+ * <p> For two objects of {@link BlobHandle} to be considered equal, the following arguments
+ * must be equal:
+ * <ul>
+ * <li> {@code digest}
+ * <li> {@code label}
+ * <li> {@code expiryTimeMillis}
+ * <li> {@code tag}
+ * </ul>
+ *
+ * @param digest the SHA-256 hash of the blob this is representing.
+ * @param label a label indicating what the blob is, that can be surfaced to the user.
+ * The length of the label cannot be more than 100 characters. It is recommended
+ * to keep this brief. This may be truncated and ellipsized if it is too long
+ * to be displayed to the user.
+ * @param expiryTimeMillis the time in secs after which the blob should be invalidated and not
+ * allowed to be accessed by any other app,
+ * in {@link System#currentTimeMillis()} timebase or {@code 0} to
+ * indicate that there is no expiry time associated with this blob.
+ * @param tag an opaque {@link String} associated with the blob. The length of the tag
+ * cannot be more than 128 characters.
+ *
+ * @return a new instance of {@link BlobHandle} object.
+ */
+ public static @NonNull BlobHandle createWithSha256(@NonNull byte[] digest,
+ @NonNull CharSequence label, @CurrentTimeMillisLong long expiryTimeMillis,
+ @NonNull String tag) {
+ return create(ALGO_SHA_256, digest, label, expiryTimeMillis, tag);
+ }
+
+ /**
+ * Returns the SHA-256 hash of the blob that this object is representing.
+ *
+ * @see #createWithSha256(byte[], CharSequence, long, String)
+ */
+ public @NonNull byte[] getSha256Digest() {
+ return digest;
+ }
+
+ /**
+ * Returns the label associated with the blob that this object is representing.
+ *
+ * @see #createWithSha256(byte[], CharSequence, long, String)
+ */
+ public @NonNull CharSequence getLabel() {
+ return label;
+ }
+
+ /**
+ * Returns the expiry time in milliseconds of the blob that this object is representing, in
+ * {@link System#currentTimeMillis()} timebase.
+ *
+ * @see #createWithSha256(byte[], CharSequence, long, String)
+ */
+ public @CurrentTimeMillisLong long getExpiryTimeMillis() {
+ return expiryTimeMillis;
+ }
+
+ /**
+ * Returns the opaque {@link String} associated with the blob this object is representing.
+ *
+ * @see #createWithSha256(byte[], CharSequence, long, String)
+ */
+ public @NonNull String getTag() {
+ return tag;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(algorithm);
+ dest.writeByteArray(digest);
+ dest.writeCharSequence(label);
+ dest.writeLong(expiryTimeMillis);
+ dest.writeString(tag);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof BlobHandle)) {
+ return false;
+ }
+ final BlobHandle other = (BlobHandle) obj;
+ return this.algorithm.equals(other.algorithm)
+ && Arrays.equals(this.digest, other.digest)
+ && this.label.toString().equals(other.label.toString())
+ && this.expiryTimeMillis == other.expiryTimeMillis
+ && this.tag.equals(other.tag);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(algorithm, Arrays.hashCode(digest), label, expiryTimeMillis, tag);
+ }
+
+ /** @hide */
+ public void dump(IndentingPrintWriter fout, boolean dumpFull) {
+ if (dumpFull) {
+ fout.println("algo: " + algorithm);
+ fout.println("digest: " + (dumpFull ? encodeDigest(digest) : safeDigest(digest)));
+ fout.println("label: " + label);
+ fout.println("expiryMs: " + expiryTimeMillis);
+ fout.println("tag: " + tag);
+ } else {
+ fout.println(toString());
+ }
+ }
+
+ /** @hide */
+ public void assertIsValid() {
+ Preconditions.checkArgumentIsSupported(SUPPORTED_ALGOS, algorithm);
+ Preconditions.checkByteArrayNotEmpty(digest, "digest");
+ Preconditions.checkStringNotEmpty(label, "label must not be null");
+ Preconditions.checkArgument(label.length() <= LIMIT_BLOB_LABEL_LENGTH, "label too long");
+ Preconditions.checkArgumentNonnegative(expiryTimeMillis,
+ "expiryTimeMillis must not be negative");
+ Preconditions.checkStringNotEmpty(tag, "tag must not be null");
+ Preconditions.checkArgument(tag.length() <= LIMIT_BLOB_TAG_LENGTH, "tag too long");
+ }
+
+ @Override
+ public String toString() {
+ return "BlobHandle {"
+ + "algo:" + algorithm + ","
+ + "digest:" + safeDigest(digest) + ","
+ + "label:" + label + ","
+ + "expiryMs:" + expiryTimeMillis + ","
+ + "tag:" + tag
+ + "}";
+ }
+
+ /** @hide */
+ public static String safeDigest(@NonNull byte[] digest) {
+ final String digestStr = encodeDigest(digest);
+ return digestStr.substring(0, 2) + ".." + digestStr.substring(digestStr.length() - 2);
+ }
+
+ private static String encodeDigest(@NonNull byte[] digest) {
+ return Base64.encodeToString(digest, Base64.NO_WRAP);
+ }
+
+ /** @hide */
+ public boolean isExpired() {
+ return expiryTimeMillis != 0 && expiryTimeMillis < System.currentTimeMillis();
+ }
+
+ public static final @NonNull Creator<BlobHandle> CREATOR = new Creator<BlobHandle>() {
+ @Override
+ public @NonNull BlobHandle createFromParcel(@NonNull Parcel source) {
+ return new BlobHandle(source);
+ }
+
+ @Override
+ public @NonNull BlobHandle[] newArray(int size) {
+ return new BlobHandle[size];
+ }
+ };
+
+ /** @hide */
+ public void writeToXml(@NonNull XmlSerializer out) throws IOException {
+ XmlUtils.writeStringAttribute(out, ATTR_ALGO, algorithm);
+ XmlUtils.writeByteArrayAttribute(out, ATTR_DIGEST, digest);
+ XmlUtils.writeStringAttribute(out, ATTR_LABEL, label);
+ XmlUtils.writeLongAttribute(out, ATTR_EXPIRY_TIME, expiryTimeMillis);
+ XmlUtils.writeStringAttribute(out, ATTR_TAG, tag);
+ }
+
+ /** @hide */
+ @NonNull
+ public static BlobHandle createFromXml(@NonNull XmlPullParser in) throws IOException {
+ final String algo = XmlUtils.readStringAttribute(in, ATTR_ALGO);
+ final byte[] digest = XmlUtils.readByteArrayAttribute(in, ATTR_DIGEST);
+ final CharSequence label = XmlUtils.readStringAttribute(in, ATTR_LABEL);
+ final long expiryTimeMs = XmlUtils.readLongAttribute(in, ATTR_EXPIRY_TIME);
+ final String tag = XmlUtils.readStringAttribute(in, ATTR_TAG);
+
+ return BlobHandle.create(algo, digest, label, expiryTimeMs, tag);
+ }
+}
diff --git a/apex/blobstore/framework/java/android/app/blob/BlobInfo.aidl b/apex/blobstore/framework/java/android/app/blob/BlobInfo.aidl
new file mode 100644
index 000000000000..25497738f685
--- /dev/null
+++ b/apex/blobstore/framework/java/android/app/blob/BlobInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 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} */
+parcelable BlobInfo; \ No newline at end of file
diff --git a/apex/blobstore/framework/java/android/app/blob/BlobInfo.java b/apex/blobstore/framework/java/android/app/blob/BlobInfo.java
new file mode 100644
index 000000000000..ba92d95b483e
--- /dev/null
+++ b/apex/blobstore/framework/java/android/app/blob/BlobInfo.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2020 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 static android.text.format.Formatter.FLAG_IEC_UNITS;
+
+import android.annotation.NonNull;
+import android.app.AppGlobals;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.format.Formatter;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Class to provide information about a shared blob.
+ *
+ * @hide
+ */
+public final class BlobInfo implements Parcelable {
+ private final long mId;
+ private final long mExpiryTimeMs;
+ private final CharSequence mLabel;
+ private final long mSizeBytes;
+ private final List<LeaseInfo> mLeaseInfos;
+
+ public BlobInfo(long id, long expiryTimeMs, CharSequence label, long sizeBytes,
+ List<LeaseInfo> leaseInfos) {
+ mId = id;
+ mExpiryTimeMs = expiryTimeMs;
+ mLabel = label;
+ mSizeBytes = sizeBytes;
+ mLeaseInfos = leaseInfos;
+ }
+
+ private BlobInfo(Parcel in) {
+ mId = in.readLong();
+ mExpiryTimeMs = in.readLong();
+ mLabel = in.readCharSequence();
+ mSizeBytes = in.readLong();
+ mLeaseInfos = in.readArrayList(null /* classloader */);
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public long getExpiryTimeMs() {
+ return mExpiryTimeMs;
+ }
+
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ public long getSizeBytes() {
+ return mSizeBytes;
+ }
+
+ public List<LeaseInfo> getLeases() {
+ return Collections.unmodifiableList(mLeaseInfos);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeLong(mId);
+ dest.writeLong(mExpiryTimeMs);
+ dest.writeCharSequence(mLabel);
+ dest.writeLong(mSizeBytes);
+ dest.writeList(mLeaseInfos);
+ }
+
+ @Override
+ public String toString() {
+ return toShortString();
+ }
+
+ private String toShortString() {
+ return "BlobInfo {"
+ + "id: " + mId + ","
+ + "expiryMs: " + mExpiryTimeMs + ","
+ + "label: " + mLabel + ","
+ + "size: " + formatBlobSize(mSizeBytes) + ","
+ + "leases: " + LeaseInfo.toShortString(mLeaseInfos) + ","
+ + "}";
+ }
+
+ private static String formatBlobSize(long sizeBytes) {
+ return Formatter.formatFileSize(AppGlobals.getInitialApplication(),
+ sizeBytes, FLAG_IEC_UNITS);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<BlobInfo> CREATOR = new Creator<BlobInfo>() {
+ @Override
+ @NonNull
+ public BlobInfo createFromParcel(Parcel source) {
+ return new BlobInfo(source);
+ }
+
+ @Override
+ @NonNull
+ public BlobInfo[] newArray(int size) {
+ return new BlobInfo[size];
+ }
+ };
+}
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..39f7526560a9
--- /dev/null
+++ b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java
@@ -0,0 +1,943 @@
+/*
+ * 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.BytesLong;
+import android.annotation.CallbackExecutor;
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.IdRes;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.os.LimitExceededException;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelableException;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.UserHandle;
+
+import com.android.internal.util.function.pooled.PooledLambda;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+
+/**
+ * This class provides access to the blob store managed by the system.
+ *
+ * <p> Apps can publish and access a data blob using a {@link BlobHandle} object which can
+ * be created with {@link BlobHandle#createWithSha256(byte[], CharSequence, long, String)}.
+ * This {@link BlobHandle} object encapsulates the following pieces of information used for
+ * identifying the blobs:
+ * <ul>
+ * <li> {@link BlobHandle#getSha256Digest()}
+ * <li> {@link BlobHandle#getLabel()}
+ * <li> {@link BlobHandle#getExpiryTimeMillis()}
+ * <li> {@link BlobHandle#getTag()}
+ * </ul>
+ * For two {@link BlobHandle} objects to be considered identical, all these pieces of information
+ * must be equal.
+ *
+ * <p> For contributing a new data blob, an app needs to create a session using
+ * {@link BlobStoreManager#createSession(BlobHandle)} and then open this session for writing using
+ * {@link BlobStoreManager#openSession(long)}.
+ *
+ * <p> The following code snippet shows how to create and open a session for writing:
+ * <pre class="prettyprint">
+ * final long sessionId = blobStoreManager.createSession(blobHandle);
+ * try (BlobStoreManager.Session session = blobStoreManager.openSession(sessionId)) {
+ * try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(
+ * session.openWrite(offsetBytes, lengthBytes))) {
+ * writeData(out);
+ * }
+ * }
+ * </pre>
+ *
+ * <p> If all the data could not be written in a single attempt, apps can close this session
+ * and re-open it again using the session id obtained via
+ * {@link BlobStoreManager#createSession(BlobHandle)}. Note that the session data is persisted
+ * and can be re-opened for completing the data contribution, even across device reboots.
+ *
+ * <p> After the data is written to the session, it can be committed using
+ * {@link Session#commit(Executor, Consumer)}. Until the session is committed, data written
+ * to the session will not be shared with any app.
+ *
+ * <p class="note"> Once a session is committed using {@link Session#commit(Executor, Consumer)},
+ * any data written as part of this session is sealed and cannot be modified anymore.
+ *
+ * <p> Before committing the session, apps can indicate which apps are allowed to access the
+ * contributed data using one or more of the following access modes:
+ * <ul>
+ * <li> {@link Session#allowPackageAccess(String, byte[])} which will allow whitelisting
+ * specific packages to access the blobs.
+ * <li> {@link Session#allowSameSignatureAccess()} which will allow only apps which are signed
+ * with the same certificate as the app which contributed the blob to access it.
+ * <li> {@link Session#allowPublicAccess()} which will allow any app on the device to access
+ * the blob.
+ * </ul>
+ *
+ * <p> The following code snippet shows how to specify the access mode and commit the session:
+ * <pre class="prettyprint">
+ * try (BlobStoreManager.Session session = blobStoreManager.openSession(sessionId)) {
+ * try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(
+ * session.openWrite(offsetBytes, lengthBytes))) {
+ * writeData(out);
+ * }
+ * session.allowSameSignatureAccess();
+ * session.allowPackageAccess(packageName, certificate);
+ * session.commit(executor, callback);
+ * }
+ * </pre>
+ *
+ * <p> Apps that satisfy at least one of the access mode constraints specified by the publisher
+ * of the data blob will be able to access it.
+ *
+ * <p> A data blob published without specifying any of
+ * these access modes will be considered private and only the app that contributed the data
+ * blob will be allowed to access it. This is still useful for overall device system health as
+ * the System can try to keep one copy of data blob on disk when multiple apps contribute the
+ * same data.
+ *
+ * <p class="note"> It is strongly recommended that apps use one of
+ * {@link Session#allowPackageAccess(String, byte[])} or {@link Session#allowSameSignatureAccess()}
+ * when they know, ahead of time, the set of apps they would like to share the blobs with.
+ * {@link Session#allowPublicAccess()} is meant for publicly available data committed from
+ * libraries and SDKs.
+ *
+ * <p> Once a data blob is committed with {@link Session#commit(Executor, Consumer)}, it
+ * can be accessed using {@link BlobStoreManager#openBlob(BlobHandle)}, assuming the caller
+ * satisfies constraints of any of the access modes associated with that data blob. An app may
+ * acquire a lease on a blob with {@link BlobStoreManager#acquireLease(BlobHandle, int)} and
+ * release the lease with {@link BlobStoreManager#releaseLease(BlobHandle)}. A blob will not be
+ * deleted from the system while there is at least one app leasing it.
+ *
+ * <p> The following code snippet shows how to access the data blob:
+ * <pre class="prettyprint">
+ * try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
+ * blobStoreManager.openBlob(blobHandle)) {
+ * useData(in);
+ * }
+ * </pre>
+ */
+@SystemService(Context.BLOB_STORE_SERVICE)
+public class BlobStoreManager {
+ /** @hide */
+ public static final int COMMIT_RESULT_SUCCESS = 0;
+ /** @hide */
+ public static final int COMMIT_RESULT_ERROR = 1;
+
+ /** @hide */
+ public static final int INVALID_RES_ID = -1;
+
+ private final Context mContext;
+ private final IBlobStoreManager mService;
+
+ /** @hide */
+ public BlobStoreManager(@NonNull Context context, @NonNull IBlobStoreManager service) {
+ mContext = context;
+ mService = service;
+ }
+
+ /**
+ * Create a new session using the given {@link BlobHandle}, returning a unique id
+ * that represents the session. Once created, the session can be opened
+ * multiple times across multiple device boots.
+ *
+ * <p> The system may automatically destroy sessions that have not been
+ * finalized (either committed or abandoned) within a reasonable period of
+ * time, typically about a week.
+ *
+ * <p> If an app is planning to acquire a lease on this data (using
+ * {@link #acquireLease(BlobHandle, int)} or one of it's other variants) after committing
+ * this data (using {@link Session#commit(Executor, Consumer)}), it is recommended that
+ * the app checks the remaining quota for acquiring a lease first using
+ * {@link #getRemainingLeaseQuotaBytes()} and can skip contributing this data if needed.
+ *
+ * @param blobHandle the {@link BlobHandle} identifier for which a new session
+ * needs to be created.
+ * @return positive, non-zero unique id that represents the created session.
+ * This id remains consistent across device reboots until the
+ * session is finalized. IDs are not reused during a given boot.
+ *
+ * @throws IOException when there is an I/O error while creating the session.
+ * @throws SecurityException when the caller is not allowed to create a session, such
+ * as when called from an Instant app.
+ * @throws IllegalArgumentException when {@code blobHandle} is invalid.
+ * @throws LimitExceededException when a new session could not be created, such as when the
+ * caller is trying to create too many sessions.
+ */
+ public @IntRange(from = 1) long createSession(@NonNull BlobHandle blobHandle)
+ throws IOException {
+ try {
+ return mService.createSession(blobHandle, mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ e.maybeRethrow(LimitExceededException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Open an existing session to actively perform work.
+ *
+ * @param sessionId a unique id obtained via {@link #createSession(BlobHandle)} that
+ * represents a particular session.
+ * @return the {@link Session} object corresponding to the {@code sessionId}.
+ *
+ * @throws IOException when there is an I/O error while opening the session.
+ * @throws SecurityException when the caller does not own the session, or
+ * the session does not exist or is invalid.
+ */
+ public @NonNull Session openSession(@IntRange(from = 1) long sessionId) throws IOException {
+ try {
+ return new Session(mService.openSession(sessionId, mContext.getOpPackageName()));
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Abandons an existing session and deletes any data that was written to that session so far.
+ *
+ * @param sessionId a unique id obtained via {@link #createSession(BlobHandle)} that
+ * represents a particular session.
+ *
+ * @throws IOException when there is an I/O error while deleting the session.
+ * @throws SecurityException when the caller does not own the session, or
+ * the session does not exist or is invalid.
+ */
+ public void abandonSession(@IntRange(from = 1) long sessionId) throws IOException {
+ try {
+ mService.abandonSession(sessionId, mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Opens an existing blob for reading from the blob store managed by the system.
+ *
+ * @param blobHandle the {@link BlobHandle} representing the blob that the caller
+ * wants to access.
+ * @return a {@link ParcelFileDescriptor} that can be used to read the blob content.
+ *
+ * @throws IOException when there is an I/O while opening the blob for read.
+ * @throws IllegalArgumentException when {@code blobHandle} is invalid.
+ * @throws SecurityException when the blob represented by the {@code blobHandle} does not
+ * exist or the caller does not have access to it.
+ */
+ public @NonNull ParcelFileDescriptor openBlob(@NonNull BlobHandle blobHandle)
+ throws IOException {
+ try {
+ return mService.openBlob(blobHandle, mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the
+ * system that the caller wants the blob to be kept around.
+ *
+ * <p> Any active leases will be automatically released when the blob's expiry time
+ * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed.
+ *
+ * <p> This lease information is persisted and calling this more than once will result in
+ * latest lease overriding any previous lease.
+ *
+ * <p> When an app acquires a lease on a blob, the System will try to keep this
+ * blob around but note that it can still be deleted if it was requested by the user.
+ *
+ * <p> In case the resource name for the {@code descriptionResId} is modified as part of
+ * an app update, apps should re-acquire the lease with the new resource id.
+ *
+ * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
+ * acquire a lease for.
+ * @param descriptionResId the resource id for a short description string that can be surfaced
+ * to the user explaining what the blob is used for.
+ * @param leaseExpiryTimeMillis the time in milliseconds after which the lease can be
+ * automatically released, in {@link System#currentTimeMillis()}
+ * timebase. If its value is {@code 0}, then the behavior of this
+ * API is identical to {@link #acquireLease(BlobHandle, int)}
+ * where clients have to explicitly call
+ * {@link #releaseLease(BlobHandle)} when they don't
+ * need the blob anymore.
+ *
+ * @throws IOException when there is an I/O error while acquiring a lease to the blob.
+ * @throws SecurityException when the blob represented by the {@code blobHandle} does not
+ * exist or the caller does not have access to it.
+ * @throws IllegalArgumentException when {@code blobHandle} is invalid or
+ * if the {@code leaseExpiryTimeMillis} is greater than the
+ * {@link BlobHandle#getExpiryTimeMillis()}.
+ * @throws LimitExceededException when a lease could not be acquired, such as when the
+ * caller is trying to acquire too many leases or acquire
+ * leases on too much data. Apps can avoid this by checking
+ * the remaining quota using
+ * {@link #getRemainingLeaseQuotaBytes()} before trying to
+ * acquire a lease.
+ *
+ * @see #acquireLease(BlobHandle, int)
+ * @see #acquireLease(BlobHandle, CharSequence)
+ */
+ public void acquireLease(@NonNull BlobHandle blobHandle, @IdRes int descriptionResId,
+ @CurrentTimeMillisLong long leaseExpiryTimeMillis) throws IOException {
+ try {
+ mService.acquireLease(blobHandle, descriptionResId, null, leaseExpiryTimeMillis,
+ mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ e.maybeRethrow(LimitExceededException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the
+ * system that the caller wants the blob to be kept around.
+ *
+ * <p> This is a variant of {@link #acquireLease(BlobHandle, int, long)} taking a
+ * {@link CharSequence} for {@code description}. It is highly recommended that callers only
+ * use this when a valid resource ID for {@code description} could not be provided. Otherwise,
+ * apps should prefer using {@link #acquireLease(BlobHandle, int)} which will allow
+ * {@code description} to be localized.
+ *
+ * <p> Any active leases will be automatically released when the blob's expiry time
+ * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed.
+ *
+ * <p> This lease information is persisted and calling this more than once will result in
+ * latest lease overriding any previous lease.
+ *
+ * <p> When an app acquires a lease on a blob, the System will try to keep this
+ * blob around but note that it can still be deleted if it was requested by the user.
+ *
+ * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
+ * acquire a lease for.
+ * @param description a short description string that can be surfaced
+ * to the user explaining what the blob is used for. It is recommended to
+ * keep this description brief. This may be truncated and ellipsized
+ * if it is too long to be displayed to the user.
+ * @param leaseExpiryTimeMillis the time in milliseconds after which the lease can be
+ * automatically released, in {@link System#currentTimeMillis()}
+ * timebase. If its value is {@code 0}, then the behavior of this
+ * API is identical to {@link #acquireLease(BlobHandle, int)}
+ * where clients have to explicitly call
+ * {@link #releaseLease(BlobHandle)} when they don't
+ * need the blob anymore.
+ *
+ * @throws IOException when there is an I/O error while acquiring a lease to the blob.
+ * @throws SecurityException when the blob represented by the {@code blobHandle} does not
+ * exist or the caller does not have access to it.
+ * @throws IllegalArgumentException when {@code blobHandle} is invalid or
+ * if the {@code leaseExpiryTimeMillis} is greater than the
+ * {@link BlobHandle#getExpiryTimeMillis()}.
+ * @throws LimitExceededException when a lease could not be acquired, such as when the
+ * caller is trying to acquire too many leases or acquire
+ * leases on too much data. Apps can avoid this by checking
+ * the remaining quota using
+ * {@link #getRemainingLeaseQuotaBytes()} before trying to
+ * acquire a lease.
+ *
+ * @see #acquireLease(BlobHandle, int, long)
+ * @see #acquireLease(BlobHandle, CharSequence)
+ */
+ public void acquireLease(@NonNull BlobHandle blobHandle, @NonNull CharSequence description,
+ @CurrentTimeMillisLong long leaseExpiryTimeMillis) throws IOException {
+ try {
+ mService.acquireLease(blobHandle, INVALID_RES_ID, description, leaseExpiryTimeMillis,
+ mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ e.maybeRethrow(LimitExceededException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the
+ * system that the caller wants the blob to be kept around.
+ *
+ * <p> This is similar to {@link #acquireLease(BlobHandle, int, long)} except clients don't
+ * have to specify the lease expiry time upfront using this API and need to explicitly
+ * release the lease using {@link #releaseLease(BlobHandle)} when they no longer like to keep
+ * a blob around.
+ *
+ * <p> Any active leases will be automatically released when the blob's expiry time
+ * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed.
+ *
+ * <p> This lease information is persisted and calling this more than once will result in
+ * latest lease overriding any previous lease.
+ *
+ * <p> When an app acquires a lease on a blob, the System will try to keep this
+ * blob around but note that it can still be deleted if it was requested by the user.
+ *
+ * <p> In case the resource name for the {@code descriptionResId} is modified as part of
+ * an app update, apps should re-acquire the lease with the new resource id.
+ *
+ * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
+ * acquire a lease for.
+ * @param descriptionResId the resource id for a short description string that can be surfaced
+ * to the user explaining what the blob is used for.
+ *
+ * @throws IOException when there is an I/O error while acquiring a lease to the blob.
+ * @throws SecurityException when the blob represented by the {@code blobHandle} does not
+ * exist or the caller does not have access to it.
+ * @throws IllegalArgumentException when {@code blobHandle} is invalid.
+ * @throws LimitExceededException when a lease could not be acquired, such as when the
+ * caller is trying to acquire too many leases or acquire
+ * leases on too much data. Apps can avoid this by checking
+ * the remaining quota using
+ * {@link #getRemainingLeaseQuotaBytes()} before trying to
+ * acquire a lease.
+ *
+ * @see #acquireLease(BlobHandle, int, long)
+ * @see #acquireLease(BlobHandle, CharSequence, long)
+ */
+ public void acquireLease(@NonNull BlobHandle blobHandle, @IdRes int descriptionResId)
+ throws IOException {
+ acquireLease(blobHandle, descriptionResId, 0);
+ }
+
+ /**
+ * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the
+ * system that the caller wants the blob to be kept around.
+ *
+ * <p> This is a variant of {@link #acquireLease(BlobHandle, int)} taking a {@link CharSequence}
+ * for {@code description}. It is highly recommended that callers only use this when a valid
+ * resource ID for {@code description} could not be provided. Otherwise, apps should prefer
+ * using {@link #acquireLease(BlobHandle, int)} which will allow {@code description} to be
+ * localized.
+ *
+ * <p> This is similar to {@link #acquireLease(BlobHandle, CharSequence, long)} except clients
+ * don't have to specify the lease expiry time upfront using this API and need to explicitly
+ * release the lease using {@link #releaseLease(BlobHandle)} when they no longer like to keep
+ * a blob around.
+ *
+ * <p> Any active leases will be automatically released when the blob's expiry time
+ * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed.
+ *
+ * <p> This lease information is persisted and calling this more than once will result in
+ * latest lease overriding any previous lease.
+ *
+ * <p> When an app acquires a lease on a blob, the System will try to keep this
+ * blob around but note that it can still be deleted if it was requested by the user.
+ *
+ * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
+ * acquire a lease for.
+ * @param description a short description string that can be surfaced
+ * to the user explaining what the blob is used for. It is recommended to
+ * keep this description brief. This may be truncated and
+ * ellipsized if it is too long to be displayed to the user.
+ *
+ * @throws IOException when there is an I/O error while acquiring a lease to the blob.
+ * @throws SecurityException when the blob represented by the {@code blobHandle} does not
+ * exist or the caller does not have access to it.
+ * @throws IllegalArgumentException when {@code blobHandle} is invalid.
+ * @throws LimitExceededException when a lease could not be acquired, such as when the
+ * caller is trying to acquire too many leases or acquire
+ * leases on too much data. Apps can avoid this by checking
+ * the remaining quota using
+ * {@link #getRemainingLeaseQuotaBytes()} before trying to
+ * acquire a lease.
+ *
+ * @see #acquireLease(BlobHandle, int)
+ * @see #acquireLease(BlobHandle, CharSequence, long)
+ */
+ public void acquireLease(@NonNull BlobHandle blobHandle, @NonNull CharSequence description)
+ throws IOException {
+ acquireLease(blobHandle, description, 0);
+ }
+
+ /**
+ * Release any active lease to the blob represented by {@code blobHandle} which is
+ * currently held by the caller.
+ *
+ * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to
+ * release the lease for.
+ *
+ * @throws IOException when there is an I/O error while releasing the release to the blob.
+ * @throws SecurityException when the blob represented by the {@code blobHandle} does not
+ * exist or the caller does not have access to it.
+ * @throws IllegalArgumentException when {@code blobHandle} is invalid.
+ */
+ public void releaseLease(@NonNull BlobHandle blobHandle) throws IOException {
+ try {
+ mService.releaseLease(blobHandle, mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return the remaining quota size for acquiring a lease (in bytes) which indicates the
+ * remaining amount of data that an app can acquire a lease on before the System starts
+ * rejecting lease requests.
+ *
+ * If an app wants to acquire a lease on a blob but the remaining quota size is not sufficient,
+ * then it can try releasing leases on any older blobs which are not needed anymore.
+ *
+ * @return the remaining quota size for acquiring a lease.
+ */
+ public @IntRange(from = 0) long getRemainingLeaseQuotaBytes() {
+ try {
+ return mService.getRemainingLeaseQuotaBytes(mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Wait until any pending tasks (like persisting data to disk) have finished.
+ *
+ * @hide
+ */
+ @TestApi
+ public void waitForIdle(long timeoutMillis) throws InterruptedException, TimeoutException {
+ try {
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ mService.waitForIdle(new RemoteCallback((result) -> countDownLatch.countDown()));
+ if (!countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS)) {
+ throw new TimeoutException("Timed out waiting for service to become idle");
+ }
+ } catch (ParcelableException e) {
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @NonNull
+ public List<BlobInfo> queryBlobsForUser(@NonNull UserHandle user) throws IOException {
+ try {
+ return mService.queryBlobsForUser(user.getIdentifier());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void deleteBlob(@NonNull BlobInfo blobInfo) throws IOException {
+ try {
+ mService.deleteBlob(blobInfo.getId());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return the {@link BlobHandle BlobHandles} corresponding to the data blobs that
+ * the calling app currently has a lease on.
+ *
+ * @return a list of {@link BlobHandle BlobHandles} that the caller has a lease on.
+ */
+ @NonNull
+ public List<BlobHandle> getLeasedBlobs() throws IOException {
+ try {
+ return mService.getLeasedBlobs(mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return {@link LeaseInfo} representing a lease acquired using
+ * {@link #acquireLease(BlobHandle, int)} or one of it's other variants,
+ * or {@code null} if there is no lease acquired.
+ *
+ * @throws SecurityException when the blob represented by the {@code blobHandle} does not
+ * exist or the caller does not have access to it.
+ * @throws IllegalArgumentException when {@code blobHandle} is invalid.
+ *
+ * @hide
+ */
+ @TestApi
+ @Nullable
+ public LeaseInfo getLeaseInfo(@NonNull BlobHandle blobHandle) throws IOException {
+ try {
+ return mService.getLeaseInfo(blobHandle, mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Represents an ongoing session of a blob's contribution to the blob store managed by the
+ * system.
+ *
+ * <p> Clients that want to contribute a blob need to first create a {@link Session} using
+ * {@link #createSession(BlobHandle)} and once the session is created, clients can open and
+ * close this session multiple times using {@link #openSession(long)} and
+ * {@link Session#close()} before committing it using
+ * {@link Session#commit(Executor, Consumer)}, at which point system will take
+ * ownership of the blob and the client can no longer make any modifications to the blob's
+ * content.
+ */
+ public static class Session implements Closeable {
+ private final IBlobStoreSession mSession;
+
+ private Session(@NonNull IBlobStoreSession session) {
+ mSession = session;
+ }
+
+ /**
+ * Opens a file descriptor to write a blob into the session.
+ *
+ * <p> The returned file descriptor will start writing data at the requested offset
+ * in the underlying file, which can be used to resume a partially
+ * written file. If a valid file length is specified, the system will
+ * preallocate the underlying disk space to optimize placement on disk.
+ * It is strongly recommended to provide a valid file length when known.
+ *
+ * @param offsetBytes offset into the file to begin writing at, or 0 to
+ * start at the beginning of the file.
+ * @param lengthBytes total size of the file being written, used to
+ * preallocate the underlying disk space, or -1 if unknown.
+ * The system may clear various caches as needed to allocate
+ * this space.
+ *
+ * @return a {@link ParcelFileDescriptor} for writing to the blob file.
+ *
+ * @throws IOException when there is an I/O error while opening the file to write.
+ * @throws SecurityException when the caller is not the owner of the session.
+ * @throws IllegalStateException when the caller tries to write to the file after it is
+ * abandoned (using {@link #abandon()})
+ * or committed (using {@link #commit})
+ * or closed (using {@link #close()}).
+ */
+ public @NonNull ParcelFileDescriptor openWrite(@BytesLong long offsetBytes,
+ @BytesLong long lengthBytes) throws IOException {
+ try {
+ final ParcelFileDescriptor pfd = mSession.openWrite(offsetBytes, lengthBytes);
+ pfd.seekTo(offsetBytes);
+ return pfd;
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Opens a file descriptor to read the blob content already written into this session.
+ *
+ * @return a {@link ParcelFileDescriptor} for reading from the blob file.
+ *
+ * @throws IOException when there is an I/O error while opening the file to read.
+ * @throws SecurityException when the caller is not the owner of the session.
+ * @throws IllegalStateException when the caller tries to read the file after it is
+ * abandoned (using {@link #abandon()})
+ * or closed (using {@link #close()}).
+ */
+ public @NonNull ParcelFileDescriptor openRead() throws IOException {
+ try {
+ return mSession.openRead();
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the size of the blob file that was written to the session so far.
+ *
+ * @return the size of the blob file so far.
+ *
+ * @throws IOException when there is an I/O error while opening the file to read.
+ * @throws SecurityException when the caller is not the owner of the session.
+ * @throws IllegalStateException when the caller tries to get the file size after it is
+ * abandoned (using {@link #abandon()})
+ * or closed (using {@link #close()}).
+ */
+ public @BytesLong long getSize() throws IOException {
+ try {
+ return mSession.getSize();
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Close this session. It can be re-opened for writing/reading if it has not been
+ * abandoned (using {@link #abandon}) or committed (using {@link #commit}).
+ *
+ * @throws IOException when there is an I/O error while closing the session.
+ * @throws SecurityException when the caller is not the owner of the session.
+ */
+ public void close() throws IOException {
+ try {
+ mSession.close();
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Abandon this session and delete any data that was written to this session so far.
+ *
+ * @throws IOException when there is an I/O error while abandoning the session.
+ * @throws SecurityException when the caller is not the owner of the session.
+ * @throws IllegalStateException when the caller tries to abandon a session which was
+ * already finalized.
+ */
+ public void abandon() throws IOException {
+ try {
+ mSession.abandon();
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Allow {@code packageName} with a particular signing certificate to access this blob
+ * data once it is committed using a {@link BlobHandle} representing the blob.
+ *
+ * <p> This needs to be called before committing the blob using
+ * {@link #commit(Executor, Consumer)}.
+ *
+ * @param packageName the name of the package which should be allowed to access the blob.
+ * @param certificate the input bytes representing a certificate of type
+ * {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
+ *
+ * @throws IOException when there is an I/O error while changing the access.
+ * @throws SecurityException when the caller is not the owner of the session.
+ * @throws IllegalStateException when the caller tries to change access for a blob which is
+ * already committed.
+ * @throws LimitExceededException when the caller tries to explicitly allow too
+ * many packages using this API.
+ */
+ public void allowPackageAccess(@NonNull String packageName, @NonNull byte[] certificate)
+ throws IOException {
+ try {
+ mSession.allowPackageAccess(packageName, certificate);
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ e.maybeRethrow(LimitExceededException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns {@code true} if access has been allowed for a {@code packageName} using either
+ * {@link #allowPackageAccess(String, byte[])}.
+ * Otherwise, {@code false}.
+ *
+ * @param packageName the name of the package to check the access for.
+ * @param certificate the input bytes representing a certificate of type
+ * {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
+ *
+ * @throws IOException when there is an I/O error while getting the access type.
+ * @throws IllegalStateException when the caller tries to get access type from a session
+ * which is closed or abandoned.
+ */
+ public boolean isPackageAccessAllowed(@NonNull String packageName,
+ @NonNull byte[] certificate) throws IOException {
+ try {
+ return mSession.isPackageAccessAllowed(packageName, certificate);
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Allow packages which are signed with the same certificate as the caller to access this
+ * blob data once it is committed using a {@link BlobHandle} representing the blob.
+ *
+ * <p> This needs to be called before committing the blob using
+ * {@link #commit(Executor, Consumer)}.
+ *
+ * @throws IOException when there is an I/O error while changing the access.
+ * @throws SecurityException when the caller is not the owner of the session.
+ * @throws IllegalStateException when the caller tries to change access for a blob which is
+ * already committed.
+ */
+ public void allowSameSignatureAccess() throws IOException {
+ try {
+ mSession.allowSameSignatureAccess();
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns {@code true} if access has been allowed for packages signed with the same
+ * certificate as the caller by using {@link #allowSameSignatureAccess()}.
+ * Otherwise, {@code false}.
+ *
+ * @throws IOException when there is an I/O error while getting the access type.
+ * @throws IllegalStateException when the caller tries to get access type from a session
+ * which is closed or abandoned.
+ */
+ public boolean isSameSignatureAccessAllowed() throws IOException {
+ try {
+ return mSession.isSameSignatureAccessAllowed();
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Allow any app on the device to access this blob data once it is committed using
+ * a {@link BlobHandle} representing the blob.
+ *
+ * <p><strong>Note:</strong> This is only meant to be used from libraries and SDKs where
+ * the apps which we want to allow access is not known ahead of time.
+ * If a blob is being committed to be shared with a particular set of apps, it is highly
+ * recommended to use {@link #allowPackageAccess(String, byte[])} instead.
+ *
+ * <p> This needs to be called before committing the blob using
+ * {@link #commit(Executor, Consumer)}.
+ *
+ * @throws IOException when there is an I/O error while changing the access.
+ * @throws SecurityException when the caller is not the owner of the session.
+ * @throws IllegalStateException when the caller tries to change access for a blob which is
+ * already committed.
+ */
+ public void allowPublicAccess() throws IOException {
+ try {
+ mSession.allowPublicAccess();
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns {@code true} if public access has been allowed by using
+ * {@link #allowPublicAccess()}. Otherwise, {@code false}.
+ *
+ * @throws IOException when there is an I/O error while getting the access type.
+ * @throws IllegalStateException when the caller tries to get access type from a session
+ * which is closed or abandoned.
+ */
+ public boolean isPublicAccessAllowed() throws IOException {
+ try {
+ return mSession.isPublicAccessAllowed();
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Commit the file that was written so far to this session to the blob store maintained by
+ * the system.
+ *
+ * <p> Once this method is called, the session is finalized and no additional
+ * mutations can be performed on the session. If the device reboots
+ * before the session has been finalized, you may commit the session again.
+ *
+ * <p> Note that this commit operation will fail if the hash of the data written so far
+ * to this session does not match with the one used for
+ * {@link BlobHandle#createWithSha256(byte[], CharSequence, long, String)} BlobHandle}
+ * associated with this session.
+ *
+ * <p> Committing the same data more than once will result in replacing the corresponding
+ * access mode (via calling one of {@link #allowPackageAccess(String, byte[])},
+ * {@link #allowSameSignatureAccess()}, etc) with the latest one.
+ *
+ * @param executor the executor on which result callback will be invoked.
+ * @param resultCallback a callback to receive the commit result. when the result is
+ * {@code 0}, it indicates success. Otherwise, failure.
+ *
+ * @throws IOException when there is an I/O error while committing the session.
+ * @throws SecurityException when the caller is not the owner of the session.
+ * @throws IllegalArgumentException when the passed parameters are not valid.
+ * @throws IllegalStateException when the caller tries to commit a session which was
+ * already finalized.
+ */
+ public void commit(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<Integer> resultCallback) throws IOException {
+ try {
+ mSession.commit(new IBlobCommitCallback.Stub() {
+ public void onResult(int result) {
+ executor.execute(PooledLambda.obtainRunnable(
+ Consumer::accept, resultCallback, result));
+ }
+ });
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+}
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/IBlobCommitCallback.aidl b/apex/blobstore/framework/java/android/app/blob/IBlobCommitCallback.aidl
new file mode 100644
index 000000000000..a9b30e20f5bd
--- /dev/null
+++ b/apex/blobstore/framework/java/android/app/blob/IBlobCommitCallback.aidl
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2020 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} */
+oneway interface IBlobCommitCallback {
+ void onResult(int result);
+} \ No newline at end of file
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..39a9fb4bb1f4
--- /dev/null
+++ b/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl
@@ -0,0 +1,43 @@
+/**
+ * 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.blob.BlobHandle;
+import android.app.blob.BlobInfo;
+import android.app.blob.IBlobStoreSession;
+import android.app.blob.LeaseInfo;
+import android.os.RemoteCallback;
+
+/** {@hide} */
+interface IBlobStoreManager {
+ long createSession(in BlobHandle handle, in String packageName);
+ IBlobStoreSession openSession(long sessionId, in String packageName);
+ ParcelFileDescriptor openBlob(in BlobHandle handle, in String packageName);
+ void abandonSession(long sessionId, in String packageName);
+
+ void acquireLease(in BlobHandle handle, int descriptionResId, in CharSequence description,
+ long leaseTimeoutMillis, in String packageName);
+ void releaseLease(in BlobHandle handle, in String packageName);
+ long getRemainingLeaseQuotaBytes(String packageName);
+
+ void waitForIdle(in RemoteCallback callback);
+
+ List<BlobInfo> queryBlobsForUser(int userId);
+ void deleteBlob(long blobId);
+
+ List<BlobHandle> getLeasedBlobs(in String packageName);
+ LeaseInfo getLeaseInfo(in BlobHandle blobHandle, in String packageName);
+} \ No newline at end of file
diff --git a/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl b/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl
new file mode 100644
index 000000000000..4035b96938d9
--- /dev/null
+++ b/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 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.blob.IBlobCommitCallback;
+import android.os.ParcelFileDescriptor;
+
+/** {@hide} */
+interface IBlobStoreSession {
+ ParcelFileDescriptor openWrite(long offsetBytes, long lengthBytes);
+ ParcelFileDescriptor openRead();
+
+ void allowPackageAccess(in String packageName, in byte[] certificate);
+ void allowSameSignatureAccess();
+ void allowPublicAccess();
+
+ boolean isPackageAccessAllowed(in String packageName, in byte[] certificate);
+ boolean isSameSignatureAccessAllowed();
+ boolean isPublicAccessAllowed();
+
+ long getSize();
+ void close();
+ void abandon();
+
+ void commit(in IBlobCommitCallback callback);
+} \ No newline at end of file
diff --git a/apex/blobstore/framework/java/android/app/blob/LeaseInfo.aidl b/apex/blobstore/framework/java/android/app/blob/LeaseInfo.aidl
new file mode 100644
index 000000000000..908885731bb1
--- /dev/null
+++ b/apex/blobstore/framework/java/android/app/blob/LeaseInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 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} */
+parcelable LeaseInfo; \ No newline at end of file
diff --git a/apex/blobstore/framework/java/android/app/blob/LeaseInfo.java b/apex/blobstore/framework/java/android/app/blob/LeaseInfo.java
new file mode 100644
index 000000000000..fef50c9e8dba
--- /dev/null
+++ b/apex/blobstore/framework/java/android/app/blob/LeaseInfo.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2020 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.CurrentTimeMillisLong;
+import android.annotation.IdRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.List;
+
+/**
+ * Class to provide information about a lease (acquired using
+ * {@link BlobStoreManager#acquireLease(BlobHandle, int)} or one of it's variants)
+ * for a shared blob.
+ *
+ * @hide
+ */
+@TestApi
+public final class LeaseInfo implements Parcelable {
+ private final String mPackageName;
+ private final long mExpiryTimeMillis;
+ private final int mDescriptionResId;
+ private final CharSequence mDescription;
+
+ public LeaseInfo(@NonNull String packageName, @CurrentTimeMillisLong long expiryTimeMs,
+ @IdRes int descriptionResId, @Nullable CharSequence description) {
+ mPackageName = packageName;
+ mExpiryTimeMillis = expiryTimeMs;
+ mDescriptionResId = descriptionResId;
+ mDescription = description;
+ }
+
+ private LeaseInfo(Parcel in) {
+ mPackageName = in.readString();
+ mExpiryTimeMillis = in.readLong();
+ mDescriptionResId = in.readInt();
+ mDescription = in.readCharSequence();
+ }
+
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ @CurrentTimeMillisLong
+ public long getExpiryTimeMillis() {
+ return mExpiryTimeMillis;
+ }
+
+ @IdRes
+ public int getDescriptionResId() {
+ return mDescriptionResId;
+ }
+
+ @Nullable
+ public CharSequence getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mPackageName);
+ dest.writeLong(mExpiryTimeMillis);
+ dest.writeInt(mDescriptionResId);
+ dest.writeCharSequence(mDescription);
+ }
+
+ @Override
+ public String toString() {
+ return "LeaseInfo {"
+ + "package: " + mPackageName + ","
+ + "expiryMs: " + mExpiryTimeMillis + ","
+ + "descriptionResId: " + mDescriptionResId + ","
+ + "description: " + mDescription + ","
+ + "}";
+ }
+
+ private String toShortString() {
+ return mPackageName;
+ }
+
+ static String toShortString(List<LeaseInfo> leaseInfos) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("[");
+ for (int i = 0, size = leaseInfos.size(); i < size; ++i) {
+ sb.append(leaseInfos.get(i).toShortString());
+ sb.append(",");
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<LeaseInfo> CREATOR = new Creator<LeaseInfo>() {
+ @Override
+ @NonNull
+ public LeaseInfo createFromParcel(Parcel source) {
+ return new LeaseInfo(source);
+ }
+
+ @Override
+ @NonNull
+ public LeaseInfo[] newArray(int size) {
+ return new LeaseInfo[size];
+ }
+ };
+}
diff --git a/apex/blobstore/framework/java/android/app/blob/XmlTags.java b/apex/blobstore/framework/java/android/app/blob/XmlTags.java
new file mode 100644
index 000000000000..656749d1a8c4
--- /dev/null
+++ b/apex/blobstore/framework/java/android/app/blob/XmlTags.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2020 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 */
+public final class XmlTags {
+ public static final String ATTR_VERSION = "v";
+
+ public static final String TAG_SESSIONS = "ss";
+ public static final String TAG_BLOBS = "bs";
+
+ // For BlobStoreSession
+ public static final String TAG_SESSION = "s";
+ public static final String ATTR_ID = "id";
+ public static final String ATTR_PACKAGE = "p";
+ public static final String ATTR_UID = "u";
+ public static final String ATTR_CREATION_TIME_MS = "crt";
+
+ // For BlobMetadata
+ public static final String TAG_BLOB = "b";
+ public static final String ATTR_USER_ID = "us";
+
+ // For BlobAccessMode
+ public static final String TAG_ACCESS_MODE = "am";
+ public static final String ATTR_TYPE = "t";
+ public static final String TAG_WHITELISTED_PACKAGE = "wl";
+ public static final String ATTR_CERTIFICATE = "ct";
+
+ // For BlobHandle
+ public static final String TAG_BLOB_HANDLE = "bh";
+ public static final String ATTR_ALGO = "al";
+ public static final String ATTR_DIGEST = "dg";
+ public static final String ATTR_LABEL = "lbl";
+ public static final String ATTR_EXPIRY_TIME = "ex";
+ public static final String ATTR_TAG = "tg";
+
+ // For committer
+ public static final String TAG_COMMITTER = "c";
+ public static final String ATTR_COMMIT_TIME_MS = "cmt";
+
+ // For leasee
+ public static final String TAG_LEASEE = "l";
+ public static final String ATTR_DESCRIPTION_RES_NAME = "rn";
+ public static final String ATTR_DESCRIPTION = "d";
+}
diff --git a/apex/blobstore/service/Android.bp b/apex/blobstore/service/Android.bp
new file mode 100644
index 000000000000..22b0cbe91e23
--- /dev/null
+++ b/apex/blobstore/service/Android.bp
@@ -0,0 +1,28 @@
+// 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: "service-blobstore",
+ installable: true,
+
+ srcs: [
+ "java/**/*.java",
+ ],
+
+ libs: [
+ "framework",
+ "services.core",
+ "services.usage",
+ ],
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java b/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java
new file mode 100644
index 000000000000..ba0fab6b4bc5
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2020 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 static android.app.blob.XmlTags.ATTR_CERTIFICATE;
+import static android.app.blob.XmlTags.ATTR_PACKAGE;
+import static android.app.blob.XmlTags.ATTR_TYPE;
+import static android.app.blob.XmlTags.TAG_WHITELISTED_PACKAGE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.util.ArraySet;
+import android.util.Base64;
+import android.util.DebugUtils;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Class for representing how a blob can be shared.
+ *
+ * Note that this class is not thread-safe, callers need to take care of synchronizing access.
+ */
+class BlobAccessMode {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {
+ ACCESS_TYPE_PRIVATE,
+ ACCESS_TYPE_PUBLIC,
+ ACCESS_TYPE_SAME_SIGNATURE,
+ ACCESS_TYPE_WHITELIST,
+ })
+ @interface AccessType {}
+ public static final int ACCESS_TYPE_PRIVATE = 1 << 0;
+ public static final int ACCESS_TYPE_PUBLIC = 1 << 1;
+ public static final int ACCESS_TYPE_SAME_SIGNATURE = 1 << 2;
+ public static final int ACCESS_TYPE_WHITELIST = 1 << 3;
+
+ private int mAccessType = ACCESS_TYPE_PRIVATE;
+
+ private final ArraySet<PackageIdentifier> mWhitelistedPackages = new ArraySet<>();
+
+ void allow(BlobAccessMode other) {
+ if ((other.mAccessType & ACCESS_TYPE_WHITELIST) != 0) {
+ mWhitelistedPackages.addAll(other.mWhitelistedPackages);
+ }
+ mAccessType |= other.mAccessType;
+ }
+
+ void allowPublicAccess() {
+ mAccessType |= ACCESS_TYPE_PUBLIC;
+ }
+
+ void allowSameSignatureAccess() {
+ mAccessType |= ACCESS_TYPE_SAME_SIGNATURE;
+ }
+
+ void allowPackageAccess(@NonNull String packageName, @NonNull byte[] certificate) {
+ mAccessType |= ACCESS_TYPE_WHITELIST;
+ mWhitelistedPackages.add(PackageIdentifier.create(packageName, certificate));
+ }
+
+ boolean isPublicAccessAllowed() {
+ return (mAccessType & ACCESS_TYPE_PUBLIC) != 0;
+ }
+
+ boolean isSameSignatureAccessAllowed() {
+ return (mAccessType & ACCESS_TYPE_SAME_SIGNATURE) != 0;
+ }
+
+ boolean isPackageAccessAllowed(@NonNull String packageName, @NonNull byte[] certificate) {
+ if ((mAccessType & ACCESS_TYPE_WHITELIST) == 0) {
+ return false;
+ }
+ return mWhitelistedPackages.contains(PackageIdentifier.create(packageName, certificate));
+ }
+
+ boolean isAccessAllowedForCaller(Context context,
+ @NonNull String callingPackage, @NonNull String committerPackage) {
+ if ((mAccessType & ACCESS_TYPE_PUBLIC) != 0) {
+ return true;
+ }
+
+ final PackageManager pm = context.getPackageManager();
+ if ((mAccessType & ACCESS_TYPE_SAME_SIGNATURE) != 0) {
+ if (pm.checkSignatures(committerPackage, callingPackage)
+ == PackageManager.SIGNATURE_MATCH) {
+ return true;
+ }
+ }
+
+ if ((mAccessType & ACCESS_TYPE_WHITELIST) != 0) {
+ for (int i = 0; i < mWhitelistedPackages.size(); ++i) {
+ final PackageIdentifier packageIdentifier = mWhitelistedPackages.valueAt(i);
+ if (packageIdentifier.packageName.equals(callingPackage)
+ && pm.hasSigningCertificate(callingPackage, packageIdentifier.certificate,
+ PackageManager.CERT_INPUT_SHA256)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ int getAccessType() {
+ return mAccessType;
+ }
+
+ int getNumWhitelistedPackages() {
+ return mWhitelistedPackages.size();
+ }
+
+ void dump(IndentingPrintWriter fout) {
+ fout.println("accessType: " + DebugUtils.flagsToString(
+ BlobAccessMode.class, "ACCESS_TYPE_", mAccessType));
+ fout.print("Whitelisted pkgs:");
+ if (mWhitelistedPackages.isEmpty()) {
+ fout.println(" (Empty)");
+ } else {
+ fout.increaseIndent();
+ for (int i = 0, count = mWhitelistedPackages.size(); i < count; ++i) {
+ fout.println(mWhitelistedPackages.valueAt(i).toString());
+ }
+ fout.decreaseIndent();
+ }
+ }
+
+ void writeToXml(@NonNull XmlSerializer out) throws IOException {
+ XmlUtils.writeIntAttribute(out, ATTR_TYPE, mAccessType);
+ for (int i = 0, count = mWhitelistedPackages.size(); i < count; ++i) {
+ out.startTag(null, TAG_WHITELISTED_PACKAGE);
+ final PackageIdentifier packageIdentifier = mWhitelistedPackages.valueAt(i);
+ XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, packageIdentifier.packageName);
+ XmlUtils.writeByteArrayAttribute(out, ATTR_CERTIFICATE, packageIdentifier.certificate);
+ out.endTag(null, TAG_WHITELISTED_PACKAGE);
+ }
+ }
+
+ @NonNull
+ static BlobAccessMode createFromXml(@NonNull XmlPullParser in)
+ throws IOException, XmlPullParserException {
+ final BlobAccessMode blobAccessMode = new BlobAccessMode();
+
+ final int accessType = XmlUtils.readIntAttribute(in, ATTR_TYPE);
+ blobAccessMode.mAccessType = accessType;
+
+ final int depth = in.getDepth();
+ while (XmlUtils.nextElementWithin(in, depth)) {
+ if (TAG_WHITELISTED_PACKAGE.equals(in.getName())) {
+ final String packageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE);
+ final byte[] certificate = XmlUtils.readByteArrayAttribute(in, ATTR_CERTIFICATE);
+ blobAccessMode.allowPackageAccess(packageName, certificate);
+ }
+ }
+ return blobAccessMode;
+ }
+
+ private static final class PackageIdentifier {
+ public final String packageName;
+ public final byte[] certificate;
+
+ private PackageIdentifier(@NonNull String packageName, @NonNull byte[] certificate) {
+ this.packageName = packageName;
+ this.certificate = certificate;
+ }
+
+ public static PackageIdentifier create(@NonNull String packageName,
+ @NonNull byte[] certificate) {
+ return new PackageIdentifier(packageName, certificate);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof PackageIdentifier)) {
+ return false;
+ }
+ final PackageIdentifier other = (PackageIdentifier) obj;
+ return this.packageName.equals(other.packageName)
+ && Arrays.equals(this.certificate, other.certificate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(packageName, Arrays.hashCode(certificate));
+ }
+
+ @Override
+ public String toString() {
+ return "[" + packageName + ", "
+ + Base64.encodeToString(certificate, Base64.NO_WRAP) + "]";
+ }
+ }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
new file mode 100644
index 000000000000..0b760a621d22
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
@@ -0,0 +1,824 @@
+/*
+ * Copyright 2020 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 static android.app.blob.XmlTags.ATTR_COMMIT_TIME_MS;
+import static android.app.blob.XmlTags.ATTR_DESCRIPTION;
+import static android.app.blob.XmlTags.ATTR_DESCRIPTION_RES_NAME;
+import static android.app.blob.XmlTags.ATTR_EXPIRY_TIME;
+import static android.app.blob.XmlTags.ATTR_ID;
+import static android.app.blob.XmlTags.ATTR_PACKAGE;
+import static android.app.blob.XmlTags.ATTR_UID;
+import static android.app.blob.XmlTags.ATTR_USER_ID;
+import static android.app.blob.XmlTags.TAG_ACCESS_MODE;
+import static android.app.blob.XmlTags.TAG_BLOB_HANDLE;
+import static android.app.blob.XmlTags.TAG_COMMITTER;
+import static android.app.blob.XmlTags.TAG_LEASEE;
+import static android.os.Process.INVALID_UID;
+import static android.system.OsConstants.O_RDONLY;
+import static android.text.format.Formatter.FLAG_IEC_UNITS;
+import static android.text.format.Formatter.formatFileSize;
+
+import static com.android.server.blob.BlobStoreConfig.TAG;
+import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_COMMIT_TIME;
+import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_DESC_RES_NAME;
+import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_STRING_DESC;
+import static com.android.server.blob.BlobStoreConfig.hasLeaseWaitTimeElapsed;
+import static com.android.server.blob.BlobStoreUtils.getDescriptionResourceId;
+import static com.android.server.blob.BlobStoreUtils.getPackageResources;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.blob.BlobHandle;
+import android.app.blob.LeaseInfo;
+import android.content.Context;
+import android.content.res.ResourceId;
+import android.content.res.Resources;
+import android.os.ParcelFileDescriptor;
+import android.os.RevocableFileDescriptor;
+import android.os.UserHandle;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.StatsEvent;
+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.internal.util.XmlUtils;
+import com.android.server.blob.BlobStoreManagerService.DumpArgs;
+
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+class BlobMetadata {
+ private final Object mMetadataLock = new Object();
+
+ private final Context mContext;
+
+ private final long mBlobId;
+ private final BlobHandle mBlobHandle;
+ private final int mUserId;
+
+ @GuardedBy("mMetadataLock")
+ private final ArraySet<Committer> mCommitters = new ArraySet<>();
+
+ @GuardedBy("mMetadataLock")
+ private final ArraySet<Leasee> mLeasees = new ArraySet<>();
+
+ /**
+ * Contains packageName -> {RevocableFileDescriptors}.
+ *
+ * Keep track of RevocableFileDescriptors given to clients which are not yet revoked/closed so
+ * that when clients access is revoked or the blob gets deleted, we can be sure that clients
+ * do not have any reference to the blob and the space occupied by the blob can be freed.
+ */
+ @GuardedBy("mRevocableFds")
+ private final ArrayMap<String, ArraySet<RevocableFileDescriptor>> mRevocableFds =
+ new ArrayMap<>();
+
+ // Do not access this directly, instead use #getBlobFile().
+ private File mBlobFile;
+
+ BlobMetadata(Context context, long blobId, BlobHandle blobHandle, int userId) {
+ mContext = context;
+ this.mBlobId = blobId;
+ this.mBlobHandle = blobHandle;
+ this.mUserId = userId;
+ }
+
+ long getBlobId() {
+ return mBlobId;
+ }
+
+ BlobHandle getBlobHandle() {
+ return mBlobHandle;
+ }
+
+ int getUserId() {
+ return mUserId;
+ }
+
+ void addOrReplaceCommitter(@NonNull Committer committer) {
+ synchronized (mMetadataLock) {
+ // We need to override the committer data, so first remove any existing
+ // committer before adding the new one.
+ mCommitters.remove(committer);
+ mCommitters.add(committer);
+ }
+ }
+
+ void setCommitters(ArraySet<Committer> committers) {
+ synchronized (mMetadataLock) {
+ mCommitters.clear();
+ mCommitters.addAll(committers);
+ }
+ }
+
+ void removeCommitter(@NonNull String packageName, int uid) {
+ synchronized (mMetadataLock) {
+ mCommitters.removeIf((committer) ->
+ committer.uid == uid && committer.packageName.equals(packageName));
+ }
+ }
+
+ void removeCommitter(@NonNull Committer committer) {
+ synchronized (mMetadataLock) {
+ mCommitters.remove(committer);
+ }
+ }
+
+ void removeCommittersFromUnknownPkgs(SparseArray<String> knownPackages) {
+ synchronized (mMetadataLock) {
+ mCommitters.removeIf(committer ->
+ !committer.packageName.equals(knownPackages.get(committer.uid)));
+ }
+ }
+
+ @Nullable
+ Committer getExistingCommitter(@NonNull String packageName, int uid) {
+ synchronized (mCommitters) {
+ for (int i = 0, size = mCommitters.size(); i < size; ++i) {
+ final Committer committer = mCommitters.valueAt(i);
+ if (committer.uid == uid && committer.packageName.equals(packageName)) {
+ return committer;
+ }
+ }
+ }
+ return null;
+ }
+
+ void addOrReplaceLeasee(String callingPackage, int callingUid, int descriptionResId,
+ CharSequence description, long leaseExpiryTimeMillis) {
+ synchronized (mMetadataLock) {
+ // We need to override the leasee data, so first remove any existing
+ // leasee before adding the new one.
+ final Leasee leasee = new Leasee(mContext, callingPackage, callingUid,
+ descriptionResId, description, leaseExpiryTimeMillis);
+ mLeasees.remove(leasee);
+ mLeasees.add(leasee);
+ }
+ }
+
+ void setLeasees(ArraySet<Leasee> leasees) {
+ synchronized (mMetadataLock) {
+ mLeasees.clear();
+ mLeasees.addAll(leasees);
+ }
+ }
+
+ void removeLeasee(String packageName, int uid) {
+ synchronized (mMetadataLock) {
+ mLeasees.removeIf((leasee) ->
+ leasee.uid == uid && leasee.packageName.equals(packageName));
+ }
+ }
+
+ void removeLeaseesFromUnknownPkgs(SparseArray<String> knownPackages) {
+ synchronized (mMetadataLock) {
+ mLeasees.removeIf(leasee ->
+ !leasee.packageName.equals(knownPackages.get(leasee.uid)));
+ }
+ }
+
+ void removeExpiredLeases() {
+ synchronized (mMetadataLock) {
+ mLeasees.removeIf(leasee -> !leasee.isStillValid());
+ }
+ }
+
+ boolean hasValidLeases() {
+ synchronized (mMetadataLock) {
+ for (int i = 0, size = mLeasees.size(); i < size; ++i) {
+ if (mLeasees.valueAt(i).isStillValid()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ long getSize() {
+ return getBlobFile().length();
+ }
+
+ boolean isAccessAllowedForCaller(@NonNull String callingPackage, int callingUid) {
+ // Don't allow the blob to be accessed after it's expiry time has passed.
+ if (getBlobHandle().isExpired()) {
+ return false;
+ }
+ synchronized (mMetadataLock) {
+ // Check if packageName already holds a lease on the blob.
+ for (int i = 0, size = mLeasees.size(); i < size; ++i) {
+ final Leasee leasee = mLeasees.valueAt(i);
+ if (leasee.isStillValid() && leasee.equals(callingPackage, callingUid)) {
+ return true;
+ }
+ }
+
+ for (int i = 0, size = mCommitters.size(); i < size; ++i) {
+ final Committer committer = mCommitters.valueAt(i);
+
+ // Check if the caller is the same package that committed the blob.
+ if (committer.equals(callingPackage, callingUid)) {
+ return true;
+ }
+
+ // Check if the caller is allowed access as per the access mode specified
+ // by the committer.
+ if (committer.blobAccessMode.isAccessAllowedForCaller(mContext,
+ callingPackage, committer.packageName)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ boolean isACommitter(@NonNull String packageName, int uid) {
+ synchronized (mMetadataLock) {
+ return isAnAccessor(mCommitters, packageName, uid);
+ }
+ }
+
+ boolean isALeasee(@Nullable String packageName, int uid) {
+ synchronized (mMetadataLock) {
+ final Leasee leasee = getAccessor(mLeasees, packageName, uid);
+ return leasee != null && leasee.isStillValid();
+ }
+ }
+
+ private static <T extends Accessor> boolean isAnAccessor(@NonNull ArraySet<T> accessors,
+ @Nullable String packageName, int uid) {
+ // Check if the package is an accessor of the data blob.
+ return getAccessor(accessors, packageName, uid) != null;
+ }
+
+ private static <T extends Accessor> T getAccessor(@NonNull ArraySet<T> accessors,
+ @Nullable String packageName, int uid) {
+ // Check if the package is an accessor of the data blob.
+ for (int i = 0, size = accessors.size(); i < size; ++i) {
+ final Accessor accessor = accessors.valueAt(i);
+ if (packageName != null && uid != INVALID_UID
+ && accessor.equals(packageName, uid)) {
+ return (T) accessor;
+ } else if (packageName != null && accessor.packageName.equals(packageName)) {
+ return (T) accessor;
+ } else if (uid != INVALID_UID && accessor.uid == uid) {
+ return (T) accessor;
+ }
+ }
+ return null;
+ }
+
+ boolean isALeasee(@NonNull String packageName) {
+ return isALeasee(packageName, INVALID_UID);
+ }
+
+ boolean isALeasee(int uid) {
+ return isALeasee(null, uid);
+ }
+
+ boolean hasOtherLeasees(@NonNull String packageName) {
+ return hasOtherLeasees(packageName, INVALID_UID);
+ }
+
+ boolean hasOtherLeasees(int uid) {
+ return hasOtherLeasees(null, uid);
+ }
+
+ private boolean hasOtherLeasees(@Nullable String packageName, int uid) {
+ synchronized (mMetadataLock) {
+ for (int i = 0, size = mLeasees.size(); i < size; ++i) {
+ final Leasee leasee = mLeasees.valueAt(i);
+ if (!leasee.isStillValid()) {
+ continue;
+ }
+ // TODO: Also exclude packages which are signed with same cert?
+ if (packageName != null && uid != INVALID_UID
+ && !leasee.equals(packageName, uid)) {
+ return true;
+ } else if (packageName != null && !leasee.packageName.equals(packageName)) {
+ return true;
+ } else if (uid != INVALID_UID && leasee.uid != uid) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ LeaseInfo getLeaseInfo(@NonNull String packageName, int uid) {
+ synchronized (mMetadataLock) {
+ for (int i = 0, size = mLeasees.size(); i < size; ++i) {
+ final Leasee leasee = mLeasees.valueAt(i);
+ if (!leasee.isStillValid()) {
+ continue;
+ }
+ if (leasee.uid == uid && leasee.packageName.equals(packageName)) {
+ final int descriptionResId = leasee.descriptionResEntryName == null
+ ? Resources.ID_NULL
+ : BlobStoreUtils.getDescriptionResourceId(
+ mContext, leasee.descriptionResEntryName, leasee.packageName,
+ UserHandle.getUserId(leasee.uid));
+ return new LeaseInfo(packageName, leasee.expiryTimeMillis,
+ descriptionResId, leasee.description);
+ }
+ }
+ }
+ return null;
+ }
+
+ void forEachLeasee(Consumer<Leasee> consumer) {
+ synchronized (mMetadataLock) {
+ mLeasees.forEach(consumer);
+ }
+ }
+
+ File getBlobFile() {
+ if (mBlobFile == null) {
+ mBlobFile = BlobStoreConfig.getBlobFile(mBlobId);
+ }
+ return mBlobFile;
+ }
+
+ ParcelFileDescriptor openForRead(String callingPackage) throws IOException {
+ // TODO: Add limit on opened fds
+ FileDescriptor fd;
+ try {
+ fd = Os.open(getBlobFile().getPath(), O_RDONLY, 0);
+ } catch (ErrnoException e) {
+ throw e.rethrowAsIOException();
+ }
+ try {
+ if (BlobStoreConfig.shouldUseRevocableFdForReads()) {
+ return createRevocableFd(fd, callingPackage);
+ } else {
+ return new ParcelFileDescriptor(fd);
+ }
+ } catch (IOException e) {
+ IoUtils.closeQuietly(fd);
+ throw e;
+ }
+ }
+
+ @NonNull
+ private ParcelFileDescriptor createRevocableFd(FileDescriptor fd,
+ String callingPackage) throws IOException {
+ final RevocableFileDescriptor revocableFd =
+ new RevocableFileDescriptor(mContext, fd);
+ synchronized (mRevocableFds) {
+ ArraySet<RevocableFileDescriptor> revocableFdsForPkg =
+ mRevocableFds.get(callingPackage);
+ if (revocableFdsForPkg == null) {
+ revocableFdsForPkg = new ArraySet<>();
+ mRevocableFds.put(callingPackage, revocableFdsForPkg);
+ }
+ revocableFdsForPkg.add(revocableFd);
+ }
+ revocableFd.addOnCloseListener((e) -> {
+ synchronized (mRevocableFds) {
+ final ArraySet<RevocableFileDescriptor> revocableFdsForPkg =
+ mRevocableFds.get(callingPackage);
+ if (revocableFdsForPkg != null) {
+ revocableFdsForPkg.remove(revocableFd);
+ if (revocableFdsForPkg.isEmpty()) {
+ mRevocableFds.remove(callingPackage);
+ }
+ }
+ }
+ });
+ return revocableFd.getRevocableFileDescriptor();
+ }
+
+ void destroy() {
+ revokeAllFds();
+ getBlobFile().delete();
+ }
+
+ private void revokeAllFds() {
+ synchronized (mRevocableFds) {
+ for (int i = 0, pkgCount = mRevocableFds.size(); i < pkgCount; ++i) {
+ final ArraySet<RevocableFileDescriptor> packageFds =
+ mRevocableFds.valueAt(i);
+ if (packageFds == null) {
+ continue;
+ }
+ for (int j = 0, fdCount = packageFds.size(); j < fdCount; ++j) {
+ packageFds.valueAt(j).revoke();
+ }
+ }
+ }
+ }
+
+ boolean shouldBeDeleted(boolean respectLeaseWaitTime) {
+ // Expired data blobs
+ if (getBlobHandle().isExpired()) {
+ return true;
+ }
+
+ // Blobs with no active leases
+ if ((!respectLeaseWaitTime || hasLeaseWaitTimeElapsedForAll())
+ && !hasValidLeases()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @VisibleForTesting
+ boolean hasLeaseWaitTimeElapsedForAll() {
+ for (int i = 0, size = mCommitters.size(); i < size; ++i) {
+ final Committer committer = mCommitters.valueAt(i);
+ if (!hasLeaseWaitTimeElapsed(committer.getCommitTimeMs())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ StatsEvent dumpAsStatsEvent(int atomTag) {
+ synchronized (mMetadataLock) {
+ ProtoOutputStream proto = new ProtoOutputStream();
+ // Write Committer data to proto format
+ for (int i = 0, size = mCommitters.size(); i < size; ++i) {
+ final Committer committer = mCommitters.valueAt(i);
+ final long token = proto.start(
+ BlobStatsEventProto.BlobCommitterListProto.COMMITTER);
+ proto.write(BlobStatsEventProto.BlobCommitterProto.UID, committer.uid);
+ proto.write(BlobStatsEventProto.BlobCommitterProto.COMMIT_TIMESTAMP_MILLIS,
+ committer.commitTimeMs);
+ proto.write(BlobStatsEventProto.BlobCommitterProto.ACCESS_MODE,
+ committer.blobAccessMode.getAccessType());
+ proto.write(BlobStatsEventProto.BlobCommitterProto.NUM_WHITELISTED_PACKAGE,
+ committer.blobAccessMode.getNumWhitelistedPackages());
+ proto.end(token);
+ }
+ final byte[] committersBytes = proto.getBytes();
+
+ proto = new ProtoOutputStream();
+ // Write Leasee data to proto format
+ for (int i = 0, size = mLeasees.size(); i < size; ++i) {
+ final Leasee leasee = mLeasees.valueAt(i);
+ final long token = proto.start(BlobStatsEventProto.BlobLeaseeListProto.LEASEE);
+ proto.write(BlobStatsEventProto.BlobLeaseeProto.UID, leasee.uid);
+ proto.write(BlobStatsEventProto.BlobLeaseeProto.LEASE_EXPIRY_TIMESTAMP_MILLIS,
+ leasee.expiryTimeMillis);
+ proto.end(token);
+ }
+ final byte[] leaseesBytes = proto.getBytes();
+
+ // Construct the StatsEvent to represent this Blob
+ return StatsEvent.newBuilder()
+ .setAtomId(atomTag)
+ .writeLong(mBlobId)
+ .writeLong(getSize())
+ .writeLong(mBlobHandle.getExpiryTimeMillis())
+ .writeByteArray(committersBytes)
+ .writeByteArray(leaseesBytes)
+ .build();
+ }
+ }
+
+ void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) {
+ synchronized (mMetadataLock) {
+ fout.println("blobHandle:");
+ fout.increaseIndent();
+ mBlobHandle.dump(fout, dumpArgs.shouldDumpFull());
+ fout.decreaseIndent();
+ fout.println("size: " + formatFileSize(mContext, getSize(), FLAG_IEC_UNITS));
+
+ fout.println("Committers:");
+ fout.increaseIndent();
+ if (mCommitters.isEmpty()) {
+ fout.println("<empty>");
+ } else {
+ for (int i = 0, count = mCommitters.size(); i < count; ++i) {
+ final Committer committer = mCommitters.valueAt(i);
+ fout.println("committer " + committer.toString());
+ fout.increaseIndent();
+ committer.dump(fout);
+ fout.decreaseIndent();
+ }
+ }
+ fout.decreaseIndent();
+
+ fout.println("Leasees:");
+ fout.increaseIndent();
+ if (mLeasees.isEmpty()) {
+ fout.println("<empty>");
+ } else {
+ for (int i = 0, count = mLeasees.size(); i < count; ++i) {
+ final Leasee leasee = mLeasees.valueAt(i);
+ fout.println("leasee " + leasee.toString());
+ fout.increaseIndent();
+ leasee.dump(mContext, fout);
+ fout.decreaseIndent();
+ }
+ }
+ fout.decreaseIndent();
+
+ fout.println("Open fds:");
+ fout.increaseIndent();
+ if (mRevocableFds.isEmpty()) {
+ fout.println("<empty>");
+ } else {
+ for (int i = 0, count = mRevocableFds.size(); i < count; ++i) {
+ final String packageName = mRevocableFds.keyAt(i);
+ final ArraySet<RevocableFileDescriptor> packageFds =
+ mRevocableFds.valueAt(i);
+ fout.println(packageName + "#" + packageFds.size());
+ }
+ }
+ fout.decreaseIndent();
+ }
+ }
+
+ void writeToXml(XmlSerializer out) throws IOException {
+ synchronized (mMetadataLock) {
+ XmlUtils.writeLongAttribute(out, ATTR_ID, mBlobId);
+ XmlUtils.writeIntAttribute(out, ATTR_USER_ID, mUserId);
+
+ out.startTag(null, TAG_BLOB_HANDLE);
+ mBlobHandle.writeToXml(out);
+ out.endTag(null, TAG_BLOB_HANDLE);
+
+ for (int i = 0, count = mCommitters.size(); i < count; ++i) {
+ out.startTag(null, TAG_COMMITTER);
+ mCommitters.valueAt(i).writeToXml(out);
+ out.endTag(null, TAG_COMMITTER);
+ }
+
+ for (int i = 0, count = mLeasees.size(); i < count; ++i) {
+ out.startTag(null, TAG_LEASEE);
+ mLeasees.valueAt(i).writeToXml(out);
+ out.endTag(null, TAG_LEASEE);
+ }
+ }
+ }
+
+ @Nullable
+ static BlobMetadata createFromXml(XmlPullParser in, int version, Context context)
+ throws XmlPullParserException, IOException {
+ final long blobId = XmlUtils.readLongAttribute(in, ATTR_ID);
+ final int userId = XmlUtils.readIntAttribute(in, ATTR_USER_ID);
+
+ BlobHandle blobHandle = null;
+ final ArraySet<Committer> committers = new ArraySet<>();
+ final ArraySet<Leasee> leasees = new ArraySet<>();
+ final int depth = in.getDepth();
+ while (XmlUtils.nextElementWithin(in, depth)) {
+ if (TAG_BLOB_HANDLE.equals(in.getName())) {
+ blobHandle = BlobHandle.createFromXml(in);
+ } else if (TAG_COMMITTER.equals(in.getName())) {
+ final Committer committer = Committer.createFromXml(in, version);
+ if (committer != null) {
+ committers.add(committer);
+ }
+ } else if (TAG_LEASEE.equals(in.getName())) {
+ leasees.add(Leasee.createFromXml(in, version));
+ }
+ }
+
+ if (blobHandle == null) {
+ Slog.wtf(TAG, "blobHandle should be available");
+ return null;
+ }
+
+ final BlobMetadata blobMetadata = new BlobMetadata(context, blobId, blobHandle, userId);
+ blobMetadata.setCommitters(committers);
+ blobMetadata.setLeasees(leasees);
+ return blobMetadata;
+ }
+
+ static final class Committer extends Accessor {
+ public final BlobAccessMode blobAccessMode;
+ public final long commitTimeMs;
+
+ Committer(String packageName, int uid, BlobAccessMode blobAccessMode, long commitTimeMs) {
+ super(packageName, uid);
+ this.blobAccessMode = blobAccessMode;
+ this.commitTimeMs = commitTimeMs;
+ }
+
+ long getCommitTimeMs() {
+ return commitTimeMs;
+ }
+
+ void dump(IndentingPrintWriter fout) {
+ fout.println("commit time: "
+ + (commitTimeMs == 0 ? "<null>" : BlobStoreUtils.formatTime(commitTimeMs)));
+ fout.println("accessMode:");
+ fout.increaseIndent();
+ blobAccessMode.dump(fout);
+ fout.decreaseIndent();
+ }
+
+ void writeToXml(@NonNull XmlSerializer out) throws IOException {
+ XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, packageName);
+ XmlUtils.writeIntAttribute(out, ATTR_UID, uid);
+ XmlUtils.writeLongAttribute(out, ATTR_COMMIT_TIME_MS, commitTimeMs);
+
+ out.startTag(null, TAG_ACCESS_MODE);
+ blobAccessMode.writeToXml(out);
+ out.endTag(null, TAG_ACCESS_MODE);
+ }
+
+ @Nullable
+ static Committer createFromXml(@NonNull XmlPullParser in, int version)
+ throws XmlPullParserException, IOException {
+ final String packageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE);
+ final int uid = XmlUtils.readIntAttribute(in, ATTR_UID);
+ final long commitTimeMs = version >= XML_VERSION_ADD_COMMIT_TIME
+ ? XmlUtils.readLongAttribute(in, ATTR_COMMIT_TIME_MS)
+ : 0;
+
+ final int depth = in.getDepth();
+ BlobAccessMode blobAccessMode = null;
+ while (XmlUtils.nextElementWithin(in, depth)) {
+ if (TAG_ACCESS_MODE.equals(in.getName())) {
+ blobAccessMode = BlobAccessMode.createFromXml(in);
+ }
+ }
+ if (blobAccessMode == null) {
+ Slog.wtf(TAG, "blobAccessMode should be available");
+ return null;
+ }
+ return new Committer(packageName, uid, blobAccessMode, commitTimeMs);
+ }
+ }
+
+ static final class Leasee extends Accessor {
+ public final String descriptionResEntryName;
+ public final CharSequence description;
+ public final long expiryTimeMillis;
+
+ Leasee(@NonNull Context context, @NonNull String packageName,
+ int uid, int descriptionResId,
+ @Nullable CharSequence description, long expiryTimeMillis) {
+ super(packageName, uid);
+ final Resources packageResources = getPackageResources(context, packageName,
+ UserHandle.getUserId(uid));
+ this.descriptionResEntryName = getResourceEntryName(packageResources, descriptionResId);
+ this.expiryTimeMillis = expiryTimeMillis;
+ this.description = description == null
+ ? getDescription(packageResources, descriptionResId)
+ : description;
+ }
+
+ Leasee(String packageName, int uid, @Nullable String descriptionResEntryName,
+ @Nullable CharSequence description, long expiryTimeMillis) {
+ super(packageName, uid);
+ this.descriptionResEntryName = descriptionResEntryName;
+ this.expiryTimeMillis = expiryTimeMillis;
+ this.description = description;
+ }
+
+ @Nullable
+ private static String getResourceEntryName(@Nullable Resources packageResources,
+ int resId) {
+ if (!ResourceId.isValid(resId) || packageResources == null) {
+ return null;
+ }
+ return packageResources.getResourceEntryName(resId);
+ }
+
+ @Nullable
+ private static String getDescription(@NonNull Context context,
+ @NonNull String descriptionResEntryName, @NonNull String packageName, int userId) {
+ if (descriptionResEntryName == null || descriptionResEntryName.isEmpty()) {
+ return null;
+ }
+ final Resources resources = getPackageResources(context, packageName, userId);
+ if (resources == null) {
+ return null;
+ }
+ final int resId = getDescriptionResourceId(resources, descriptionResEntryName,
+ packageName);
+ return resId == Resources.ID_NULL ? null : resources.getString(resId);
+ }
+
+ @Nullable
+ private static String getDescription(@Nullable Resources packageResources,
+ int descriptionResId) {
+ if (!ResourceId.isValid(descriptionResId) || packageResources == null) {
+ return null;
+ }
+ return packageResources.getString(descriptionResId);
+ }
+
+ boolean isStillValid() {
+ return expiryTimeMillis == 0 || expiryTimeMillis >= System.currentTimeMillis();
+ }
+
+ void dump(@NonNull Context context, @NonNull IndentingPrintWriter fout) {
+ fout.println("desc: " + getDescriptionToDump(context));
+ fout.println("expiryMs: " + expiryTimeMillis);
+ }
+
+ @NonNull
+ private String getDescriptionToDump(@NonNull Context context) {
+ String desc = getDescription(context, descriptionResEntryName, packageName,
+ UserHandle.getUserId(uid));
+ if (desc == null) {
+ desc = description.toString();
+ }
+ return desc == null ? "<none>" : desc;
+ }
+
+ void writeToXml(@NonNull XmlSerializer out) throws IOException {
+ XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, packageName);
+ XmlUtils.writeIntAttribute(out, ATTR_UID, uid);
+ XmlUtils.writeStringAttribute(out, ATTR_DESCRIPTION_RES_NAME, descriptionResEntryName);
+ XmlUtils.writeLongAttribute(out, ATTR_EXPIRY_TIME, expiryTimeMillis);
+ XmlUtils.writeStringAttribute(out, ATTR_DESCRIPTION, description);
+ }
+
+ @NonNull
+ static Leasee createFromXml(@NonNull XmlPullParser in, int version)
+ throws IOException {
+ final String packageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE);
+ final int uid = XmlUtils.readIntAttribute(in, ATTR_UID);
+ final String descriptionResEntryName;
+ if (version >= XML_VERSION_ADD_DESC_RES_NAME) {
+ descriptionResEntryName = XmlUtils.readStringAttribute(
+ in, ATTR_DESCRIPTION_RES_NAME);
+ } else {
+ descriptionResEntryName = null;
+ }
+ final long expiryTimeMillis = XmlUtils.readLongAttribute(in, ATTR_EXPIRY_TIME);
+ final CharSequence description;
+ if (version >= XML_VERSION_ADD_STRING_DESC) {
+ description = XmlUtils.readStringAttribute(in, ATTR_DESCRIPTION);
+ } else {
+ description = null;
+ }
+
+ return new Leasee(packageName, uid, descriptionResEntryName,
+ description, expiryTimeMillis);
+ }
+ }
+
+ static class Accessor {
+ public final String packageName;
+ public final int uid;
+
+ Accessor(String packageName, int uid) {
+ this.packageName = packageName;
+ this.uid = uid;
+ }
+
+ public boolean equals(String packageName, int uid) {
+ return this.uid == uid && this.packageName.equals(packageName);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof Accessor)) {
+ return false;
+ }
+ final Accessor other = (Accessor) obj;
+ return this.uid == other.uid && this.packageName.equals(other.packageName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(packageName, uid);
+ }
+
+ @Override
+ public String toString() {
+ return "[" + packageName + ", " + uid + "]";
+ }
+ }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
new file mode 100644
index 000000000000..bb9f13f1712c
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
@@ -0,0 +1,479 @@
+/*
+ * Copyright 2020 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 static android.provider.DeviceConfig.NAMESPACE_BLOBSTORE;
+import static android.text.format.Formatter.FLAG_IEC_UNITS;
+import static android.text.format.Formatter.formatFileSize;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Environment;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import android.text.TextUtils;
+import android.util.DataUnit;
+import android.util.Log;
+import android.util.Slog;
+import android.util.TimeUtils;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+class BlobStoreConfig {
+ public static final String TAG = "BlobStore";
+ public static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
+
+ // Initial version.
+ public static final int XML_VERSION_INIT = 1;
+ // Added a string variant of lease description.
+ public static final int XML_VERSION_ADD_STRING_DESC = 2;
+ public static final int XML_VERSION_ADD_DESC_RES_NAME = 3;
+ public static final int XML_VERSION_ADD_COMMIT_TIME = 4;
+ public static final int XML_VERSION_ADD_SESSION_CREATION_TIME = 5;
+
+ public static final int XML_VERSION_CURRENT = XML_VERSION_ADD_SESSION_CREATION_TIME;
+
+ public static final long INVALID_BLOB_ID = 0;
+ public static final long INVALID_BLOB_SIZE = 0;
+
+ private static final String ROOT_DIR_NAME = "blobstore";
+ private static final String BLOBS_DIR_NAME = "blobs";
+ private static final String SESSIONS_INDEX_FILE_NAME = "sessions_index.xml";
+ private static final String BLOBS_INDEX_FILE_NAME = "blobs_index.xml";
+
+ /**
+ * Job Id for idle maintenance job ({@link BlobStoreIdleJobService}).
+ */
+ public static final int IDLE_JOB_ID = 0xB70B1D7; // 191934935L
+
+ public static class DeviceConfigProperties {
+ /**
+ * Denotes the max time period (in millis) between each idle maintenance job run.
+ */
+ public static final String KEY_IDLE_JOB_PERIOD_MS = "idle_job_period_ms";
+ public static final long DEFAULT_IDLE_JOB_PERIOD_MS = TimeUnit.DAYS.toMillis(1);
+ public static long IDLE_JOB_PERIOD_MS = DEFAULT_IDLE_JOB_PERIOD_MS;
+
+ /**
+ * Denotes the timeout in millis after which sessions with no updates will be deleted.
+ */
+ public static final String KEY_SESSION_EXPIRY_TIMEOUT_MS =
+ "session_expiry_timeout_ms";
+ public static final long DEFAULT_SESSION_EXPIRY_TIMEOUT_MS = TimeUnit.DAYS.toMillis(7);
+ public static long SESSION_EXPIRY_TIMEOUT_MS = DEFAULT_SESSION_EXPIRY_TIMEOUT_MS;
+
+ /**
+ * Denotes how low the limit for the amount of data, that an app will be allowed to acquire
+ * a lease on, can be.
+ */
+ public static final String KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR =
+ "total_bytes_per_app_limit_floor";
+ public static final long DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR =
+ DataUnit.MEBIBYTES.toBytes(300); // 300 MiB
+ public static long TOTAL_BYTES_PER_APP_LIMIT_FLOOR =
+ DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR;
+
+ /**
+ * Denotes the maximum amount of data an app can acquire a lease on, in terms of fraction
+ * of total disk space.
+ */
+ public static final String KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION =
+ "total_bytes_per_app_limit_fraction";
+ public static final float DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION = 0.01f;
+ public static float TOTAL_BYTES_PER_APP_LIMIT_FRACTION =
+ DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION;
+
+ /**
+ * Denotes the duration from the time a blob is committed that we wait for a lease to
+ * be acquired before deciding to delete the blob for having no leases.
+ */
+ public static final String KEY_LEASE_ACQUISITION_WAIT_DURATION_MS =
+ "lease_acquisition_wait_time_ms";
+ public static final long DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS =
+ TimeUnit.HOURS.toMillis(6);
+ public static long LEASE_ACQUISITION_WAIT_DURATION_MS =
+ DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS;
+
+ /**
+ * Denotes the duration from the time a blob is committed that any new commits of the same
+ * data blob from the same committer will be treated as if they occurred at the earlier
+ * commit time.
+ */
+ public static final String KEY_COMMIT_COOL_OFF_DURATION_MS =
+ "commit_cool_off_duration_ms";
+ public static final long DEFAULT_COMMIT_COOL_OFF_DURATION_MS =
+ TimeUnit.HOURS.toMillis(48);
+ public static long COMMIT_COOL_OFF_DURATION_MS =
+ DEFAULT_COMMIT_COOL_OFF_DURATION_MS;
+
+ /**
+ * Denotes whether to use RevocableFileDescriptor when apps try to read session/blob data.
+ */
+ public static final String KEY_USE_REVOCABLE_FD_FOR_READS =
+ "use_revocable_fd_for_reads";
+ public static final boolean DEFAULT_USE_REVOCABLE_FD_FOR_READS = true;
+ public static boolean USE_REVOCABLE_FD_FOR_READS =
+ DEFAULT_USE_REVOCABLE_FD_FOR_READS;
+
+ /**
+ * Denotes how long before a blob is deleted, once the last lease on it is released.
+ */
+ public static final String KEY_DELETE_ON_LAST_LEASE_DELAY_MS =
+ "delete_on_last_lease_delay_ms";
+ public static final long DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS =
+ TimeUnit.HOURS.toMillis(6);
+ public static long DELETE_ON_LAST_LEASE_DELAY_MS =
+ DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS;
+
+ /**
+ * Denotes the maximum number of active sessions per app at any time.
+ */
+ public static final String KEY_MAX_ACTIVE_SESSIONS = "max_active_sessions";
+ public static int DEFAULT_MAX_ACTIVE_SESSIONS = 250;
+ public static int MAX_ACTIVE_SESSIONS = DEFAULT_MAX_ACTIVE_SESSIONS;
+
+ /**
+ * Denotes the maximum number of committed blobs per app at any time.
+ */
+ public static final String KEY_MAX_COMMITTED_BLOBS = "max_committed_blobs";
+ public static int DEFAULT_MAX_COMMITTED_BLOBS = 1000;
+ public static int MAX_COMMITTED_BLOBS = DEFAULT_MAX_COMMITTED_BLOBS;
+
+ /**
+ * Denotes the maximum number of leased blobs per app at any time.
+ */
+ public static final String KEY_MAX_LEASED_BLOBS = "max_leased_blobs";
+ public static int DEFAULT_MAX_LEASED_BLOBS = 500;
+ public static int MAX_LEASED_BLOBS = DEFAULT_MAX_LEASED_BLOBS;
+
+ /**
+ * Denotes the maximum number of packages explicitly permitted to access a blob
+ * (permitted as part of creating a {@link BlobAccessMode}).
+ */
+ public static final String KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES = "max_permitted_pks";
+ public static int DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES = 300;
+ public static int MAX_BLOB_ACCESS_PERMITTED_PACKAGES =
+ DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES;
+
+ /**
+ * Denotes the maximum number of characters that a lease description can have.
+ */
+ public static final String KEY_LEASE_DESC_CHAR_LIMIT = "lease_desc_char_limit";
+ public static int DEFAULT_LEASE_DESC_CHAR_LIMIT = 300;
+ public static int LEASE_DESC_CHAR_LIMIT = DEFAULT_LEASE_DESC_CHAR_LIMIT;
+
+ static void refresh(Properties properties) {
+ if (!NAMESPACE_BLOBSTORE.equals(properties.getNamespace())) {
+ return;
+ }
+ properties.getKeyset().forEach(key -> {
+ switch (key) {
+ case KEY_IDLE_JOB_PERIOD_MS:
+ IDLE_JOB_PERIOD_MS = properties.getLong(key, DEFAULT_IDLE_JOB_PERIOD_MS);
+ break;
+ case KEY_SESSION_EXPIRY_TIMEOUT_MS:
+ SESSION_EXPIRY_TIMEOUT_MS = properties.getLong(key,
+ DEFAULT_SESSION_EXPIRY_TIMEOUT_MS);
+ break;
+ case KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR:
+ TOTAL_BYTES_PER_APP_LIMIT_FLOOR = properties.getLong(key,
+ DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR);
+ break;
+ case KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION:
+ TOTAL_BYTES_PER_APP_LIMIT_FRACTION = properties.getFloat(key,
+ DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION);
+ break;
+ case KEY_LEASE_ACQUISITION_WAIT_DURATION_MS:
+ LEASE_ACQUISITION_WAIT_DURATION_MS = properties.getLong(key,
+ DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS);
+ break;
+ case KEY_COMMIT_COOL_OFF_DURATION_MS:
+ COMMIT_COOL_OFF_DURATION_MS = properties.getLong(key,
+ DEFAULT_COMMIT_COOL_OFF_DURATION_MS);
+ break;
+ case KEY_USE_REVOCABLE_FD_FOR_READS:
+ USE_REVOCABLE_FD_FOR_READS = properties.getBoolean(key,
+ DEFAULT_USE_REVOCABLE_FD_FOR_READS);
+ break;
+ case KEY_DELETE_ON_LAST_LEASE_DELAY_MS:
+ DELETE_ON_LAST_LEASE_DELAY_MS = properties.getLong(key,
+ DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS);
+ break;
+ case KEY_MAX_ACTIVE_SESSIONS:
+ MAX_ACTIVE_SESSIONS = properties.getInt(key, DEFAULT_MAX_ACTIVE_SESSIONS);
+ break;
+ case KEY_MAX_COMMITTED_BLOBS:
+ MAX_COMMITTED_BLOBS = properties.getInt(key, DEFAULT_MAX_COMMITTED_BLOBS);
+ break;
+ case KEY_MAX_LEASED_BLOBS:
+ MAX_LEASED_BLOBS = properties.getInt(key, DEFAULT_MAX_LEASED_BLOBS);
+ break;
+ case KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES:
+ MAX_BLOB_ACCESS_PERMITTED_PACKAGES = properties.getInt(key,
+ DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES);
+ break;
+ case KEY_LEASE_DESC_CHAR_LIMIT:
+ LEASE_DESC_CHAR_LIMIT = properties.getInt(key,
+ DEFAULT_LEASE_DESC_CHAR_LIMIT);
+ break;
+ default:
+ Slog.wtf(TAG, "Unknown key in device config properties: " + key);
+ }
+ });
+ }
+
+ static void dump(IndentingPrintWriter fout, Context context) {
+ final String dumpFormat = "%s: [cur: %s, def: %s]";
+ fout.println(String.format(dumpFormat, KEY_IDLE_JOB_PERIOD_MS,
+ TimeUtils.formatDuration(IDLE_JOB_PERIOD_MS),
+ TimeUtils.formatDuration(DEFAULT_IDLE_JOB_PERIOD_MS)));
+ fout.println(String.format(dumpFormat, KEY_SESSION_EXPIRY_TIMEOUT_MS,
+ TimeUtils.formatDuration(SESSION_EXPIRY_TIMEOUT_MS),
+ TimeUtils.formatDuration(DEFAULT_SESSION_EXPIRY_TIMEOUT_MS)));
+ fout.println(String.format(dumpFormat, KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR,
+ formatFileSize(context, TOTAL_BYTES_PER_APP_LIMIT_FLOOR, FLAG_IEC_UNITS),
+ formatFileSize(context, DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR,
+ FLAG_IEC_UNITS)));
+ fout.println(String.format(dumpFormat, KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION,
+ TOTAL_BYTES_PER_APP_LIMIT_FRACTION,
+ DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION));
+ fout.println(String.format(dumpFormat, KEY_LEASE_ACQUISITION_WAIT_DURATION_MS,
+ TimeUtils.formatDuration(LEASE_ACQUISITION_WAIT_DURATION_MS),
+ TimeUtils.formatDuration(DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS)));
+ fout.println(String.format(dumpFormat, KEY_COMMIT_COOL_OFF_DURATION_MS,
+ TimeUtils.formatDuration(COMMIT_COOL_OFF_DURATION_MS),
+ TimeUtils.formatDuration(DEFAULT_COMMIT_COOL_OFF_DURATION_MS)));
+ fout.println(String.format(dumpFormat, KEY_USE_REVOCABLE_FD_FOR_READS,
+ USE_REVOCABLE_FD_FOR_READS, DEFAULT_USE_REVOCABLE_FD_FOR_READS));
+ fout.println(String.format(dumpFormat, KEY_DELETE_ON_LAST_LEASE_DELAY_MS,
+ TimeUtils.formatDuration(DELETE_ON_LAST_LEASE_DELAY_MS),
+ TimeUtils.formatDuration(DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS)));
+ fout.println(String.format(dumpFormat, KEY_MAX_ACTIVE_SESSIONS,
+ MAX_ACTIVE_SESSIONS, DEFAULT_MAX_ACTIVE_SESSIONS));
+ fout.println(String.format(dumpFormat, KEY_MAX_COMMITTED_BLOBS,
+ MAX_COMMITTED_BLOBS, DEFAULT_MAX_COMMITTED_BLOBS));
+ fout.println(String.format(dumpFormat, KEY_MAX_LEASED_BLOBS,
+ MAX_LEASED_BLOBS, DEFAULT_MAX_LEASED_BLOBS));
+ fout.println(String.format(dumpFormat, KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES,
+ MAX_BLOB_ACCESS_PERMITTED_PACKAGES,
+ DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES));
+ fout.println(String.format(dumpFormat, KEY_LEASE_DESC_CHAR_LIMIT,
+ LEASE_DESC_CHAR_LIMIT, DEFAULT_LEASE_DESC_CHAR_LIMIT));
+ }
+ }
+
+ public static void initialize(Context context) {
+ DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_BLOBSTORE,
+ context.getMainExecutor(),
+ properties -> DeviceConfigProperties.refresh(properties));
+ DeviceConfigProperties.refresh(DeviceConfig.getProperties(NAMESPACE_BLOBSTORE));
+ }
+
+ /**
+ * Returns the max time period (in millis) between each idle maintenance job run.
+ */
+ public static long getIdleJobPeriodMs() {
+ return DeviceConfigProperties.IDLE_JOB_PERIOD_MS;
+ }
+
+ /**
+ * Returns whether a session is expired or not. A session is considered expired if the session
+ * has not been modified in a while (i.e. SESSION_EXPIRY_TIMEOUT_MS).
+ */
+ public static boolean hasSessionExpired(long sessionLastModifiedMs) {
+ return sessionLastModifiedMs
+ < System.currentTimeMillis() - DeviceConfigProperties.SESSION_EXPIRY_TIMEOUT_MS;
+ }
+
+ /**
+ * Returns the maximum amount of data that an app can acquire a lease on.
+ */
+ public static long getAppDataBytesLimit() {
+ final long totalBytesLimit = (long) (Environment.getDataSystemDirectory().getTotalSpace()
+ * DeviceConfigProperties.TOTAL_BYTES_PER_APP_LIMIT_FRACTION);
+ return Math.max(DeviceConfigProperties.TOTAL_BYTES_PER_APP_LIMIT_FLOOR, totalBytesLimit);
+ }
+
+ /**
+ * Returns whether the wait time for lease acquisition for a blob has elapsed.
+ */
+ public static boolean hasLeaseWaitTimeElapsed(long commitTimeMs) {
+ return commitTimeMs + DeviceConfigProperties.LEASE_ACQUISITION_WAIT_DURATION_MS
+ < System.currentTimeMillis();
+ }
+
+ /**
+ * Returns an adjusted commit time depending on whether commit cool-off period has elapsed.
+ *
+ * If this is the initial commit or the earlier commit cool-off period has elapsed, then
+ * the new commit time is used. Otherwise, the earlier commit time is used.
+ */
+ public static long getAdjustedCommitTimeMs(long oldCommitTimeMs, long newCommitTimeMs) {
+ if (oldCommitTimeMs == 0 || hasCommitCoolOffPeriodElapsed(oldCommitTimeMs)) {
+ return newCommitTimeMs;
+ }
+ return oldCommitTimeMs;
+ }
+
+ /**
+ * Returns whether the commit cool-off period has elapsed.
+ */
+ private static boolean hasCommitCoolOffPeriodElapsed(long commitTimeMs) {
+ return commitTimeMs + DeviceConfigProperties.COMMIT_COOL_OFF_DURATION_MS
+ < System.currentTimeMillis();
+ }
+
+ /**
+ * Return whether to use RevocableFileDescriptor when apps try to read session/blob data.
+ */
+ public static boolean shouldUseRevocableFdForReads() {
+ return DeviceConfigProperties.USE_REVOCABLE_FD_FOR_READS;
+ }
+
+ /**
+ * Returns the duration to wait before a blob is deleted, once the last lease on it is released.
+ */
+ public static long getDeletionOnLastLeaseDelayMs() {
+ return DeviceConfigProperties.DELETE_ON_LAST_LEASE_DELAY_MS;
+ }
+
+ /**
+ * Returns the maximum number of active sessions per app.
+ */
+ public static int getMaxActiveSessions() {
+ return DeviceConfigProperties.MAX_ACTIVE_SESSIONS;
+ }
+
+ /**
+ * Returns the maximum number of committed blobs per app.
+ */
+ public static int getMaxCommittedBlobs() {
+ return DeviceConfigProperties.MAX_COMMITTED_BLOBS;
+ }
+
+ /**
+ * Returns the maximum number of leased blobs per app.
+ */
+ public static int getMaxLeasedBlobs() {
+ return DeviceConfigProperties.MAX_LEASED_BLOBS;
+ }
+
+ /**
+ * Returns the maximum number of packages explicitly permitted to access a blob.
+ */
+ public static int getMaxPermittedPackages() {
+ return DeviceConfigProperties.MAX_BLOB_ACCESS_PERMITTED_PACKAGES;
+ }
+
+ /**
+ * Returns the lease description truncated to
+ * {@link DeviceConfigProperties#LEASE_DESC_CHAR_LIMIT} characters.
+ */
+ public static CharSequence getTruncatedLeaseDescription(CharSequence description) {
+ if (TextUtils.isEmpty(description)) {
+ return description;
+ }
+ return TextUtils.trimToLengthWithEllipsis(description,
+ DeviceConfigProperties.LEASE_DESC_CHAR_LIMIT);
+ }
+
+ @Nullable
+ public static File prepareBlobFile(long sessionId) {
+ final File blobsDir = prepareBlobsDir();
+ return blobsDir == null ? null : getBlobFile(blobsDir, sessionId);
+ }
+
+ @NonNull
+ public static File getBlobFile(long sessionId) {
+ return getBlobFile(getBlobsDir(), sessionId);
+ }
+
+ @NonNull
+ private static File getBlobFile(File blobsDir, long sessionId) {
+ return new File(blobsDir, String.valueOf(sessionId));
+ }
+
+ @Nullable
+ public static File prepareBlobsDir() {
+ final File blobsDir = getBlobsDir(prepareBlobStoreRootDir());
+ if (!blobsDir.exists() && !blobsDir.mkdir()) {
+ Slog.e(TAG, "Failed to mkdir(): " + blobsDir);
+ return null;
+ }
+ return blobsDir;
+ }
+
+ @NonNull
+ public static File getBlobsDir() {
+ return getBlobsDir(getBlobStoreRootDir());
+ }
+
+ @NonNull
+ private static File getBlobsDir(File blobsRootDir) {
+ return new File(blobsRootDir, BLOBS_DIR_NAME);
+ }
+
+ @Nullable
+ public static File prepareSessionIndexFile() {
+ final File blobStoreRootDir = prepareBlobStoreRootDir();
+ if (blobStoreRootDir == null) {
+ return null;
+ }
+ return new File(blobStoreRootDir, SESSIONS_INDEX_FILE_NAME);
+ }
+
+ @Nullable
+ public static File prepareBlobsIndexFile() {
+ final File blobsStoreRootDir = prepareBlobStoreRootDir();
+ if (blobsStoreRootDir == null) {
+ return null;
+ }
+ return new File(blobsStoreRootDir, BLOBS_INDEX_FILE_NAME);
+ }
+
+ @Nullable
+ public static File prepareBlobStoreRootDir() {
+ final File blobStoreRootDir = getBlobStoreRootDir();
+ if (!blobStoreRootDir.exists() && !blobStoreRootDir.mkdir()) {
+ Slog.e(TAG, "Failed to mkdir(): " + blobStoreRootDir);
+ return null;
+ }
+ return blobStoreRootDir;
+ }
+
+ @NonNull
+ public static File getBlobStoreRootDir() {
+ return new File(Environment.getDataSystemDirectory(), ROOT_DIR_NAME);
+ }
+
+ public static void dump(IndentingPrintWriter fout, Context context) {
+ fout.println("XML current version: " + XML_VERSION_CURRENT);
+
+ fout.println("Idle job ID: " + IDLE_JOB_ID);
+
+ fout.println("Total bytes per app limit: " + formatFileSize(context,
+ getAppDataBytesLimit(), FLAG_IEC_UNITS));
+
+ fout.println("Device config properties:");
+ fout.increaseIndent();
+ DeviceConfigProperties.dump(fout, context);
+ fout.decreaseIndent();
+ }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java
new file mode 100644
index 000000000000..4b0f719b13be
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2020 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 static com.android.server.blob.BlobStoreConfig.IDLE_JOB_ID;
+import static com.android.server.blob.BlobStoreConfig.LOGV;
+import static com.android.server.blob.BlobStoreConfig.TAG;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+
+/**
+ * Maintenance job to clean up stale sessions and blobs.
+ */
+public class BlobStoreIdleJobService extends JobService {
+ @Override
+ public boolean onStartJob(final JobParameters params) {
+ AsyncTask.execute(() -> {
+ final BlobStoreManagerInternal blobStoreManagerInternal = LocalServices.getService(
+ BlobStoreManagerInternal.class);
+ blobStoreManagerInternal.onIdleMaintenance();
+ jobFinished(params, false);
+ });
+ return false;
+ }
+
+ @Override
+ public boolean onStopJob(final JobParameters params) {
+ Slog.d(TAG, "Idle maintenance job is stopped; id=" + params.getJobId()
+ + ", reason=" + JobParameters.getReasonCodeDescription(params.getStopReason()));
+ return false;
+ }
+
+ static void schedule(Context context) {
+ final JobScheduler jobScheduler = (JobScheduler) context.getSystemService(
+ Context.JOB_SCHEDULER_SERVICE);
+ final JobInfo job = new JobInfo.Builder(IDLE_JOB_ID,
+ new ComponentName(context, BlobStoreIdleJobService.class))
+ .setRequiresDeviceIdle(true)
+ .setRequiresCharging(true)
+ .setPeriodic(BlobStoreConfig.getIdleJobPeriodMs())
+ .build();
+ jobScheduler.schedule(job);
+ if (LOGV) {
+ Slog.v(TAG, "Scheduling the idle maintenance job");
+ }
+ }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerInternal.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerInternal.java
new file mode 100644
index 000000000000..5358245f517f
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerInternal.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 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;
+
+/**
+ * BlobStoreManager local system service interface.
+ *
+ * Only for use within the system server.
+ */
+public abstract class BlobStoreManagerInternal {
+ /**
+ * Triggered from idle maintenance job to cleanup stale blobs and sessions.
+ */
+ public abstract void onIdleMaintenance();
+}
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..d37dfdeaa583
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
@@ -0,0 +1,1915 @@
+/*
+ * 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 static android.app.blob.BlobStoreManager.COMMIT_RESULT_ERROR;
+import static android.app.blob.BlobStoreManager.COMMIT_RESULT_SUCCESS;
+import static android.app.blob.XmlTags.ATTR_VERSION;
+import static android.app.blob.XmlTags.TAG_BLOB;
+import static android.app.blob.XmlTags.TAG_BLOBS;
+import static android.app.blob.XmlTags.TAG_SESSION;
+import static android.app.blob.XmlTags.TAG_SESSIONS;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
+import static android.os.UserHandle.USER_CURRENT;
+import static android.os.UserHandle.USER_NULL;
+
+import static com.android.server.blob.BlobStoreConfig.INVALID_BLOB_ID;
+import static com.android.server.blob.BlobStoreConfig.INVALID_BLOB_SIZE;
+import static com.android.server.blob.BlobStoreConfig.LOGV;
+import static com.android.server.blob.BlobStoreConfig.TAG;
+import static com.android.server.blob.BlobStoreConfig.XML_VERSION_CURRENT;
+import static com.android.server.blob.BlobStoreConfig.getAdjustedCommitTimeMs;
+import static com.android.server.blob.BlobStoreConfig.getDeletionOnLastLeaseDelayMs;
+import static com.android.server.blob.BlobStoreConfig.getMaxActiveSessions;
+import static com.android.server.blob.BlobStoreConfig.getMaxCommittedBlobs;
+import static com.android.server.blob.BlobStoreConfig.getMaxLeasedBlobs;
+import static com.android.server.blob.BlobStoreSession.STATE_ABANDONED;
+import static com.android.server.blob.BlobStoreSession.STATE_COMMITTED;
+import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_INVALID;
+import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_VALID;
+import static com.android.server.blob.BlobStoreSession.stateToString;
+import static com.android.server.blob.BlobStoreUtils.getDescriptionResourceId;
+import static com.android.server.blob.BlobStoreUtils.getPackageResources;
+
+import android.annotation.CurrentTimeSecondsLong;
+import android.annotation.IdRes;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.StatsManager;
+import android.app.blob.BlobHandle;
+import android.app.blob.BlobInfo;
+import android.app.blob.IBlobStoreManager;
+import android.app.blob.IBlobStoreSession;
+import android.app.blob.LeaseInfo;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.PackageStats;
+import android.content.res.ResourceId;
+import android.content.res.Resources;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.LimitExceededException;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelableException;
+import android.os.Process;
+import android.os.RemoteCallback;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManagerInternal;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.ExceptionUtils;
+import android.util.LongSparseArray;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.StatsEvent;
+import android.util.Xml;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.CollectionUtils;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.XmlUtils;
+import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
+import com.android.server.SystemService;
+import com.android.server.Watchdog;
+import com.android.server.blob.BlobMetadata.Committer;
+import com.android.server.usage.StorageStatsManagerInternal;
+import com.android.server.usage.StorageStatsManagerInternal.StorageStatsAugmenter;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Service responsible for maintaining and facilitating access to data blobs published by apps.
+ */
+public class BlobStoreManagerService extends SystemService {
+
+ private final Object mBlobsLock = new Object();
+
+ // Contains data of userId -> {sessionId -> {BlobStoreSession}}.
+ @GuardedBy("mBlobsLock")
+ private final SparseArray<LongSparseArray<BlobStoreSession>> mSessions = new SparseArray<>();
+
+ @GuardedBy("mBlobsLock")
+ private long mCurrentMaxSessionId;
+
+ // Contains data of userId -> {BlobHandle -> {BlobMetadata}}
+ @GuardedBy("mBlobsLock")
+ private final SparseArray<ArrayMap<BlobHandle, BlobMetadata>> mBlobsMap = new SparseArray<>();
+
+ // Contains all ids that are currently in use.
+ @GuardedBy("mBlobsLock")
+ private final ArraySet<Long> mActiveBlobIds = new ArraySet<>();
+ // Contains all ids that are currently in use and those that were in use but got deleted in the
+ // current boot session.
+ @GuardedBy("mBlobsLock")
+ private final ArraySet<Long> mKnownBlobIds = new ArraySet<>();
+
+ // Random number generator for new session ids.
+ private final Random mRandom = new SecureRandom();
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final Handler mBackgroundHandler;
+ private final Injector mInjector;
+ private final SessionStateChangeListener mSessionStateChangeListener =
+ new SessionStateChangeListener();
+
+ private PackageManagerInternal mPackageManagerInternal;
+ private StatsManager mStatsManager;
+ private StatsPullAtomCallbackImpl mStatsCallbackImpl = new StatsPullAtomCallbackImpl();
+
+ private final Runnable mSaveBlobsInfoRunnable = this::writeBlobsInfo;
+ private final Runnable mSaveSessionsRunnable = this::writeBlobSessions;
+
+ public BlobStoreManagerService(Context context) {
+ this(context, new Injector());
+ }
+
+ @VisibleForTesting
+ BlobStoreManagerService(Context context, Injector injector) {
+ super(context);
+
+ mContext = context;
+ mInjector = injector;
+ mHandler = mInjector.initializeMessageHandler();
+ mBackgroundHandler = mInjector.getBackgroundHandler();
+ }
+
+ private static Handler initializeMessageHandler() {
+ final HandlerThread handlerThread = new ServiceThread(TAG,
+ Process.THREAD_PRIORITY_DEFAULT, true /* allowIo */);
+ handlerThread.start();
+ final Handler handler = new Handler(handlerThread.getLooper());
+ Watchdog.getInstance().addThread(handler);
+ return handler;
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(Context.BLOB_STORE_SERVICE, new Stub());
+ LocalServices.addService(BlobStoreManagerInternal.class, new LocalService());
+
+ mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+ mStatsManager = getContext().getSystemService(StatsManager.class);
+ registerReceivers();
+ LocalServices.getService(StorageStatsManagerInternal.class)
+ .registerStorageStatsAugmenter(new BlobStorageStatsAugmenter(), TAG);
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase == PHASE_ACTIVITY_MANAGER_READY) {
+ BlobStoreConfig.initialize(mContext);
+ } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
+ synchronized (mBlobsLock) {
+ final SparseArray<SparseArray<String>> allPackages = getAllPackages();
+ readBlobSessionsLocked(allPackages);
+ readBlobsInfoLocked(allPackages);
+ }
+ registerBlobStorePuller();
+ } else if (phase == PHASE_BOOT_COMPLETED) {
+ BlobStoreIdleJobService.schedule(mContext);
+ }
+ }
+
+ @GuardedBy("mBlobsLock")
+ private long generateNextSessionIdLocked() {
+ // Logic borrowed from PackageInstallerService.
+ int n = 0;
+ long sessionId;
+ do {
+ final long randomLong = mRandom.nextLong();
+ sessionId = (randomLong == Long.MIN_VALUE) ? INVALID_BLOB_ID : Math.abs(randomLong);
+ if (mKnownBlobIds.indexOf(sessionId) < 0 && sessionId != INVALID_BLOB_ID) {
+ return sessionId;
+ }
+ } while (n++ < 32);
+ throw new IllegalStateException("Failed to allocate session ID");
+ }
+
+ private void registerReceivers() {
+ final IntentFilter packageChangedFilter = new IntentFilter();
+ packageChangedFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
+ packageChangedFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
+ packageChangedFilter.addDataScheme("package");
+ mContext.registerReceiverAsUser(new PackageChangedReceiver(), UserHandle.ALL,
+ packageChangedFilter, null, mHandler);
+
+ final IntentFilter userActionFilter = new IntentFilter();
+ userActionFilter.addAction(Intent.ACTION_USER_REMOVED);
+ mContext.registerReceiverAsUser(new UserActionReceiver(), UserHandle.ALL,
+ userActionFilter, null, mHandler);
+ }
+
+ @GuardedBy("mBlobsLock")
+ private LongSparseArray<BlobStoreSession> getUserSessionsLocked(int userId) {
+ LongSparseArray<BlobStoreSession> userSessions = mSessions.get(userId);
+ if (userSessions == null) {
+ userSessions = new LongSparseArray<>();
+ mSessions.put(userId, userSessions);
+ }
+ return userSessions;
+ }
+
+ @GuardedBy("mBlobsLock")
+ private ArrayMap<BlobHandle, BlobMetadata> getUserBlobsLocked(int userId) {
+ ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.get(userId);
+ if (userBlobs == null) {
+ userBlobs = new ArrayMap<>();
+ mBlobsMap.put(userId, userBlobs);
+ }
+ return userBlobs;
+ }
+
+ @VisibleForTesting
+ void addUserSessionsForTest(LongSparseArray<BlobStoreSession> userSessions, int userId) {
+ synchronized (mBlobsLock) {
+ mSessions.put(userId, userSessions);
+ }
+ }
+
+ @VisibleForTesting
+ void addUserBlobsForTest(ArrayMap<BlobHandle, BlobMetadata> userBlobs, int userId) {
+ synchronized (mBlobsLock) {
+ mBlobsMap.put(userId, userBlobs);
+ }
+ }
+
+ @VisibleForTesting
+ void addActiveIdsForTest(long... activeIds) {
+ synchronized (mBlobsLock) {
+ for (long id : activeIds) {
+ addActiveBlobIdLocked(id);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ Set<Long> getActiveIdsForTest() {
+ synchronized (mBlobsLock) {
+ return mActiveBlobIds;
+ }
+ }
+
+ @VisibleForTesting
+ Set<Long> getKnownIdsForTest() {
+ synchronized (mBlobsLock) {
+ return mKnownBlobIds;
+ }
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void addSessionForUserLocked(BlobStoreSession session, int userId) {
+ getUserSessionsLocked(userId).put(session.getSessionId(), session);
+ addActiveBlobIdLocked(session.getSessionId());
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void addBlobForUserLocked(BlobMetadata blobMetadata, int userId) {
+ addBlobForUserLocked(blobMetadata, getUserBlobsLocked(userId));
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void addBlobForUserLocked(BlobMetadata blobMetadata,
+ ArrayMap<BlobHandle, BlobMetadata> userBlobs) {
+ userBlobs.put(blobMetadata.getBlobHandle(), blobMetadata);
+ addActiveBlobIdLocked(blobMetadata.getBlobId());
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void addActiveBlobIdLocked(long id) {
+ mActiveBlobIds.add(id);
+ mKnownBlobIds.add(id);
+ }
+
+ @GuardedBy("mBlobsLock")
+ private int getSessionsCountLocked(int uid, String packageName) {
+ // TODO: Maintain a counter instead of traversing all the sessions
+ final AtomicInteger sessionsCount = new AtomicInteger(0);
+ forEachSessionInUser(session -> {
+ if (session.getOwnerUid() == uid && session.getOwnerPackageName().equals(packageName)) {
+ sessionsCount.getAndIncrement();
+ }
+ }, UserHandle.getUserId(uid));
+ return sessionsCount.get();
+ }
+
+ private long createSessionInternal(BlobHandle blobHandle,
+ int callingUid, String callingPackage) {
+ synchronized (mBlobsLock) {
+ final int sessionsCount = getSessionsCountLocked(callingUid, callingPackage);
+ if (sessionsCount >= getMaxActiveSessions()) {
+ throw new LimitExceededException("Too many active sessions for the caller: "
+ + sessionsCount);
+ }
+ // TODO: throw if there is already an active session associated with blobHandle.
+ final long sessionId = generateNextSessionIdLocked();
+ final BlobStoreSession session = new BlobStoreSession(mContext,
+ sessionId, blobHandle, callingUid, callingPackage,
+ mSessionStateChangeListener);
+ addSessionForUserLocked(session, UserHandle.getUserId(callingUid));
+ if (LOGV) {
+ Slog.v(TAG, "Created session for " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
+ writeBlobSessionsAsync();
+ return sessionId;
+ }
+ }
+
+ private BlobStoreSession openSessionInternal(long sessionId,
+ int callingUid, String callingPackage) {
+ final BlobStoreSession session;
+ synchronized (mBlobsLock) {
+ session = getUserSessionsLocked(
+ UserHandle.getUserId(callingUid)).get(sessionId);
+ if (session == null || !session.hasAccess(callingUid, callingPackage)
+ || session.isFinalized()) {
+ throw new SecurityException("Session not found: " + sessionId);
+ }
+ }
+ session.open();
+ return session;
+ }
+
+ private void abandonSessionInternal(long sessionId,
+ int callingUid, String callingPackage) {
+ synchronized (mBlobsLock) {
+ final BlobStoreSession session = openSessionInternal(sessionId,
+ callingUid, callingPackage);
+ session.open();
+ session.abandon();
+ if (LOGV) {
+ Slog.v(TAG, "Abandoned session with id " + sessionId
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
+ writeBlobSessionsAsync();
+ }
+ }
+
+ private ParcelFileDescriptor openBlobInternal(BlobHandle blobHandle, int callingUid,
+ String callingPackage) throws IOException {
+ synchronized (mBlobsLock) {
+ final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid))
+ .get(blobHandle);
+ if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller(
+ callingPackage, callingUid)) {
+ if (blobMetadata == null) {
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_OPENED, callingUid,
+ INVALID_BLOB_ID, INVALID_BLOB_SIZE,
+ FrameworkStatsLog.BLOB_OPENED__RESULT__BLOB_DNE);
+ } else {
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_OPENED, callingUid,
+ blobMetadata.getBlobId(), blobMetadata.getSize(),
+ FrameworkStatsLog.BLOB_LEASED__RESULT__ACCESS_NOT_ALLOWED);
+ }
+ throw new SecurityException("Caller not allowed to access " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
+
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_OPENED, callingUid,
+ blobMetadata.getBlobId(), blobMetadata.getSize(),
+ FrameworkStatsLog.BLOB_OPENED__RESULT__SUCCESS);
+
+ return blobMetadata.openForRead(callingPackage);
+ }
+ }
+
+ @GuardedBy("mBlobsLock")
+ private int getCommittedBlobsCountLocked(int uid, String packageName) {
+ // TODO: Maintain a counter instead of traversing all the blobs
+ final AtomicInteger blobsCount = new AtomicInteger(0);
+ forEachBlobInUser((blobMetadata) -> {
+ if (blobMetadata.isACommitter(packageName, uid)) {
+ blobsCount.getAndIncrement();
+ }
+ }, UserHandle.getUserId(uid));
+ return blobsCount.get();
+ }
+
+ @GuardedBy("mBlobsLock")
+ private int getLeasedBlobsCountLocked(int uid, String packageName) {
+ // TODO: Maintain a counter instead of traversing all the blobs
+ final AtomicInteger blobsCount = new AtomicInteger(0);
+ forEachBlobInUser((blobMetadata) -> {
+ if (blobMetadata.isALeasee(packageName, uid)) {
+ blobsCount.getAndIncrement();
+ }
+ }, UserHandle.getUserId(uid));
+ return blobsCount.get();
+ }
+
+ private void acquireLeaseInternal(BlobHandle blobHandle, int descriptionResId,
+ CharSequence description, long leaseExpiryTimeMillis,
+ int callingUid, String callingPackage) {
+ synchronized (mBlobsLock) {
+ final int leasesCount = getLeasedBlobsCountLocked(callingUid, callingPackage);
+ if (leasesCount >= getMaxLeasedBlobs()) {
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid,
+ INVALID_BLOB_ID, INVALID_BLOB_SIZE,
+ FrameworkStatsLog.BLOB_LEASED__RESULT__COUNT_LIMIT_EXCEEDED);
+ throw new LimitExceededException("Too many leased blobs for the caller: "
+ + leasesCount);
+ }
+ final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid))
+ .get(blobHandle);
+ if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller(
+ callingPackage, callingUid)) {
+ if (blobMetadata == null) {
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid,
+ INVALID_BLOB_ID, INVALID_BLOB_SIZE,
+ FrameworkStatsLog.BLOB_LEASED__RESULT__BLOB_DNE);
+ } else {
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid,
+ blobMetadata.getBlobId(), blobMetadata.getSize(),
+ FrameworkStatsLog.BLOB_LEASED__RESULT__ACCESS_NOT_ALLOWED);
+ }
+ throw new SecurityException("Caller not allowed to access " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
+ if (leaseExpiryTimeMillis != 0 && blobHandle.expiryTimeMillis != 0
+ && leaseExpiryTimeMillis > blobHandle.expiryTimeMillis) {
+
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid,
+ blobMetadata.getBlobId(), blobMetadata.getSize(),
+ FrameworkStatsLog.BLOB_LEASED__RESULT__LEASE_EXPIRY_INVALID);
+ throw new IllegalArgumentException(
+ "Lease expiry cannot be later than blobs expiry time");
+ }
+ if (blobMetadata.getSize()
+ > getRemainingLeaseQuotaBytesInternal(callingUid, callingPackage)) {
+
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid,
+ blobMetadata.getBlobId(), blobMetadata.getSize(),
+ FrameworkStatsLog.BLOB_LEASED__RESULT__DATA_SIZE_LIMIT_EXCEEDED);
+ throw new LimitExceededException("Total amount of data with an active lease"
+ + " is exceeding the max limit");
+ }
+
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid,
+ blobMetadata.getBlobId(), blobMetadata.getSize(),
+ FrameworkStatsLog.BLOB_LEASED__RESULT__SUCCESS);
+
+ blobMetadata.addOrReplaceLeasee(callingPackage, callingUid,
+ descriptionResId, description, leaseExpiryTimeMillis);
+ if (LOGV) {
+ Slog.v(TAG, "Acquired lease on " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
+ writeBlobsInfoAsync();
+ }
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mBlobsLock")
+ long getTotalUsageBytesLocked(int callingUid, String callingPackage) {
+ final AtomicLong totalBytes = new AtomicLong(0);
+ forEachBlobInUser((blobMetadata) -> {
+ if (blobMetadata.isALeasee(callingPackage, callingUid)) {
+ totalBytes.getAndAdd(blobMetadata.getSize());
+ }
+ }, UserHandle.getUserId(callingUid));
+ return totalBytes.get();
+ }
+
+ private void releaseLeaseInternal(BlobHandle blobHandle, int callingUid,
+ String callingPackage) {
+ synchronized (mBlobsLock) {
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs =
+ getUserBlobsLocked(UserHandle.getUserId(callingUid));
+ final BlobMetadata blobMetadata = userBlobs.get(blobHandle);
+ if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller(
+ callingPackage, callingUid)) {
+ throw new SecurityException("Caller not allowed to access " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
+ blobMetadata.removeLeasee(callingPackage, callingUid);
+ if (LOGV) {
+ Slog.v(TAG, "Released lease on " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
+ if (!blobMetadata.hasValidLeases()) {
+ mHandler.postDelayed(() -> {
+ synchronized (mBlobsLock) {
+ // Check if blobMetadata object is still valid. If it is not, then
+ // it means that it was already deleted and nothing else to do here.
+ if (!Objects.equals(userBlobs.get(blobHandle), blobMetadata)) {
+ return;
+ }
+ if (blobMetadata.shouldBeDeleted(true /* respectLeaseWaitTime */)) {
+ deleteBlobLocked(blobMetadata);
+ userBlobs.remove(blobHandle);
+ }
+ writeBlobsInfoAsync();
+ }
+ }, getDeletionOnLastLeaseDelayMs());
+ }
+ writeBlobsInfoAsync();
+ }
+ }
+
+ private long getRemainingLeaseQuotaBytesInternal(int callingUid, String callingPackage) {
+ synchronized (mBlobsLock) {
+ final long remainingQuota = BlobStoreConfig.getAppDataBytesLimit()
+ - getTotalUsageBytesLocked(callingUid, callingPackage);
+ return remainingQuota > 0 ? remainingQuota : 0;
+ }
+ }
+
+ private List<BlobInfo> queryBlobsForUserInternal(int userId) {
+ final ArrayList<BlobInfo> blobInfos = new ArrayList<>();
+ synchronized (mBlobsLock) {
+ final ArrayMap<String, WeakReference<Resources>> resources = new ArrayMap<>();
+ final Function<String, Resources> resourcesGetter = (packageName) -> {
+ final WeakReference<Resources> resourcesRef = resources.get(packageName);
+ Resources packageResources = resourcesRef == null ? null : resourcesRef.get();
+ if (packageResources == null) {
+ packageResources = getPackageResources(mContext, packageName, userId);
+ resources.put(packageName, new WeakReference<>(packageResources));
+ }
+ return packageResources;
+ };
+ getUserBlobsLocked(userId).forEach((blobHandle, blobMetadata) -> {
+ final ArrayList<LeaseInfo> leaseInfos = new ArrayList<>();
+ blobMetadata.forEachLeasee(leasee -> {
+ if (!leasee.isStillValid()) {
+ return;
+ }
+ final int descriptionResId = leasee.descriptionResEntryName == null
+ ? Resources.ID_NULL
+ : getDescriptionResourceId(resourcesGetter.apply(leasee.packageName),
+ leasee.descriptionResEntryName, leasee.packageName);
+ final long expiryTimeMs = leasee.expiryTimeMillis == 0
+ ? blobHandle.getExpiryTimeMillis() : leasee.expiryTimeMillis;
+ leaseInfos.add(new LeaseInfo(leasee.packageName, expiryTimeMs,
+ descriptionResId, leasee.description));
+ });
+ blobInfos.add(new BlobInfo(blobMetadata.getBlobId(),
+ blobHandle.getExpiryTimeMillis(), blobHandle.getLabel(),
+ blobMetadata.getSize(), leaseInfos));
+ });
+ }
+ return blobInfos;
+ }
+
+ private void deleteBlobInternal(long blobId, int callingUid) {
+ synchronized (mBlobsLock) {
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked(
+ UserHandle.getUserId(callingUid));
+ userBlobs.entrySet().removeIf(entry -> {
+ final BlobMetadata blobMetadata = entry.getValue();
+ if (blobMetadata.getBlobId() == blobId) {
+ deleteBlobLocked(blobMetadata);
+ return true;
+ }
+ return false;
+ });
+ writeBlobsInfoAsync();
+ }
+ }
+
+ private List<BlobHandle> getLeasedBlobsInternal(int callingUid,
+ @NonNull String callingPackage) {
+ final ArrayList<BlobHandle> leasedBlobs = new ArrayList<>();
+ forEachBlobInUser(blobMetadata -> {
+ if (blobMetadata.isALeasee(callingPackage, callingUid)) {
+ leasedBlobs.add(blobMetadata.getBlobHandle());
+ }
+ }, UserHandle.getUserId(callingUid));
+ return leasedBlobs;
+ }
+
+ private LeaseInfo getLeaseInfoInternal(BlobHandle blobHandle,
+ int callingUid, @NonNull String callingPackage) {
+ synchronized (mBlobsLock) {
+ final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid))
+ .get(blobHandle);
+ if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller(
+ callingPackage, callingUid)) {
+ throw new SecurityException("Caller not allowed to access " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
+ return blobMetadata.getLeaseInfo(callingPackage, callingUid);
+ }
+ }
+
+ private void verifyCallingPackage(int callingUid, String callingPackage) {
+ if (mPackageManagerInternal.getPackageUid(
+ callingPackage, 0, UserHandle.getUserId(callingUid)) != callingUid) {
+ throw new SecurityException("Specified calling package [" + callingPackage
+ + "] does not match the calling uid " + callingUid);
+ }
+ }
+
+ class SessionStateChangeListener {
+ public void onStateChanged(@NonNull BlobStoreSession session) {
+ mHandler.post(PooledLambda.obtainRunnable(
+ BlobStoreManagerService::onStateChangedInternal,
+ BlobStoreManagerService.this, session).recycleOnUse());
+ }
+ }
+
+ private void onStateChangedInternal(@NonNull BlobStoreSession session) {
+ switch (session.getState()) {
+ case STATE_ABANDONED:
+ case STATE_VERIFIED_INVALID:
+ synchronized (mBlobsLock) {
+ deleteSessionLocked(session);
+ getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid()))
+ .remove(session.getSessionId());
+ if (LOGV) {
+ Slog.v(TAG, "Session is invalid; deleted " + session);
+ }
+ }
+ break;
+ case STATE_COMMITTED:
+ mBackgroundHandler.post(() -> {
+ session.computeDigest();
+ mHandler.post(PooledLambda.obtainRunnable(
+ BlobStoreSession::verifyBlobData, session).recycleOnUse());
+ });
+ break;
+ case STATE_VERIFIED_VALID:
+ synchronized (mBlobsLock) {
+ final int committedBlobsCount = getCommittedBlobsCountLocked(
+ session.getOwnerUid(), session.getOwnerPackageName());
+ if (committedBlobsCount >= getMaxCommittedBlobs()) {
+ Slog.d(TAG, "Failed to commit: too many committed blobs. count: "
+ + committedBlobsCount + "; blob: " + session);
+ session.sendCommitCallbackResult(COMMIT_RESULT_ERROR);
+ deleteSessionLocked(session);
+ getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid()))
+ .remove(session.getSessionId());
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED,
+ session.getOwnerUid(), session.getSessionId(), session.getSize(),
+ FrameworkStatsLog.BLOB_COMMITTED__RESULT__COUNT_LIMIT_EXCEEDED);
+ break;
+ }
+ final int userId = UserHandle.getUserId(session.getOwnerUid());
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked(
+ userId);
+ BlobMetadata blob = userBlobs.get(session.getBlobHandle());
+ if (blob == null) {
+ blob = new BlobMetadata(mContext, session.getSessionId(),
+ session.getBlobHandle(), userId);
+ addBlobForUserLocked(blob, userBlobs);
+ }
+ final Committer existingCommitter = blob.getExistingCommitter(
+ session.getOwnerPackageName(), session.getOwnerUid());
+ final long existingCommitTimeMs =
+ (existingCommitter == null) ? 0 : existingCommitter.getCommitTimeMs();
+ final Committer newCommitter = new Committer(session.getOwnerPackageName(),
+ session.getOwnerUid(), session.getBlobAccessMode(),
+ getAdjustedCommitTimeMs(existingCommitTimeMs,
+ System.currentTimeMillis()));
+ blob.addOrReplaceCommitter(newCommitter);
+ try {
+ writeBlobsInfoLocked();
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED,
+ session.getOwnerUid(), blob.getBlobId(), blob.getSize(),
+ FrameworkStatsLog.BLOB_COMMITTED__RESULT__SUCCESS);
+ session.sendCommitCallbackResult(COMMIT_RESULT_SUCCESS);
+ } catch (Exception e) {
+ if (existingCommitter == null) {
+ blob.removeCommitter(newCommitter);
+ } else {
+ blob.addOrReplaceCommitter(existingCommitter);
+ }
+ Slog.d(TAG, "Error committing the blob: " + session, e);
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED,
+ session.getOwnerUid(), session.getSessionId(), blob.getSize(),
+ FrameworkStatsLog.BLOB_COMMITTED__RESULT__ERROR_DURING_COMMIT);
+ session.sendCommitCallbackResult(COMMIT_RESULT_ERROR);
+ // If the commit fails and this blob data didn't exist before, delete it.
+ // But if it is a recommit, just leave it as is.
+ if (session.getSessionId() == blob.getBlobId()) {
+ deleteBlobLocked(blob);
+ userBlobs.remove(blob.getBlobHandle());
+ }
+ }
+ // Delete redundant data from recommits.
+ if (session.getSessionId() != blob.getBlobId()) {
+ deleteSessionLocked(session);
+ }
+ getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid()))
+ .remove(session.getSessionId());
+ if (LOGV) {
+ Slog.v(TAG, "Successfully committed session " + session);
+ }
+ }
+ break;
+ default:
+ Slog.wtf(TAG, "Invalid session state: "
+ + stateToString(session.getState()));
+ }
+ synchronized (mBlobsLock) {
+ try {
+ writeBlobSessionsLocked();
+ } catch (Exception e) {
+ // already logged, ignore.
+ }
+ }
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void writeBlobSessionsLocked() throws Exception {
+ final AtomicFile sessionsIndexFile = prepareSessionsIndexFile();
+ if (sessionsIndexFile == null) {
+ Slog.wtf(TAG, "Error creating sessions index file");
+ return;
+ }
+ FileOutputStream fos = null;
+ try {
+ fos = sessionsIndexFile.startWrite(SystemClock.uptimeMillis());
+ final XmlSerializer out = new FastXmlSerializer();
+ out.setOutput(fos, StandardCharsets.UTF_8.name());
+ out.startDocument(null, true);
+ out.startTag(null, TAG_SESSIONS);
+ XmlUtils.writeIntAttribute(out, ATTR_VERSION, XML_VERSION_CURRENT);
+
+ for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) {
+ final LongSparseArray<BlobStoreSession> userSessions =
+ mSessions.valueAt(i);
+ for (int j = 0, sessionsCount = userSessions.size(); j < sessionsCount; ++j) {
+ out.startTag(null, TAG_SESSION);
+ userSessions.valueAt(j).writeToXml(out);
+ out.endTag(null, TAG_SESSION);
+ }
+ }
+
+ out.endTag(null, TAG_SESSIONS);
+ out.endDocument();
+ sessionsIndexFile.finishWrite(fos);
+ if (LOGV) {
+ Slog.v(TAG, "Finished persisting sessions data");
+ }
+ } catch (Exception e) {
+ sessionsIndexFile.failWrite(fos);
+ Slog.wtf(TAG, "Error writing sessions data", e);
+ throw e;
+ }
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void readBlobSessionsLocked(SparseArray<SparseArray<String>> allPackages) {
+ if (!BlobStoreConfig.getBlobStoreRootDir().exists()) {
+ return;
+ }
+ final AtomicFile sessionsIndexFile = prepareSessionsIndexFile();
+ if (sessionsIndexFile == null) {
+ Slog.wtf(TAG, "Error creating sessions index file");
+ return;
+ } else if (!sessionsIndexFile.exists()) {
+ Slog.w(TAG, "Sessions index file not available: " + sessionsIndexFile.getBaseFile());
+ return;
+ }
+
+ mSessions.clear();
+ try (FileInputStream fis = sessionsIndexFile.openRead()) {
+ final XmlPullParser in = Xml.newPullParser();
+ in.setInput(fis, StandardCharsets.UTF_8.name());
+ XmlUtils.beginDocument(in, TAG_SESSIONS);
+ final int version = XmlUtils.readIntAttribute(in, ATTR_VERSION);
+ while (true) {
+ XmlUtils.nextElement(in);
+ if (in.getEventType() == XmlPullParser.END_DOCUMENT) {
+ break;
+ }
+
+ if (TAG_SESSION.equals(in.getName())) {
+ final BlobStoreSession session = BlobStoreSession.createFromXml(
+ in, version, mContext, mSessionStateChangeListener);
+ if (session == null) {
+ continue;
+ }
+ final SparseArray<String> userPackages = allPackages.get(
+ UserHandle.getUserId(session.getOwnerUid()));
+ if (userPackages != null
+ && session.getOwnerPackageName().equals(
+ userPackages.get(session.getOwnerUid()))) {
+ addSessionForUserLocked(session,
+ UserHandle.getUserId(session.getOwnerUid()));
+ } else {
+ // Unknown package or the session data does not belong to this package.
+ session.getSessionFile().delete();
+ }
+ mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, session.getSessionId());
+ }
+ }
+ if (LOGV) {
+ Slog.v(TAG, "Finished reading sessions data");
+ }
+ } catch (Exception e) {
+ Slog.wtf(TAG, "Error reading sessions data", e);
+ }
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void writeBlobsInfoLocked() throws Exception {
+ final AtomicFile blobsIndexFile = prepareBlobsIndexFile();
+ if (blobsIndexFile == null) {
+ Slog.wtf(TAG, "Error creating blobs index file");
+ return;
+ }
+ FileOutputStream fos = null;
+ try {
+ fos = blobsIndexFile.startWrite(SystemClock.uptimeMillis());
+ final XmlSerializer out = new FastXmlSerializer();
+ out.setOutput(fos, StandardCharsets.UTF_8.name());
+ out.startDocument(null, true);
+ out.startTag(null, TAG_BLOBS);
+ XmlUtils.writeIntAttribute(out, ATTR_VERSION, XML_VERSION_CURRENT);
+
+ for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) {
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i);
+ for (int j = 0, blobsCount = userBlobs.size(); j < blobsCount; ++j) {
+ out.startTag(null, TAG_BLOB);
+ userBlobs.valueAt(j).writeToXml(out);
+ out.endTag(null, TAG_BLOB);
+ }
+ }
+
+ out.endTag(null, TAG_BLOBS);
+ out.endDocument();
+ blobsIndexFile.finishWrite(fos);
+ if (LOGV) {
+ Slog.v(TAG, "Finished persisting blobs data");
+ }
+ } catch (Exception e) {
+ blobsIndexFile.failWrite(fos);
+ Slog.wtf(TAG, "Error writing blobs data", e);
+ throw e;
+ }
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void readBlobsInfoLocked(SparseArray<SparseArray<String>> allPackages) {
+ if (!BlobStoreConfig.getBlobStoreRootDir().exists()) {
+ return;
+ }
+ final AtomicFile blobsIndexFile = prepareBlobsIndexFile();
+ if (blobsIndexFile == null) {
+ Slog.wtf(TAG, "Error creating blobs index file");
+ return;
+ } else if (!blobsIndexFile.exists()) {
+ Slog.w(TAG, "Blobs index file not available: " + blobsIndexFile.getBaseFile());
+ return;
+ }
+
+ mBlobsMap.clear();
+ try (FileInputStream fis = blobsIndexFile.openRead()) {
+ final XmlPullParser in = Xml.newPullParser();
+ in.setInput(fis, StandardCharsets.UTF_8.name());
+ XmlUtils.beginDocument(in, TAG_BLOBS);
+ final int version = XmlUtils.readIntAttribute(in, ATTR_VERSION);
+ while (true) {
+ XmlUtils.nextElement(in);
+ if (in.getEventType() == XmlPullParser.END_DOCUMENT) {
+ break;
+ }
+
+ if (TAG_BLOB.equals(in.getName())) {
+ final BlobMetadata blobMetadata = BlobMetadata.createFromXml(
+ in, version, mContext);
+ final SparseArray<String> userPackages = allPackages.get(
+ blobMetadata.getUserId());
+ if (userPackages == null) {
+ blobMetadata.getBlobFile().delete();
+ } else {
+ addBlobForUserLocked(blobMetadata, blobMetadata.getUserId());
+ blobMetadata.removeCommittersFromUnknownPkgs(userPackages);
+ blobMetadata.removeLeaseesFromUnknownPkgs(userPackages);
+ }
+ mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, blobMetadata.getBlobId());
+ }
+ }
+ if (LOGV) {
+ Slog.v(TAG, "Finished reading blobs data");
+ }
+ } catch (Exception e) {
+ Slog.wtf(TAG, "Error reading blobs data", e);
+ }
+ }
+
+ private void writeBlobsInfo() {
+ synchronized (mBlobsLock) {
+ try {
+ writeBlobsInfoLocked();
+ } catch (Exception e) {
+ // Already logged, ignore
+ }
+ }
+ }
+
+ private void writeBlobsInfoAsync() {
+ if (!mHandler.hasCallbacks(mSaveBlobsInfoRunnable)) {
+ mHandler.post(mSaveBlobsInfoRunnable);
+ }
+ }
+
+ private void writeBlobSessions() {
+ synchronized (mBlobsLock) {
+ try {
+ writeBlobSessionsLocked();
+ } catch (Exception e) {
+ // Already logged, ignore
+ }
+ }
+ }
+
+ private void writeBlobSessionsAsync() {
+ if (!mHandler.hasCallbacks(mSaveSessionsRunnable)) {
+ mHandler.post(mSaveSessionsRunnable);
+ }
+ }
+
+ private int getPackageUid(String packageName, int userId) {
+ final int uid = mPackageManagerInternal.getPackageUid(
+ packageName,
+ MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE | MATCH_UNINSTALLED_PACKAGES,
+ userId);
+ return uid;
+ }
+
+ private SparseArray<SparseArray<String>> getAllPackages() {
+ final SparseArray<SparseArray<String>> allPackages = new SparseArray<>();
+ final int[] allUsers = LocalServices.getService(UserManagerInternal.class).getUserIds();
+ for (int userId : allUsers) {
+ final SparseArray<String> userPackages = new SparseArray<>();
+ allPackages.put(userId, userPackages);
+ final List<ApplicationInfo> applicationInfos = mPackageManagerInternal
+ .getInstalledApplications(
+ MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE
+ | MATCH_UNINSTALLED_PACKAGES,
+ userId, Process.myUid());
+ for (int i = 0, count = applicationInfos.size(); i < count; ++i) {
+ final ApplicationInfo applicationInfo = applicationInfos.get(i);
+ userPackages.put(applicationInfo.uid, applicationInfo.packageName);
+ }
+ }
+ return allPackages;
+ }
+
+ AtomicFile prepareSessionsIndexFile() {
+ final File file = BlobStoreConfig.prepareSessionIndexFile();
+ if (file == null) {
+ return null;
+ }
+ return new AtomicFile(file, "session_index" /* commitLogTag */);
+ }
+
+ AtomicFile prepareBlobsIndexFile() {
+ final File file = BlobStoreConfig.prepareBlobsIndexFile();
+ if (file == null) {
+ return null;
+ }
+ return new AtomicFile(file, "blobs_index" /* commitLogTag */);
+ }
+
+ @VisibleForTesting
+ void handlePackageRemoved(String packageName, int uid) {
+ synchronized (mBlobsLock) {
+ // Clean up any pending sessions
+ final LongSparseArray<BlobStoreSession> userSessions =
+ getUserSessionsLocked(UserHandle.getUserId(uid));
+ userSessions.removeIf((sessionId, blobStoreSession) -> {
+ if (blobStoreSession.getOwnerUid() == uid
+ && blobStoreSession.getOwnerPackageName().equals(packageName)) {
+ deleteSessionLocked(blobStoreSession);
+ return true;
+ }
+ return false;
+ });
+ writeBlobSessionsAsync();
+
+ // Remove the package from the committer and leasee list
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs =
+ getUserBlobsLocked(UserHandle.getUserId(uid));
+ userBlobs.entrySet().removeIf(entry -> {
+ final BlobMetadata blobMetadata = entry.getValue();
+ final boolean isACommitter = blobMetadata.isACommitter(packageName, uid);
+ if (isACommitter) {
+ blobMetadata.removeCommitter(packageName, uid);
+ }
+ blobMetadata.removeLeasee(packageName, uid);
+ // Regardless of when the blob is committed, we need to delete
+ // it if it was from the deleted package to ensure we delete all traces of it.
+ if (blobMetadata.shouldBeDeleted(isACommitter /* respectLeaseWaitTime */)) {
+ deleteBlobLocked(blobMetadata);
+ return true;
+ }
+ return false;
+ });
+ writeBlobsInfoAsync();
+
+ if (LOGV) {
+ Slog.v(TAG, "Removed blobs data associated with pkg="
+ + packageName + ", uid=" + uid);
+ }
+ }
+ }
+
+ private void handleUserRemoved(int userId) {
+ synchronized (mBlobsLock) {
+ final LongSparseArray<BlobStoreSession> userSessions =
+ mSessions.removeReturnOld(userId);
+ if (userSessions != null) {
+ for (int i = 0, count = userSessions.size(); i < count; ++i) {
+ final BlobStoreSession session = userSessions.valueAt(i);
+ deleteSessionLocked(session);
+ }
+ }
+
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs =
+ mBlobsMap.removeReturnOld(userId);
+ if (userBlobs != null) {
+ for (int i = 0, count = userBlobs.size(); i < count; ++i) {
+ final BlobMetadata blobMetadata = userBlobs.valueAt(i);
+ deleteBlobLocked(blobMetadata);
+ }
+ }
+ if (LOGV) {
+ Slog.v(TAG, "Removed blobs data in user " + userId);
+ }
+ }
+ }
+
+ @GuardedBy("mBlobsLock")
+ @VisibleForTesting
+ void handleIdleMaintenanceLocked() {
+ // Cleanup any left over data on disk that is not part of index.
+ final ArrayList<Long> deletedBlobIds = new ArrayList<>();
+ final ArrayList<File> filesToDelete = new ArrayList<>();
+ final File blobsDir = BlobStoreConfig.getBlobsDir();
+ if (blobsDir.exists()) {
+ for (File file : blobsDir.listFiles()) {
+ try {
+ final long id = Long.parseLong(file.getName());
+ if (mActiveBlobIds.indexOf(id) < 0) {
+ filesToDelete.add(file);
+ deletedBlobIds.add(id);
+ }
+ } catch (NumberFormatException e) {
+ Slog.wtf(TAG, "Error parsing the file name: " + file, e);
+ filesToDelete.add(file);
+ }
+ }
+ for (int i = 0, count = filesToDelete.size(); i < count; ++i) {
+ filesToDelete.get(i).delete();
+ }
+ }
+
+ // Cleanup any stale blobs.
+ for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) {
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i);
+ userBlobs.entrySet().removeIf(entry -> {
+ final BlobMetadata blobMetadata = entry.getValue();
+
+ // Remove expired leases
+ blobMetadata.removeExpiredLeases();
+
+ if (blobMetadata.shouldBeDeleted(true /* respectLeaseWaitTime */)) {
+ deleteBlobLocked(blobMetadata);
+ deletedBlobIds.add(blobMetadata.getBlobId());
+ return true;
+ }
+ return false;
+ });
+ }
+ writeBlobsInfoAsync();
+
+ // Cleanup any stale sessions.
+ for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) {
+ final LongSparseArray<BlobStoreSession> userSessions = mSessions.valueAt(i);
+ userSessions.removeIf((sessionId, blobStoreSession) -> {
+ boolean shouldRemove = false;
+
+ // Cleanup sessions which haven't been modified in a while.
+ if (blobStoreSession.isExpired()) {
+ shouldRemove = true;
+ }
+
+ // Cleanup sessions with already expired data.
+ if (blobStoreSession.getBlobHandle().isExpired()) {
+ shouldRemove = true;
+ }
+
+ if (shouldRemove) {
+ deleteSessionLocked(blobStoreSession);
+ deletedBlobIds.add(blobStoreSession.getSessionId());
+ }
+ return shouldRemove;
+ });
+ }
+ Slog.d(TAG, "Completed idle maintenance; deleted "
+ + Arrays.toString(deletedBlobIds.toArray()));
+ writeBlobSessionsAsync();
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void deleteSessionLocked(BlobStoreSession blobStoreSession) {
+ blobStoreSession.destroy();
+ mActiveBlobIds.remove(blobStoreSession.getSessionId());
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void deleteBlobLocked(BlobMetadata blobMetadata) {
+ blobMetadata.destroy();
+ mActiveBlobIds.remove(blobMetadata.getBlobId());
+ }
+
+ void runClearAllSessions(@UserIdInt int userId) {
+ synchronized (mBlobsLock) {
+ for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) {
+ final int sessionUserId = mSessions.keyAt(i);
+ if (userId != UserHandle.USER_ALL && userId != sessionUserId) {
+ continue;
+ }
+ final LongSparseArray<BlobStoreSession> userSessions = mSessions.valueAt(i);
+ for (int j = 0, sessionsCount = userSessions.size(); j < sessionsCount; ++j) {
+ mActiveBlobIds.remove(userSessions.valueAt(j).getSessionId());
+ }
+ }
+ if (userId == UserHandle.USER_ALL) {
+ mSessions.clear();
+ } else {
+ mSessions.remove(userId);
+ }
+ writeBlobSessionsAsync();
+ }
+ }
+
+ void runClearAllBlobs(@UserIdInt int userId) {
+ synchronized (mBlobsLock) {
+ for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) {
+ final int blobUserId = mBlobsMap.keyAt(i);
+ if (userId != UserHandle.USER_ALL && userId != blobUserId) {
+ continue;
+ }
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i);
+ for (int j = 0, blobsCount = userBlobs.size(); j < blobsCount; ++j) {
+ mActiveBlobIds.remove(userBlobs.valueAt(j).getBlobId());
+ }
+ }
+ if (userId == UserHandle.USER_ALL) {
+ mBlobsMap.clear();
+ } else {
+ mBlobsMap.remove(userId);
+ }
+ writeBlobsInfoAsync();
+ }
+ }
+
+ void deleteBlob(@NonNull BlobHandle blobHandle, @UserIdInt int userId) {
+ synchronized (mBlobsLock) {
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked(userId);
+ final BlobMetadata blobMetadata = userBlobs.get(blobHandle);
+ if (blobMetadata == null) {
+ return;
+ }
+ deleteBlobLocked(blobMetadata);
+ userBlobs.remove(blobHandle);
+ writeBlobsInfoAsync();
+ }
+ }
+
+ void runIdleMaintenance() {
+ synchronized (mBlobsLock) {
+ handleIdleMaintenanceLocked();
+ }
+ }
+
+ boolean isBlobAvailable(long blobId, int userId) {
+ synchronized (mBlobsLock) {
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked(userId);
+ for (BlobMetadata blobMetadata : userBlobs.values()) {
+ if (blobMetadata.getBlobId() == blobId) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void dumpSessionsLocked(IndentingPrintWriter fout, DumpArgs dumpArgs) {
+ for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) {
+ final int userId = mSessions.keyAt(i);
+ if (!dumpArgs.shouldDumpUser(userId)) {
+ continue;
+ }
+ final LongSparseArray<BlobStoreSession> userSessions = mSessions.valueAt(i);
+ fout.println("List of sessions in user #"
+ + userId + " (" + userSessions.size() + "):");
+ fout.increaseIndent();
+ for (int j = 0, sessionsCount = userSessions.size(); j < sessionsCount; ++j) {
+ final long sessionId = userSessions.keyAt(j);
+ final BlobStoreSession session = userSessions.valueAt(j);
+ if (!dumpArgs.shouldDumpSession(session.getOwnerPackageName(),
+ session.getOwnerUid(), session.getSessionId())) {
+ continue;
+ }
+ fout.println("Session #" + sessionId);
+ fout.increaseIndent();
+ session.dump(fout, dumpArgs);
+ fout.decreaseIndent();
+ }
+ fout.decreaseIndent();
+ }
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void dumpBlobsLocked(IndentingPrintWriter fout, DumpArgs dumpArgs) {
+ for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) {
+ final int userId = mBlobsMap.keyAt(i);
+ if (!dumpArgs.shouldDumpUser(userId)) {
+ continue;
+ }
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i);
+ fout.println("List of blobs in user #"
+ + userId + " (" + userBlobs.size() + "):");
+ fout.increaseIndent();
+ for (int j = 0, blobsCount = userBlobs.size(); j < blobsCount; ++j) {
+ final BlobMetadata blobMetadata = userBlobs.valueAt(j);
+ if (!dumpArgs.shouldDumpBlob(blobMetadata.getBlobId())) {
+ continue;
+ }
+ fout.println("Blob #" + blobMetadata.getBlobId());
+ fout.increaseIndent();
+ blobMetadata.dump(fout, dumpArgs);
+ fout.decreaseIndent();
+ }
+ fout.decreaseIndent();
+ }
+ }
+
+ private class BlobStorageStatsAugmenter implements StorageStatsAugmenter {
+ @Override
+ public void augmentStatsForPackage(@NonNull PackageStats stats, @NonNull String packageName,
+ @UserIdInt int userId, boolean callerHasStatsPermission) {
+ final AtomicLong blobsDataSize = new AtomicLong(0);
+ forEachSessionInUser(session -> {
+ if (session.getOwnerPackageName().equals(packageName)) {
+ blobsDataSize.getAndAdd(session.getSize());
+ }
+ }, userId);
+
+ forEachBlobInUser(blobMetadata -> {
+ if (blobMetadata.isALeasee(packageName)) {
+ if (!blobMetadata.hasOtherLeasees(packageName) || !callerHasStatsPermission) {
+ blobsDataSize.getAndAdd(blobMetadata.getSize());
+ }
+ }
+ }, userId);
+
+ stats.dataSize += blobsDataSize.get();
+ }
+
+ @Override
+ public void augmentStatsForUid(@NonNull PackageStats stats, int uid,
+ boolean callerHasStatsPermission) {
+ final int userId = UserHandle.getUserId(uid);
+ final AtomicLong blobsDataSize = new AtomicLong(0);
+ forEachSessionInUser(session -> {
+ if (session.getOwnerUid() == uid) {
+ blobsDataSize.getAndAdd(session.getSize());
+ }
+ }, userId);
+
+ forEachBlobInUser(blobMetadata -> {
+ if (blobMetadata.isALeasee(uid)) {
+ if (!blobMetadata.hasOtherLeasees(uid) || !callerHasStatsPermission) {
+ blobsDataSize.getAndAdd(blobMetadata.getSize());
+ }
+ }
+ }, userId);
+
+ stats.dataSize += blobsDataSize.get();
+ }
+ }
+
+ private void forEachSessionInUser(Consumer<BlobStoreSession> consumer, int userId) {
+ synchronized (mBlobsLock) {
+ final LongSparseArray<BlobStoreSession> userSessions = getUserSessionsLocked(userId);
+ for (int i = 0, count = userSessions.size(); i < count; ++i) {
+ final BlobStoreSession session = userSessions.valueAt(i);
+ consumer.accept(session);
+ }
+ }
+ }
+
+ private void forEachBlobInUser(Consumer<BlobMetadata> consumer, int userId) {
+ synchronized (mBlobsLock) {
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked(userId);
+ for (int i = 0, count = userBlobs.size(); i < count; ++i) {
+ final BlobMetadata blobMetadata = userBlobs.valueAt(i);
+ consumer.accept(blobMetadata);
+ }
+ }
+ }
+
+ private class PackageChangedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (LOGV) {
+ Slog.v(TAG, "Received " + intent);
+ }
+ switch (intent.getAction()) {
+ case Intent.ACTION_PACKAGE_FULLY_REMOVED:
+ case Intent.ACTION_PACKAGE_DATA_CLEARED:
+ final String packageName = intent.getData().getSchemeSpecificPart();
+ if (packageName == null) {
+ Slog.wtf(TAG, "Package name is missing in the intent: " + intent);
+ return;
+ }
+ final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ if (uid == -1) {
+ Slog.wtf(TAG, "uid is missing in the intent: " + intent);
+ return;
+ }
+ handlePackageRemoved(packageName, uid);
+ break;
+ default:
+ Slog.wtf(TAG, "Received unknown intent: " + intent);
+ }
+ }
+ }
+
+ private class UserActionReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (LOGV) {
+ Slog.v(TAG, "Received: " + intent);
+ }
+ switch (intent.getAction()) {
+ case Intent.ACTION_USER_REMOVED:
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
+ USER_NULL);
+ if (userId == USER_NULL) {
+ Slog.wtf(TAG, "userId is missing in the intent: " + intent);
+ return;
+ }
+ handleUserRemoved(userId);
+ break;
+ default:
+ Slog.wtf(TAG, "Received unknown intent: " + intent);
+ }
+ }
+ }
+
+ private class Stub extends IBlobStoreManager.Stub {
+ @Override
+ @IntRange(from = 1)
+ public long createSession(@NonNull BlobHandle blobHandle,
+ @NonNull String packageName) {
+ Objects.requireNonNull(blobHandle, "blobHandle must not be null");
+ blobHandle.assertIsValid();
+ Objects.requireNonNull(packageName, "packageName must not be null");
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp(
+ packageName, UserHandle.getUserId(callingUid))) {
+ throw new SecurityException("Caller not allowed to create session; "
+ + "callingUid=" + callingUid + ", callingPackage=" + packageName);
+ }
+
+ try {
+ return createSessionInternal(blobHandle, callingUid, packageName);
+ } catch (LimitExceededException e) {
+ throw new ParcelableException(e);
+ }
+ }
+
+ @Override
+ @NonNull
+ public IBlobStoreSession openSession(@IntRange(from = 1) long sessionId,
+ @NonNull String packageName) {
+ Preconditions.checkArgumentPositive(sessionId,
+ "sessionId must be positive: " + sessionId);
+ Objects.requireNonNull(packageName, "packageName must not be null");
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ return openSessionInternal(sessionId, callingUid, packageName);
+ }
+
+ @Override
+ public void abandonSession(@IntRange(from = 1) long sessionId,
+ @NonNull String packageName) {
+ Preconditions.checkArgumentPositive(sessionId,
+ "sessionId must be positive: " + sessionId);
+ Objects.requireNonNull(packageName, "packageName must not be null");
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ abandonSessionInternal(sessionId, callingUid, packageName);
+ }
+
+ @Override
+ public ParcelFileDescriptor openBlob(@NonNull BlobHandle blobHandle,
+ @NonNull String packageName) {
+ Objects.requireNonNull(blobHandle, "blobHandle must not be null");
+ blobHandle.assertIsValid();
+ Objects.requireNonNull(packageName, "packageName must not be null");
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp(
+ packageName, UserHandle.getUserId(callingUid))) {
+ throw new SecurityException("Caller not allowed to open blob; "
+ + "callingUid=" + callingUid + ", callingPackage=" + packageName);
+ }
+
+ try {
+ return openBlobInternal(blobHandle, callingUid, packageName);
+ } catch (IOException e) {
+ throw ExceptionUtils.wrap(e);
+ }
+ }
+
+ @Override
+ public void acquireLease(@NonNull BlobHandle blobHandle, @IdRes int descriptionResId,
+ @Nullable CharSequence description,
+ @CurrentTimeSecondsLong long leaseExpiryTimeMillis, @NonNull String packageName) {
+ Objects.requireNonNull(blobHandle, "blobHandle must not be null");
+ blobHandle.assertIsValid();
+ Preconditions.checkArgument(
+ ResourceId.isValid(descriptionResId) || description != null,
+ "Description must be valid; descriptionId=" + descriptionResId
+ + ", description=" + description);
+ Preconditions.checkArgumentNonnegative(leaseExpiryTimeMillis,
+ "leaseExpiryTimeMillis must not be negative");
+ Objects.requireNonNull(packageName, "packageName must not be null");
+
+ description = BlobStoreConfig.getTruncatedLeaseDescription(description);
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp(
+ packageName, UserHandle.getUserId(callingUid))) {
+ throw new SecurityException("Caller not allowed to open blob; "
+ + "callingUid=" + callingUid + ", callingPackage=" + packageName);
+ }
+
+ try {
+ acquireLeaseInternal(blobHandle, descriptionResId, description,
+ leaseExpiryTimeMillis, callingUid, packageName);
+ } catch (Resources.NotFoundException e) {
+ throw new IllegalArgumentException(e);
+ } catch (LimitExceededException e) {
+ throw new ParcelableException(e);
+ }
+ }
+
+ @Override
+ public void releaseLease(@NonNull BlobHandle blobHandle, @NonNull String packageName) {
+ Objects.requireNonNull(blobHandle, "blobHandle must not be null");
+ blobHandle.assertIsValid();
+ Objects.requireNonNull(packageName, "packageName must not be null");
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp(
+ packageName, UserHandle.getUserId(callingUid))) {
+ throw new SecurityException("Caller not allowed to open blob; "
+ + "callingUid=" + callingUid + ", callingPackage=" + packageName);
+ }
+
+ releaseLeaseInternal(blobHandle, callingUid, packageName);
+ }
+
+ @Override
+ public long getRemainingLeaseQuotaBytes(@NonNull String packageName) {
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ return getRemainingLeaseQuotaBytesInternal(callingUid, packageName);
+ }
+
+ @Override
+ public void waitForIdle(@NonNull RemoteCallback remoteCallback) {
+ Objects.requireNonNull(remoteCallback, "remoteCallback must not be null");
+
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP,
+ "Caller is not allowed to call this; caller=" + Binder.getCallingUid());
+ // We post messages back and forth between mHandler thread and mBackgroundHandler
+ // thread while committing a blob. We need to replicate the same pattern here to
+ // ensure pending messages have been handled.
+ mHandler.post(() -> {
+ mBackgroundHandler.post(() -> {
+ mHandler.post(PooledLambda.obtainRunnable(remoteCallback::sendResult, null)
+ .recycleOnUse());
+ });
+ });
+ }
+
+ @Override
+ @NonNull
+ public List<BlobInfo> queryBlobsForUser(@UserIdInt int userId) {
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("Only system uid is allowed to call "
+ + "queryBlobsForUser()");
+ }
+
+ final int resolvedUserId = userId == USER_CURRENT
+ ? ActivityManager.getCurrentUser() : userId;
+ // Don't allow any other special user ids apart from USER_CURRENT
+ final ActivityManagerInternal amInternal = LocalServices.getService(
+ ActivityManagerInternal.class);
+ amInternal.ensureNotSpecialUser(resolvedUserId);
+
+ return queryBlobsForUserInternal(resolvedUserId);
+ }
+
+ @Override
+ public void deleteBlob(long blobId) {
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid != Process.SYSTEM_UID) {
+ throw new SecurityException("Only system uid is allowed to call "
+ + "deleteBlob()");
+ }
+
+ deleteBlobInternal(blobId, callingUid);
+ }
+
+ @Override
+ @NonNull
+ public List<BlobHandle> getLeasedBlobs(@NonNull String packageName) {
+ Objects.requireNonNull(packageName, "packageName must not be null");
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ return getLeasedBlobsInternal(callingUid, packageName);
+ }
+
+ @Override
+ @Nullable
+ public LeaseInfo getLeaseInfo(@NonNull BlobHandle blobHandle, @NonNull String packageName) {
+ Objects.requireNonNull(blobHandle, "blobHandle must not be null");
+ blobHandle.assertIsValid();
+ Objects.requireNonNull(packageName, "packageName must not be null");
+
+ final int callingUid = Binder.getCallingUid();
+ verifyCallingPackage(callingUid, packageName);
+
+ if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp(
+ packageName, UserHandle.getUserId(callingUid))) {
+ throw new SecurityException("Caller not allowed to open blob; "
+ + "callingUid=" + callingUid + ", callingPackage=" + packageName);
+ }
+
+ return getLeaseInfoInternal(blobHandle, callingUid, packageName);
+ }
+
+ @Override
+ public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
+ @Nullable String[] args) {
+ // TODO: add proto-based version of this.
+ if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, writer)) return;
+
+ final DumpArgs dumpArgs = DumpArgs.parse(args);
+
+ final IndentingPrintWriter fout = new IndentingPrintWriter(writer, " ");
+ if (dumpArgs.shouldDumpHelp()) {
+ writer.println("dumpsys blob_store [options]:");
+ fout.increaseIndent();
+ dumpArgs.dumpArgsUsage(fout);
+ fout.decreaseIndent();
+ return;
+ }
+
+ synchronized (mBlobsLock) {
+ if (dumpArgs.shouldDumpAllSections()) {
+ fout.println("mCurrentMaxSessionId: " + mCurrentMaxSessionId);
+ fout.println();
+ }
+
+ if (dumpArgs.shouldDumpSessions()) {
+ dumpSessionsLocked(fout, dumpArgs);
+ fout.println();
+ }
+ if (dumpArgs.shouldDumpBlobs()) {
+ dumpBlobsLocked(fout, dumpArgs);
+ fout.println();
+ }
+ }
+
+ if (dumpArgs.shouldDumpConfig()) {
+ fout.println("BlobStore config:");
+ fout.increaseIndent();
+ BlobStoreConfig.dump(fout, mContext);
+ fout.decreaseIndent();
+ fout.println();
+ }
+ }
+
+ @Override
+ public int handleShellCommand(@NonNull ParcelFileDescriptor in,
+ @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
+ @NonNull String[] args) {
+ return new BlobStoreManagerShellCommand(BlobStoreManagerService.this).exec(this,
+ in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args);
+ }
+ }
+
+ static final class DumpArgs {
+ private static final int FLAG_DUMP_SESSIONS = 1 << 0;
+ private static final int FLAG_DUMP_BLOBS = 1 << 1;
+ private static final int FLAG_DUMP_CONFIG = 1 << 2;
+
+ private int mSelectedSectionFlags;
+ private boolean mDumpUnredacted;
+ private final ArrayList<String> mDumpPackages = new ArrayList<>();
+ private final ArrayList<Integer> mDumpUids = new ArrayList<>();
+ private final ArrayList<Integer> mDumpUserIds = new ArrayList<>();
+ private final ArrayList<Long> mDumpBlobIds = new ArrayList<>();
+ private boolean mDumpHelp;
+ private boolean mDumpAll;
+
+ public boolean shouldDumpSession(String packageName, int uid, long blobId) {
+ if (!CollectionUtils.isEmpty(mDumpPackages)
+ && mDumpPackages.indexOf(packageName) < 0) {
+ return false;
+ }
+ if (!CollectionUtils.isEmpty(mDumpUids)
+ && mDumpUids.indexOf(uid) < 0) {
+ return false;
+ }
+ if (!CollectionUtils.isEmpty(mDumpBlobIds)
+ && mDumpBlobIds.indexOf(blobId) < 0) {
+ return false;
+ }
+ return true;
+ }
+
+ public boolean shouldDumpAllSections() {
+ return mDumpAll || (mSelectedSectionFlags == 0);
+ }
+
+ public void allowDumpSessions() {
+ mSelectedSectionFlags |= FLAG_DUMP_SESSIONS;
+ }
+
+ public boolean shouldDumpSessions() {
+ if (shouldDumpAllSections()) {
+ return true;
+ }
+ return (mSelectedSectionFlags & FLAG_DUMP_SESSIONS) != 0;
+ }
+
+ public void allowDumpBlobs() {
+ mSelectedSectionFlags |= FLAG_DUMP_BLOBS;
+ }
+
+ public boolean shouldDumpBlobs() {
+ if (shouldDumpAllSections()) {
+ return true;
+ }
+ return (mSelectedSectionFlags & FLAG_DUMP_BLOBS) != 0;
+ }
+
+ public void allowDumpConfig() {
+ mSelectedSectionFlags |= FLAG_DUMP_CONFIG;
+ }
+
+ public boolean shouldDumpConfig() {
+ if (shouldDumpAllSections()) {
+ return true;
+ }
+ return (mSelectedSectionFlags & FLAG_DUMP_CONFIG) != 0;
+ }
+
+ public boolean shouldDumpBlob(long blobId) {
+ return CollectionUtils.isEmpty(mDumpBlobIds)
+ || mDumpBlobIds.indexOf(blobId) >= 0;
+ }
+
+ public boolean shouldDumpFull() {
+ return mDumpUnredacted;
+ }
+
+ public boolean shouldDumpUser(int userId) {
+ return CollectionUtils.isEmpty(mDumpUserIds)
+ || mDumpUserIds.indexOf(userId) >= 0;
+ }
+
+ public boolean shouldDumpHelp() {
+ return mDumpHelp;
+ }
+
+ private DumpArgs() {}
+
+ public static DumpArgs parse(String[] args) {
+ final DumpArgs dumpArgs = new DumpArgs();
+ if (args == null) {
+ return dumpArgs;
+ }
+
+ for (int i = 0; i < args.length; ++i) {
+ final String opt = args[i];
+ if ("--all".equals(opt) || "-a".equals(opt)) {
+ dumpArgs.mDumpAll = true;
+ } else if ("--unredacted".equals(opt) || "-u".equals(opt)) {
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID) {
+ dumpArgs.mDumpUnredacted = true;
+ }
+ } else if ("--sessions".equals(opt)) {
+ dumpArgs.allowDumpSessions();
+ } else if ("--blobs".equals(opt)) {
+ dumpArgs.allowDumpBlobs();
+ } else if ("--config".equals(opt)) {
+ dumpArgs.allowDumpConfig();
+ } else if ("--package".equals(opt) || "-p".equals(opt)) {
+ dumpArgs.mDumpPackages.add(getStringArgRequired(args, ++i, "packageName"));
+ } else if ("--uid".equals(opt)) {
+ dumpArgs.mDumpUids.add(getIntArgRequired(args, ++i, "uid"));
+ } else if ("--user".equals(opt)) {
+ dumpArgs.mDumpUserIds.add(getIntArgRequired(args, ++i, "userId"));
+ } else if ("--blob".equals(opt) || "-b".equals(opt)) {
+ dumpArgs.mDumpBlobIds.add(getLongArgRequired(args, ++i, "blobId"));
+ } else if ("--help".equals(opt) || "-h".equals(opt)) {
+ dumpArgs.mDumpHelp = true;
+ } else {
+ // Everything else is assumed to be blob ids.
+ dumpArgs.mDumpBlobIds.add(getLongArgRequired(args, i, "blobId"));
+ }
+ }
+ return dumpArgs;
+ }
+
+ private static String getStringArgRequired(String[] args, int index, String argName) {
+ if (index >= args.length) {
+ throw new IllegalArgumentException("Missing " + argName);
+ }
+ return args[index];
+ }
+
+ private static int getIntArgRequired(String[] args, int index, String argName) {
+ if (index >= args.length) {
+ throw new IllegalArgumentException("Missing " + argName);
+ }
+ final int value;
+ try {
+ value = Integer.parseInt(args[index]);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid " + argName + ": " + args[index]);
+ }
+ return value;
+ }
+
+ private static long getLongArgRequired(String[] args, int index, String argName) {
+ if (index >= args.length) {
+ throw new IllegalArgumentException("Missing " + argName);
+ }
+ final long value;
+ try {
+ value = Long.parseLong(args[index]);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid " + argName + ": " + args[index]);
+ }
+ return value;
+ }
+
+ private void dumpArgsUsage(IndentingPrintWriter pw) {
+ pw.println("--help | -h");
+ printWithIndent(pw, "Dump this help text");
+ pw.println("--sessions");
+ printWithIndent(pw, "Dump only the sessions info");
+ pw.println("--blobs");
+ printWithIndent(pw, "Dump only the committed blobs info");
+ pw.println("--config");
+ printWithIndent(pw, "Dump only the config values");
+ pw.println("--package | -p [package-name]");
+ printWithIndent(pw, "Dump blobs info associated with the given package");
+ pw.println("--uid | -u [uid]");
+ printWithIndent(pw, "Dump blobs info associated with the given uid");
+ pw.println("--user [user-id]");
+ printWithIndent(pw, "Dump blobs info in the given user");
+ pw.println("--blob | -b [session-id | blob-id]");
+ printWithIndent(pw, "Dump blob info corresponding to the given ID");
+ pw.println("--full | -f");
+ printWithIndent(pw, "Dump full unredacted blobs data");
+ }
+
+ private void printWithIndent(IndentingPrintWriter pw, String str) {
+ pw.increaseIndent();
+ pw.println(str);
+ pw.decreaseIndent();
+ }
+ }
+
+ private void registerBlobStorePuller() {
+ mStatsManager.setPullAtomCallback(
+ FrameworkStatsLog.BLOB_INFO,
+ null, // use default PullAtomMetadata values
+ BackgroundThread.getExecutor(),
+ mStatsCallbackImpl
+ );
+ }
+
+ private class StatsPullAtomCallbackImpl implements StatsManager.StatsPullAtomCallback {
+ @Override
+ public int onPullAtom(int atomTag, List<StatsEvent> data) {
+ switch (atomTag) {
+ case FrameworkStatsLog.BLOB_INFO:
+ return pullBlobData(atomTag, data);
+ default:
+ throw new UnsupportedOperationException("Unknown tagId=" + atomTag);
+ }
+ }
+ }
+
+ private int pullBlobData(int atomTag, List<StatsEvent> data) {
+ synchronized (mBlobsLock) {
+ for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) {
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i);
+ for (int j = 0, blobsCount = userBlobs.size(); j < blobsCount; ++j) {
+ final BlobMetadata blob = userBlobs.valueAt(j);
+ data.add(blob.dumpAsStatsEvent(atomTag));
+ }
+ }
+ }
+ return StatsManager.PULL_SUCCESS;
+ }
+
+ private class LocalService extends BlobStoreManagerInternal {
+ @Override
+ public void onIdleMaintenance() {
+ runIdleMaintenance();
+ }
+ }
+
+ @VisibleForTesting
+ static class Injector {
+ public Handler initializeMessageHandler() {
+ return BlobStoreManagerService.initializeMessageHandler();
+ }
+
+ public Handler getBackgroundHandler() {
+ return BackgroundThread.getHandler();
+ }
+ }
+} \ No newline at end of file
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java
new file mode 100644
index 000000000000..a4a2e80c195a
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2020 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.ActivityManager;
+import android.app.blob.BlobHandle;
+import android.os.ShellCommand;
+import android.os.UserHandle;
+
+import java.io.PrintWriter;
+import java.util.Base64;
+
+class BlobStoreManagerShellCommand extends ShellCommand {
+
+ private final BlobStoreManagerService mService;
+
+ BlobStoreManagerShellCommand(BlobStoreManagerService blobStoreManagerService) {
+ mService = blobStoreManagerService;
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(null);
+ }
+ final PrintWriter pw = getOutPrintWriter();
+ switch (cmd) {
+ case "clear-all-sessions":
+ return runClearAllSessions(pw);
+ case "clear-all-blobs":
+ return runClearAllBlobs(pw);
+ case "delete-blob":
+ return runDeleteBlob(pw);
+ case "idle-maintenance":
+ return runIdleMaintenance(pw);
+ case "query-blob-existence":
+ return runQueryBlobExistence(pw);
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ }
+
+ private int runClearAllSessions(PrintWriter pw) {
+ final ParsedArgs args = new ParsedArgs();
+ args.userId = UserHandle.USER_ALL;
+
+ if (parseOptions(pw, args) < 0) {
+ return -1;
+ }
+
+ mService.runClearAllSessions(args.userId);
+ return 0;
+ }
+
+ private int runClearAllBlobs(PrintWriter pw) {
+ final ParsedArgs args = new ParsedArgs();
+ args.userId = UserHandle.USER_ALL;
+
+ if (parseOptions(pw, args) < 0) {
+ return -1;
+ }
+
+ mService.runClearAllBlobs(args.userId);
+ return 0;
+ }
+
+ private int runDeleteBlob(PrintWriter pw) {
+ final ParsedArgs args = new ParsedArgs();
+
+ if (parseOptions(pw, args) < 0) {
+ return -1;
+ }
+
+ mService.deleteBlob(args.getBlobHandle(), args.userId);
+ return 0;
+ }
+
+ private int runIdleMaintenance(PrintWriter pw) {
+ mService.runIdleMaintenance();
+ return 0;
+ }
+
+ private int runQueryBlobExistence(PrintWriter pw) {
+ final ParsedArgs args = new ParsedArgs();
+ if (parseOptions(pw, args) < 0) {
+ return -1;
+ }
+
+ pw.println(mService.isBlobAvailable(args.blobId, args.userId) ? 1 : 0);
+ return 0;
+ }
+
+ @Override
+ public void onHelp() {
+ final PrintWriter pw = getOutPrintWriter();
+ pw.println("BlobStore service (blob_store) commands:");
+ pw.println("help");
+ pw.println(" Print this help text.");
+ pw.println();
+ pw.println("clear-all-sessions [-u | --user USER_ID]");
+ pw.println(" Remove all sessions.");
+ pw.println(" Options:");
+ pw.println(" -u or --user: specify which user's sessions to be removed.");
+ pw.println(" If not specified, sessions in all users are removed.");
+ pw.println();
+ pw.println("clear-all-blobs [-u | --user USER_ID]");
+ pw.println(" Remove all blobs.");
+ pw.println(" Options:");
+ pw.println(" -u or --user: specify which user's blobs to be removed.");
+ pw.println(" If not specified, blobs in all users are removed.");
+ pw.println("delete-blob [-u | --user USER_ID] [--digest DIGEST] [--expiry EXPIRY_TIME] "
+ + "[--label LABEL] [--tag TAG]");
+ pw.println(" Delete a blob.");
+ pw.println(" Options:");
+ pw.println(" -u or --user: specify which user's blobs to be removed;");
+ pw.println(" If not specified, blobs in all users are removed.");
+ pw.println(" --digest: Base64 encoded digest of the blob to delete.");
+ pw.println(" --expiry: Expiry time of the blob to delete, in milliseconds.");
+ pw.println(" --label: Label of the blob to delete.");
+ pw.println(" --tag: Tag of the blob to delete.");
+ pw.println("idle-maintenance");
+ pw.println(" Run idle maintenance which takes care of removing stale data.");
+ pw.println("query-blob-existence [-b BLOB_ID]");
+ pw.println(" Prints 1 if blob exists, otherwise 0.");
+ pw.println();
+ }
+
+ private int parseOptions(PrintWriter pw, ParsedArgs args) {
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-u":
+ case "--user":
+ args.userId = Integer.parseInt(getNextArgRequired());
+ break;
+ case "--algo":
+ args.algorithm = getNextArgRequired();
+ break;
+ case "--digest":
+ args.digest = Base64.getDecoder().decode(getNextArgRequired());
+ break;
+ case "--label":
+ args.label = getNextArgRequired();
+ break;
+ case "--expiry":
+ args.expiryTimeMillis = Long.parseLong(getNextArgRequired());
+ break;
+ case "--tag":
+ args.tag = getNextArgRequired();
+ break;
+ case "-b":
+ args.blobId = Long.parseLong(getNextArgRequired());
+ break;
+ default:
+ pw.println("Error: unknown option '" + opt + "'");
+ return -1;
+ }
+ }
+ if (args.userId == UserHandle.USER_CURRENT) {
+ args.userId = ActivityManager.getCurrentUser();
+ }
+ return 0;
+ }
+
+ private static class ParsedArgs {
+ public int userId = UserHandle.USER_CURRENT;
+
+ public String algorithm = BlobHandle.ALGO_SHA_256;
+ public byte[] digest;
+ public long expiryTimeMillis;
+ public CharSequence label;
+ public String tag;
+ public long blobId;
+
+ public BlobHandle getBlobHandle() {
+ return BlobHandle.create(algorithm, digest, label, expiryTimeMillis, tag);
+ }
+ }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
new file mode 100644
index 000000000000..2f83be1e0370
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
@@ -0,0 +1,629 @@
+/*
+ * Copyright 2020 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 static android.app.blob.BlobStoreManager.COMMIT_RESULT_ERROR;
+import static android.app.blob.XmlTags.ATTR_CREATION_TIME_MS;
+import static android.app.blob.XmlTags.ATTR_ID;
+import static android.app.blob.XmlTags.ATTR_PACKAGE;
+import static android.app.blob.XmlTags.ATTR_UID;
+import static android.app.blob.XmlTags.TAG_ACCESS_MODE;
+import static android.app.blob.XmlTags.TAG_BLOB_HANDLE;
+import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER;
+import static android.system.OsConstants.O_CREAT;
+import static android.system.OsConstants.O_RDONLY;
+import static android.system.OsConstants.O_RDWR;
+import static android.system.OsConstants.SEEK_SET;
+import static android.text.format.Formatter.FLAG_IEC_UNITS;
+import static android.text.format.Formatter.formatFileSize;
+
+import static com.android.server.blob.BlobStoreConfig.TAG;
+import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_SESSION_CREATION_TIME;
+import static com.android.server.blob.BlobStoreConfig.getMaxPermittedPackages;
+import static com.android.server.blob.BlobStoreConfig.hasSessionExpired;
+
+import android.annotation.BytesLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.blob.BlobHandle;
+import android.app.blob.IBlobCommitCallback;
+import android.app.blob.IBlobStoreSession;
+import android.content.Context;
+import android.os.Binder;
+import android.os.FileUtils;
+import android.os.LimitExceededException;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelableException;
+import android.os.RemoteException;
+import android.os.RevocableFileDescriptor;
+import android.os.Trace;
+import android.os.storage.StorageManager;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.ExceptionUtils;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.XmlUtils;
+import com.android.server.blob.BlobStoreManagerService.DumpArgs;
+import com.android.server.blob.BlobStoreManagerService.SessionStateChangeListener;
+
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Class to represent the state corresponding to an ongoing
+ * {@link android.app.blob.BlobStoreManager.Session}
+ */
+@VisibleForTesting
+class BlobStoreSession extends IBlobStoreSession.Stub {
+
+ static final int STATE_OPENED = 1;
+ static final int STATE_CLOSED = 0;
+ static final int STATE_ABANDONED = 2;
+ static final int STATE_COMMITTED = 3;
+ static final int STATE_VERIFIED_VALID = 4;
+ static final int STATE_VERIFIED_INVALID = 5;
+
+ private final Object mSessionLock = new Object();
+
+ private final Context mContext;
+ private final SessionStateChangeListener mListener;
+
+ private final BlobHandle mBlobHandle;
+ private final long mSessionId;
+ private final int mOwnerUid;
+ private final String mOwnerPackageName;
+ private final long mCreationTimeMs;
+
+ // Do not access this directly, instead use getSessionFile().
+ private File mSessionFile;
+
+ @GuardedBy("mRevocableFds")
+ private final ArrayList<RevocableFileDescriptor> mRevocableFds = new ArrayList<>();
+
+ // This will be accessed from only one thread at any point of time, so no need to grab
+ // a lock for this.
+ private byte[] mDataDigest;
+
+ @GuardedBy("mSessionLock")
+ private int mState = STATE_CLOSED;
+
+ @GuardedBy("mSessionLock")
+ private final BlobAccessMode mBlobAccessMode = new BlobAccessMode();
+
+ @GuardedBy("mSessionLock")
+ private IBlobCommitCallback mBlobCommitCallback;
+
+ private BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle,
+ int ownerUid, String ownerPackageName, long creationTimeMs,
+ SessionStateChangeListener listener) {
+ this.mContext = context;
+ this.mBlobHandle = blobHandle;
+ this.mSessionId = sessionId;
+ this.mOwnerUid = ownerUid;
+ this.mOwnerPackageName = ownerPackageName;
+ this.mCreationTimeMs = creationTimeMs;
+ this.mListener = listener;
+ }
+
+ BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle,
+ int ownerUid, String ownerPackageName, SessionStateChangeListener listener) {
+ this(context, sessionId, blobHandle, ownerUid, ownerPackageName,
+ System.currentTimeMillis(), listener);
+ }
+
+ public BlobHandle getBlobHandle() {
+ return mBlobHandle;
+ }
+
+ public long getSessionId() {
+ return mSessionId;
+ }
+
+ public int getOwnerUid() {
+ return mOwnerUid;
+ }
+
+ public String getOwnerPackageName() {
+ return mOwnerPackageName;
+ }
+
+ boolean hasAccess(int callingUid, String callingPackageName) {
+ return mOwnerUid == callingUid && mOwnerPackageName.equals(callingPackageName);
+ }
+
+ void open() {
+ synchronized (mSessionLock) {
+ if (isFinalized()) {
+ throw new IllegalStateException("Not allowed to open session with state: "
+ + stateToString(mState));
+ }
+ mState = STATE_OPENED;
+ }
+ }
+
+ int getState() {
+ synchronized (mSessionLock) {
+ return mState;
+ }
+ }
+
+ void sendCommitCallbackResult(int result) {
+ synchronized (mSessionLock) {
+ try {
+ mBlobCommitCallback.onResult(result);
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Error sending the callback result", e);
+ }
+ mBlobCommitCallback = null;
+ }
+ }
+
+ BlobAccessMode getBlobAccessMode() {
+ synchronized (mSessionLock) {
+ return mBlobAccessMode;
+ }
+ }
+
+ boolean isFinalized() {
+ synchronized (mSessionLock) {
+ return mState == STATE_COMMITTED || mState == STATE_ABANDONED;
+ }
+ }
+
+ boolean isExpired() {
+ final long lastModifiedTimeMs = getSessionFile().lastModified();
+ return hasSessionExpired(lastModifiedTimeMs == 0
+ ? mCreationTimeMs : lastModifiedTimeMs);
+ }
+
+ @Override
+ @NonNull
+ public ParcelFileDescriptor openWrite(@BytesLong long offsetBytes,
+ @BytesLong long lengthBytes) {
+ Preconditions.checkArgumentNonnegative(offsetBytes, "offsetBytes must not be negative");
+
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to write in state: "
+ + stateToString(mState));
+ }
+ }
+
+ FileDescriptor fd = null;
+ try {
+ fd = openWriteInternal(offsetBytes, lengthBytes);
+ final RevocableFileDescriptor revocableFd = new RevocableFileDescriptor(mContext, fd);
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ IoUtils.closeQuietly(fd);
+ throw new IllegalStateException("Not allowed to write in state: "
+ + stateToString(mState));
+ }
+ trackRevocableFdLocked(revocableFd);
+ return revocableFd.getRevocableFileDescriptor();
+ }
+ } catch (IOException e) {
+ IoUtils.closeQuietly(fd);
+ throw ExceptionUtils.wrap(e);
+ }
+ }
+
+ @NonNull
+ private FileDescriptor openWriteInternal(@BytesLong long offsetBytes,
+ @BytesLong long lengthBytes) throws IOException {
+ // TODO: Add limit on active open sessions/writes/reads
+ try {
+ final File sessionFile = getSessionFile();
+ if (sessionFile == null) {
+ throw new IllegalStateException("Couldn't get the file for this session");
+ }
+ final FileDescriptor fd = Os.open(sessionFile.getPath(), O_CREAT | O_RDWR, 0600);
+ if (offsetBytes > 0) {
+ final long curOffset = Os.lseek(fd, offsetBytes, SEEK_SET);
+ if (curOffset != offsetBytes) {
+ throw new IllegalStateException("Failed to seek " + offsetBytes
+ + "; curOffset=" + offsetBytes);
+ }
+ }
+ if (lengthBytes > 0) {
+ mContext.getSystemService(StorageManager.class).allocateBytes(fd, lengthBytes);
+ }
+ return fd;
+ } catch (ErrnoException e) {
+ throw e.rethrowAsIOException();
+ }
+ }
+
+ @Override
+ @NonNull
+ public ParcelFileDescriptor openRead() {
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to read in state: "
+ + stateToString(mState));
+ }
+ if (!BlobStoreConfig.shouldUseRevocableFdForReads()) {
+ try {
+ return new ParcelFileDescriptor(openReadInternal());
+ } catch (IOException e) {
+ throw ExceptionUtils.wrap(e);
+ }
+ }
+ }
+
+ FileDescriptor fd = null;
+ try {
+ fd = openReadInternal();
+ final RevocableFileDescriptor revocableFd = new RevocableFileDescriptor(mContext, fd);
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ IoUtils.closeQuietly(fd);
+ throw new IllegalStateException("Not allowed to read in state: "
+ + stateToString(mState));
+ }
+ trackRevocableFdLocked(revocableFd);
+ return revocableFd.getRevocableFileDescriptor();
+ }
+ } catch (IOException e) {
+ IoUtils.closeQuietly(fd);
+ throw ExceptionUtils.wrap(e);
+ }
+ }
+
+ @NonNull
+ private FileDescriptor openReadInternal() throws IOException {
+ try {
+ final File sessionFile = getSessionFile();
+ if (sessionFile == null) {
+ throw new IllegalStateException("Couldn't get the file for this session");
+ }
+ final FileDescriptor fd = Os.open(sessionFile.getPath(), O_RDONLY, 0);
+ return fd;
+ } catch (ErrnoException e) {
+ throw e.rethrowAsIOException();
+ }
+ }
+
+ @Override
+ @BytesLong
+ public long getSize() {
+ return getSessionFile().length();
+ }
+
+ @Override
+ public void allowPackageAccess(@NonNull String packageName,
+ @NonNull byte[] certificate) {
+ assertCallerIsOwner();
+ Objects.requireNonNull(packageName, "packageName must not be null");
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to change access type in state: "
+ + stateToString(mState));
+ }
+ if (mBlobAccessMode.getNumWhitelistedPackages() >= getMaxPermittedPackages()) {
+ throw new ParcelableException(new LimitExceededException(
+ "Too many packages permitted to access the blob: "
+ + mBlobAccessMode.getNumWhitelistedPackages()));
+ }
+ mBlobAccessMode.allowPackageAccess(packageName, certificate);
+ }
+ }
+
+ @Override
+ public void allowSameSignatureAccess() {
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to change access type in state: "
+ + stateToString(mState));
+ }
+ mBlobAccessMode.allowSameSignatureAccess();
+ }
+ }
+
+ @Override
+ public void allowPublicAccess() {
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to change access type in state: "
+ + stateToString(mState));
+ }
+ mBlobAccessMode.allowPublicAccess();
+ }
+ }
+
+ @Override
+ public boolean isPackageAccessAllowed(@NonNull String packageName,
+ @NonNull byte[] certificate) {
+ assertCallerIsOwner();
+ Objects.requireNonNull(packageName, "packageName must not be null");
+ Preconditions.checkByteArrayNotEmpty(certificate, "certificate");
+
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to get access type in state: "
+ + stateToString(mState));
+ }
+ return mBlobAccessMode.isPackageAccessAllowed(packageName, certificate);
+ }
+ }
+
+ @Override
+ public boolean isSameSignatureAccessAllowed() {
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to get access type in state: "
+ + stateToString(mState));
+ }
+ return mBlobAccessMode.isSameSignatureAccessAllowed();
+ }
+ }
+
+ @Override
+ public boolean isPublicAccessAllowed() {
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ throw new IllegalStateException("Not allowed to get access type in state: "
+ + stateToString(mState));
+ }
+ return mBlobAccessMode.isPublicAccessAllowed();
+ }
+ }
+
+ @Override
+ public void close() {
+ closeSession(STATE_CLOSED, false /* sendCallback */);
+ }
+
+ @Override
+ public void abandon() {
+ closeSession(STATE_ABANDONED, true /* sendCallback */);
+ }
+
+ @Override
+ public void commit(IBlobCommitCallback callback) {
+ synchronized (mSessionLock) {
+ mBlobCommitCallback = callback;
+
+ closeSession(STATE_COMMITTED, true /* sendCallback */);
+ }
+ }
+
+ private void closeSession(int state, boolean sendCallback) {
+ assertCallerIsOwner();
+ synchronized (mSessionLock) {
+ if (mState != STATE_OPENED) {
+ if (state == STATE_CLOSED) {
+ // Just trying to close the session which is already deleted or abandoned,
+ // ignore.
+ return;
+ } else {
+ throw new IllegalStateException("Not allowed to delete or abandon a session"
+ + " with state: " + stateToString(mState));
+ }
+ }
+
+ mState = state;
+ revokeAllFds();
+
+ if (sendCallback) {
+ mListener.onStateChanged(this);
+ }
+ }
+ }
+
+ void computeDigest() {
+ try {
+ Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER,
+ "computeBlobDigest-i" + mSessionId + "-l" + getSessionFile().length());
+ mDataDigest = FileUtils.digest(getSessionFile(), mBlobHandle.algorithm);
+ } catch (IOException | NoSuchAlgorithmException e) {
+ Slog.e(TAG, "Error computing the digest", e);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
+ }
+ }
+
+ void verifyBlobData() {
+ synchronized (mSessionLock) {
+ if (mDataDigest != null && Arrays.equals(mDataDigest, mBlobHandle.digest)) {
+ mState = STATE_VERIFIED_VALID;
+ // Commit callback will be sent once the data is persisted.
+ } else {
+ Slog.d(TAG, "Digest of the data ("
+ + (mDataDigest == null ? "null" : BlobHandle.safeDigest(mDataDigest))
+ + ") didn't match the given BlobHandle.digest ("
+ + BlobHandle.safeDigest(mBlobHandle.digest) + ")");
+ mState = STATE_VERIFIED_INVALID;
+
+ FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED, getOwnerUid(), mSessionId,
+ getSize(), FrameworkStatsLog.BLOB_COMMITTED__RESULT__DIGEST_MISMATCH);
+ sendCommitCallbackResult(COMMIT_RESULT_ERROR);
+ }
+ mListener.onStateChanged(this);
+ }
+ }
+
+ void destroy() {
+ revokeAllFds();
+ getSessionFile().delete();
+ }
+
+ private void revokeAllFds() {
+ synchronized (mRevocableFds) {
+ for (int i = mRevocableFds.size() - 1; i >= 0; --i) {
+ mRevocableFds.get(i).revoke();
+ }
+ mRevocableFds.clear();
+ }
+ }
+
+ @GuardedBy("mSessionLock")
+ private void trackRevocableFdLocked(RevocableFileDescriptor revocableFd) {
+ synchronized (mRevocableFds) {
+ mRevocableFds.add(revocableFd);
+ }
+ revocableFd.addOnCloseListener((e) -> {
+ synchronized (mRevocableFds) {
+ mRevocableFds.remove(revocableFd);
+ }
+ });
+ }
+
+ @Nullable
+ File getSessionFile() {
+ if (mSessionFile == null) {
+ mSessionFile = BlobStoreConfig.prepareBlobFile(mSessionId);
+ }
+ return mSessionFile;
+ }
+
+ @NonNull
+ static String stateToString(int state) {
+ switch (state) {
+ case STATE_OPENED:
+ return "<opened>";
+ case STATE_CLOSED:
+ return "<closed>";
+ case STATE_ABANDONED:
+ return "<abandoned>";
+ case STATE_COMMITTED:
+ return "<committed>";
+ case STATE_VERIFIED_VALID:
+ return "<verified_valid>";
+ case STATE_VERIFIED_INVALID:
+ return "<verified_invalid>";
+ default:
+ Slog.wtf(TAG, "Unknown state: " + state);
+ return "<unknown>";
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "BlobStoreSession {"
+ + "id:" + mSessionId
+ + ",handle:" + mBlobHandle
+ + ",uid:" + mOwnerUid
+ + ",pkg:" + mOwnerPackageName
+ + "}";
+ }
+
+ private void assertCallerIsOwner() {
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid != mOwnerUid) {
+ throw new SecurityException(mOwnerUid + " is not the session owner");
+ }
+ }
+
+ void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) {
+ synchronized (mSessionLock) {
+ fout.println("state: " + stateToString(mState));
+ fout.println("ownerUid: " + mOwnerUid);
+ fout.println("ownerPkg: " + mOwnerPackageName);
+ fout.println("creation time: " + BlobStoreUtils.formatTime(mCreationTimeMs));
+ fout.println("size: " + formatFileSize(mContext, getSize(), FLAG_IEC_UNITS));
+
+ fout.println("blobHandle:");
+ fout.increaseIndent();
+ mBlobHandle.dump(fout, dumpArgs.shouldDumpFull());
+ fout.decreaseIndent();
+
+ fout.println("accessMode:");
+ fout.increaseIndent();
+ mBlobAccessMode.dump(fout);
+ fout.decreaseIndent();
+
+ fout.println("Open fds: #" + mRevocableFds.size());
+ }
+ }
+
+ void writeToXml(@NonNull XmlSerializer out) throws IOException {
+ synchronized (mSessionLock) {
+ XmlUtils.writeLongAttribute(out, ATTR_ID, mSessionId);
+ XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, mOwnerPackageName);
+ XmlUtils.writeIntAttribute(out, ATTR_UID, mOwnerUid);
+ XmlUtils.writeLongAttribute(out, ATTR_CREATION_TIME_MS, mCreationTimeMs);
+
+ out.startTag(null, TAG_BLOB_HANDLE);
+ mBlobHandle.writeToXml(out);
+ out.endTag(null, TAG_BLOB_HANDLE);
+
+ out.startTag(null, TAG_ACCESS_MODE);
+ mBlobAccessMode.writeToXml(out);
+ out.endTag(null, TAG_ACCESS_MODE);
+ }
+ }
+
+ @Nullable
+ static BlobStoreSession createFromXml(@NonNull XmlPullParser in, int version,
+ @NonNull Context context, @NonNull SessionStateChangeListener stateChangeListener)
+ throws IOException, XmlPullParserException {
+ final long sessionId = XmlUtils.readLongAttribute(in, ATTR_ID);
+ final String ownerPackageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE);
+ final int ownerUid = XmlUtils.readIntAttribute(in, ATTR_UID);
+ final long creationTimeMs = version >= XML_VERSION_ADD_SESSION_CREATION_TIME
+ ? XmlUtils.readLongAttribute(in, ATTR_CREATION_TIME_MS)
+ : System.currentTimeMillis();
+
+ final int depth = in.getDepth();
+ BlobHandle blobHandle = null;
+ BlobAccessMode blobAccessMode = null;
+ while (XmlUtils.nextElementWithin(in, depth)) {
+ if (TAG_BLOB_HANDLE.equals(in.getName())) {
+ blobHandle = BlobHandle.createFromXml(in);
+ } else if (TAG_ACCESS_MODE.equals(in.getName())) {
+ blobAccessMode = BlobAccessMode.createFromXml(in);
+ }
+ }
+
+ if (blobHandle == null) {
+ Slog.wtf(TAG, "blobHandle should be available");
+ return null;
+ }
+ if (blobAccessMode == null) {
+ Slog.wtf(TAG, "blobAccessMode should be available");
+ return null;
+ }
+
+ final BlobStoreSession blobStoreSession = new BlobStoreSession(context, sessionId,
+ blobHandle, ownerUid, ownerPackageName, creationTimeMs, stateChangeListener);
+ blobStoreSession.mBlobAccessMode.allow(blobAccessMode);
+ return blobStoreSession;
+ }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreUtils.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreUtils.java
new file mode 100644
index 000000000000..1d07e88773c3
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreUtils.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 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 static com.android.server.blob.BlobStoreConfig.TAG;
+
+import android.annotation.IdRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.text.format.TimeMigrationUtils;
+import android.util.Slog;
+
+class BlobStoreUtils {
+ private static final String DESC_RES_TYPE_STRING = "string";
+
+ @Nullable
+ static Resources getPackageResources(@NonNull Context context,
+ @NonNull String packageName, int userId) {
+ try {
+ return context.getPackageManager()
+ .getResourcesForApplicationAsUser(packageName, userId);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.d(TAG, "Unknown package in user " + userId + ": "
+ + packageName, e);
+ return null;
+ }
+ }
+
+ @IdRes
+ static int getDescriptionResourceId(@NonNull Resources resources,
+ @NonNull String resourceEntryName, @NonNull String packageName) {
+ return resources.getIdentifier(resourceEntryName, DESC_RES_TYPE_STRING, packageName);
+ }
+
+ @IdRes
+ static int getDescriptionResourceId(@NonNull Context context,
+ @NonNull String resourceEntryName, @NonNull String packageName, int userId) {
+ final Resources resources = getPackageResources(context, packageName, userId);
+ return resources == null
+ ? Resources.ID_NULL
+ : getDescriptionResourceId(resources, resourceEntryName, packageName);
+ }
+
+ @NonNull
+ static String formatTime(long timeMs) {
+ return TimeMigrationUtils.formatMillisWithFixedFormat(timeMs);
+ }
+}
diff --git a/apex/extservices/Android.bp b/apex/extservices/Android.bp
new file mode 100644
index 000000000000..0c6c4c23dce1
--- /dev/null
+++ b/apex/extservices/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2020 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.extservices",
+ defaults: ["com.android.extservices-defaults"],
+ manifest: "apex_manifest.json",
+}
+
+apex_defaults {
+ name: "com.android.extservices-defaults",
+ updatable: true,
+ min_sdk_version: "current",
+ key: "com.android.extservices.key",
+ certificate: ":com.android.extservices.certificate",
+ apps: ["ExtServices"],
+}
+
+apex_key {
+ name: "com.android.extservices.key",
+ public_key: "com.android.extservices.avbpubkey",
+ private_key: "com.android.extservices.pem",
+}
+
+android_app_certificate {
+ name: "com.android.extservices.certificate",
+ certificate: "com.android.extservices",
+}
diff --git a/apex/extservices/apex_manifest.json b/apex/extservices/apex_manifest.json
new file mode 100644
index 000000000000..b4acf1283d3e
--- /dev/null
+++ b/apex/extservices/apex_manifest.json
@@ -0,0 +1,4 @@
+{
+ "name": "com.android.extservices",
+ "version": 300000000
+}
diff --git a/apex/extservices/com.android.extservices.avbpubkey b/apex/extservices/com.android.extservices.avbpubkey
new file mode 100644
index 000000000000..f37d3e4a14d4
--- /dev/null
+++ b/apex/extservices/com.android.extservices.avbpubkey
Binary files differ
diff --git a/apex/extservices/com.android.extservices.pem b/apex/extservices/com.android.extservices.pem
new file mode 100644
index 000000000000..7bfbd34ff9b9
--- /dev/null
+++ b/apex/extservices/com.android.extservices.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKAIBAAKCAgEAuYshVDiRkt3tmBhqcWkKOm5GcviKpLbHSPpYQDHGDwS0dqqL
+SqAd1/BgT/bVVtUkAciFApPnXn96WhNYCypptyC5FHCxM21uBCGmow+3WermD++w
+5dQk4QP2ONPIpG+KzOWBl9SiBud4SpOHDyr0JycBsrXS89Tln9kAsTDuDEFfXL/J
+8cX/S3IUwhPV0pAlgUIHdDp0DGFjZaJlEZBZ+HmImriC/AUNUMVb5lfbczXOEZPF
+0A9+JzYschfXUxn8nu1N7RN5GDbq+chszx1FMVhuFUheukkd4dLNSDl0O0RlUnD+
+C/xz1ilDzEVZhnMtMnxS9oJ8bA/HUVMfsFnaQbgGmQ0CcxFxnfbYyGXGG1H+b8vA
+MTVQi5rZXG2p+VgHIAKVrYmpETVnRPgoMqp18KuGtp5SDngi13G3YEzS7iFbqfYh
+6iW2G974nD/Dq0cSire8Oljd9PEaMCMZiP5PTFJp0G/mtw7ROoyZqsSM6rX3XVTo
+Y5dBmBMctSJ8rgDMi0ZNvRH+rq/E5+RT6yMAJ7DDbOJzBnQ3IIoGn8NzUT3P1FCB
+HYEp1U2N7QNirIQMAuVz3IlHae9N1kl3eGAO6f2CjV7vZmFpDeWw+KSYs71mRkOb
+WBgl6D9FFq4u1azrU3AwV0dj3x1eU6yVnKUy1J7ppF/mcR+VzH7ThzTdV7cCAwEA
+AQKCAgEApWFU2Mv/PYhg0bPZlLLKsiA+3RWaBo0AfpTd+oIjBpnr/OWweFjVoPcZ
+8cyShe4/RPOlUxHgJcO8m/MoA/PO/LLHJWf5GlzMthQEgs1sYVJVtBiydXitUn+E
+hUyIR8FAV7et1lZqAXtqJhbvSF7B9u/2vIMCv+GgtuTmkAmL9RKD3Jj6eG1CS84o
+oICrkx52v4rKOBgt/icEQMAKFCi1eRti3n3eCqK6JqdzbZIcAcoQnmw34mccy/im
+jx+fBuxf1oywa8NyqVmyAehazBVL6lrm7ENwY9zuLK4H2fuUFYu2QFCEsMxZt6da
+TgX2cTfSLnDQRfcyzeMWhu9vjHHabjpLNjiCKhIhGyO0rO1rtea8ajZHgM/2sxXq
+6gLynW0dlatlxmjANlN9WQPGNdzvcIFJ0TLnI4mlJnWpqCsN9iW1d4ey13WiZUVR
+DgtnR60zao+LRCCM4D3cuVLq0DjL2BlHGXnOPK/LpQG1LbI1TroZpgSEHSZlQRzT
+ql9txgNqTHxijXuPL2VhhwhW7cqDoO8sLwV3BqDMIH56U0cbUBiSA/G9fKeI/DEG
+i7LcrMgrBk+xnuAWoFHuzfBMAdD9i3kYyk+41tOmcza2TNJgxadVYp5woHFvYvS/
+GKaNiRz0XmcijO5Ir0yxgCq21BdkWzo5zVrTFABiKeR7YXiee8kCggEBAOeULWgR
+spolJJrACWJspRvKb9FGnbGiYOnCGJoAc751kuXmNxoyWnEwgcjrSEoayNPUfOtz
+IgA+twqjgl0Zec2XFPfUcgWUBrrvvUEV4NIH5ibaR7ezHGeovCWs9XoDyzHHvhDr
+c6T5kXFZ60rS5h6LGUnE1hkHFJoHuTIBbn9j7eIbri8S71i7HWQ04s4KuQ+Bwbxm
+UnkEhbc+zMWHXfXy7rx4/eEZcZwtEybIORcHXYNPGeqMfOlcEMHpKEOi+NvDA6cp
+vTaTSwJ6ZBgYh7Tw3bNgRxSknaIhcGwMD0ojStjC5xzXT1Zr2Z3GXwYvOGcq3MeZ
+z+V2cx5xuwyp7R0CggEBAM0cKKNZEZwi/1zBPUDMFB4iJoX12BxQX6e5wdlHGXgF
+XeZwCnaIxOxMDxH79M5Svmpdu/jkUijI/pRvcE1iohFyIBvTUSDmlAoy4keXqMEQ
+M2hA+TwVA3JLmMcV8HKy/MFlwwKJB1JDcoxGjnXsM5UjVTD2jilO7vlJZs3+0ws0
+R7qzRT3ED25QTpZyDYcKE2otc5bzIZG3yAaJtWd3NugWsKpxDgr2RFUGJiHBq72n
+48FkSjfgaDTn83zYcPvS0Uykb2ho8G/N+EurstL41n3nQo0I7FLbyptOopDDwsSp
+Ndejn08NVAQ+xFAafOyqHkA3Ytpl0QCZDpMBuLdvw+MCggEAOVMt1kgjPRMat4/4
+ArxANtvqyBRB7vnyIYthiaW5ARmbrntJgpuaVdCbIABWGbn9oqpD7gjHDuZ3axPE
+roUi6KiQkTSusQDOlbHI2Haw+2znJRD9ldSpoGNdh7oD3htYTk9Sll+ideEthrCq
+lRAV1NO8A83M7c8Z43Mr/dvq3XAAL+uIN7DpPL687NRGnJh87QDC039ExR5Ad3b9
+O5xhvwNO46rTtcgVnoJt7ji8IR46oMmQ8cWrGh0nLMkppWyPS98/ZT7ozryxYcCo
+TGquFTVWvBOGJO8G8l5ytNxbYI/R9Exy52nJAuyZpvu3BBHmVWt/0Y0asIOcxZmD
+owPhZQKCAQAfWAFBzReq05JQe1s/7q/YVwGqEQKgeQvVFsbvzDSxKajK0S5YJNhq
+/8iByA4GBZEBsidKhqGjh+uXhVwVB1Ca9+S+O9G3BGV1FYeMxzlLn40rjlpH+zIW
+okTLj6e5724+o61kUspioNn9Y77beGf9j3OyUsswttZAFB54tktL+AZKGqEnKjHt
+eqo3xWAZ1clXvXBfjfIAUaRok1y8XfRvDSCcO0CZHj8c+x6SpAT5q5FbeVb6KPnj
+s9p6ppzFbtb7Llm0C+1KOKCL98YRBWPJw7Bg2w86LkpM53xiQPgfk3gd5uwuaWwA
+ZhMb5qBWjjynNY+OrmZ8/+bBQk8XASZfAoIBAFkHOnZOD1JJQ0QvaJ9tuCgHi216
+I8QPMMTdm3ZEDHSYMNwl7ayeseBcmB2zaqBKYz75qcU0SK4lnZkR2wIpbsHZNSVM
+J0WpN6r9G4JdnVi11J04RsfSMjCUr/PTVMmPvw8xPHrCxkJmB+d56olSE80I1Jrx
+djCv1LtSsT10W7FIcY82/cOi4xxGLOA70lDCf+szofQgVP8WvuOA1YaFw98ca8zc
+A401CyNexk24/c3d6C19YW/MppdE0uGMxL/oHsPgwkZAf6LmvF/UF71PsBUEniLc
+YFaJl3wn1cPfBBo9L4sZzyP2qokL8YHdg+wW7b4IOsYwbeqceBvqPtcUUPs=
+-----END RSA PRIVATE KEY-----
diff --git a/apex/extservices/com.android.extservices.pk8 b/apex/extservices/com.android.extservices.pk8
new file mode 100644
index 000000000000..59585a212592
--- /dev/null
+++ b/apex/extservices/com.android.extservices.pk8
Binary files differ
diff --git a/apex/extservices/com.android.extservices.x509.pem b/apex/extservices/com.android.extservices.x509.pem
new file mode 100644
index 000000000000..e0343b81d279
--- /dev/null
+++ b/apex/extservices/com.android.extservices.x509.pem
@@ -0,0 +1,36 @@
+-----BEGIN CERTIFICATE-----
+MIIGLTCCBBWgAwIBAgIUdqdMmx/5OsCP3Ew3/hcr7+1ACHEwDQYJKoZIhvcNAQEL
+BQAwgaQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy
+b2lkMSAwHgYDVQQDDBdjb20uYW5kcm9pZC5leHRzZXJ2aWNlczEiMCAGCSqGSIb3
+DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAgFw0yMDAxMTcxMDIxMzZaGA80NzU3
+MTIxMzEwMjEzNlowgaQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh
+MRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYD
+VQQLDAdBbmRyb2lkMSAwHgYDVQQDDBdjb20uYW5kcm9pZC5leHRzZXJ2aWNlczEi
+MCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCAiIwDQYJKoZIhvcN
+AQEBBQADggIPADCCAgoCggIBANKaSeLGaFRRt779vAtTfG3t2aQZrWOByUYc7yUN
+RdmJqWxU47OL5urYmanWPbz2f972Q9oi8x+8y4ny9SEY3wg0pUbzvKNTXpkxWyG1
+HE2C2zTfzuDDLpDIf2usWynt1wLVhpYC3k+7Yv2vOIK5dKkezh6PfdKmsbDae5DE
+d22tTSYZ5KwNpIWrgQle26cRG5sqhAFdkpgGMF00Huz06cjUoTjs2sNSlXTRBOTP
+CCy8UoRjBivQZkwHbddfsn+Z22ARPG8JDg/n4mEi8C0T6bJeQeirSPkBCkD6Djgq
+7RddJ2eLYZII8l8r6A6x+6cnTkXHaV5g3LUwPvi8XEn9IUuT9WJNRje/vfYLycTQ
+kP415CZMxDvsi1Ul4YsbL3enE89ryGMTpVZPogch/36DG5Sye28yISItNUy3urJa
+OXbg7mh+MwPd4bQaW4CJk+AUweKaF4aV0SZFT+nCewL4xLdGdy889KazlW98NqtK
+hOSxIg1jHkZq48ajuq2A+ns1yDKt1l0f9IYCz3mz/IXInokbkjPvHahJTJ+OMHXO
+THD8e5gBzcK841jJk+H3EsIYOHsp66uy2IgEHN+9pAS6vI0xfrXOYuKzuSL3oxcV
+FlVTimt4xokMMerdcW4KD+MC5NFEip4DUS4JKCyG0wRI3ffEs9Zcpxi3QSibrjLW
+rz+hAgMBAAGjUzBRMB0GA1UdDgQWBBTP2AhZzEUUgtAFlkaMaq+RvY06fDAfBgNV
+HSMEGDAWgBTP2AhZzEUUgtAFlkaMaq+RvY06fDAPBgNVHRMBAf8EBTADAQH/MA0G
+CSqGSIb3DQEBCwUAA4ICAQCbwtfo37j62Sudmt32PCfRN/r5ZNDNNA2JhR8uDUmX
+xXfF5YfDvSKsNLiQKcDagu6a+0C+QnzXHXCBlXZFrTJ8NAVMlmqdHGwoFoYMfJZH
+R1lCTidyFMoMLJ8GRGPJjzDkKnOeAqKMCtKvXoH2r12+JB2/ov4ooLREu/wPkEXT
+OymkyWNP5XLQTKWqfEQyXXFpuwZ+m35Wkr0Fm92mZeJpVeIZPK7M7aK3zyoj7XJP
+YLMsR/AQs8OULdpfNMddAuN3ndlYu03LZlsF6LG5bduaDDcESJ5hdJrgBa/NBKRU
+IbS+q/6WAjYKMNRT/fPGew4wUzlWKi1Ihdk79oaqKKijE1b2JSJD1/SEYiBf+JPE
+bXobUrMbBwFpdhT+YLMF9FsuPQKsUIONaWiO4QcQoY/rQwGxPP6fV8ZbBrUWJewj
+MpSdU9foZNa/TmOAgfS/JxH+nXnG4+H1m8mdNBsxvsYmF2ZuGb/jdEeA2cuHIJDZ
+FJeWwCFxzlCGZJaUsxsnZByADBuufUVaO/9gGs0YQC/JP1i9hK4DyZdKwZpXdLi2
+Nw27Qma4WEIZnMb6Rgk1nTV+7ALcOSIhGgFOOeDTuCGfnEcz2coai5fbD/K6Q7Xu
+IRNyxHQjheZPdei2x912Ex/KqKGfaFaZJxrvCSKdhzxcTFIsO4JuZs+SDpRTKcI7
+Cw==
+-----END CERTIFICATE-----
diff --git a/apex/extservices/testing/Android.bp b/apex/extservices/testing/Android.bp
new file mode 100644
index 000000000000..88a47246c824
--- /dev/null
+++ b/apex/extservices/testing/Android.bp
@@ -0,0 +1,25 @@
+// Copyright (C) 2020 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_test {
+ name: "test_com.android.extservices",
+ visibility: [
+ "//system/apex/tests",
+ ],
+ defaults: ["com.android.extservices-defaults"],
+ manifest: "test_manifest.json",
+ file_contexts: ":com.android.extservices-file_contexts",
+ // Test APEX, should never be installed
+ installable: false,
+}
diff --git a/apex/extservices/testing/test_manifest.json b/apex/extservices/testing/test_manifest.json
new file mode 100644
index 000000000000..23a50e37bdd3
--- /dev/null
+++ b/apex/extservices/testing/test_manifest.json
@@ -0,0 +1,4 @@
+{
+ "name": "com.android.extservices",
+ "version": 2147483647
+}
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..134ff3da4507
--- /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 `service-jobscheduler.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/service-jobscheduler.jar`
+
+ `SYSTEMSERVERCLASSPATH` is generated from `PRODUCT_SYSTEM_SERVER_JARS`.
+
+- JS framework side classes are put in `framework-jobscheduler.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..ec074262fb13
--- /dev/null
+++ b/apex/jobscheduler/framework/Android.bp
@@ -0,0 +1,30 @@
+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: "framework-jobscheduler",
+ installable: false,
+ compile_dex: true,
+ sdk_version: "core_platform",
+ srcs: [
+ ":framework-jobscheduler-sources",
+ ],
+ aidl: {
+ export_include_dirs: [
+ "java",
+ ],
+ },
+ libs: [
+ "framework-minus-apex",
+ "unsupportedappusage",
+ ],
+}
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..9f98f8efc774
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -0,0 +1,1596 @@
+/*
+ * 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.compat.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}.
+ * The goal here is to provide the scheduler with high-level semantics about the work you want to
+ * accomplish.
+ * <p> Prior to Android version {@link Build.VERSION_CODES#Q}, you had to specify at least one
+ * constraint on the JobInfo object that you are creating. Otherwise, the builder would throw an
+ * exception when building. From Android version {@link Build.VERSION_CODES#Q} and onwards, it is
+ * valid to schedule jobs with no constraints.
+ */
+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 than 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;
+ }
+
+ @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 (!BaseBundle.kindofEquals(extras, j.extras)) {
+ return false;
+ }
+ // XXX won't be correct if one is parcelled and the other not.
+ if (!BaseBundle.kindofEquals(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 = Objects.requireNonNull(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 lose 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..62c90dfa8a86
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -0,0 +1,397 @@
+/*
+ * 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.compat.annotation.UnsupportedAppUsage;
+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.
+ /**
+ * The job is in the {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
+ * bucket.
+ *
+ * @hide
+ */
+ public static final int REASON_RESTRICTED_BUCKET =
+ JobProtoEnums.STOP_REASON_RESTRICTED_BUCKET; // 6.
+
+ /**
+ * 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,
+ REASON_RESTRICTED_BUCKET,
+ };
+
+ /**
+ * @hide
+ */
+ // TODO(142420609): make it @SystemApi for mainline
+ @NonNull
+ public static String getReasonCodeDescription(int reasonCode) {
+ switch (reasonCode) {
+ 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";
+ case REASON_RESTRICTED_BUCKET: return "restricted_bucket";
+ default: return "unknown:" + reasonCode;
+ }
+ }
+
+ /** @hide */
+ // @SystemApi TODO make it a system api for mainline
+ @NonNull
+ public static int[] getJobStopReasonCodes() {
+ return JOB_STOP_REASON_CODES;
+ }
+
+ @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..42725c51fd87
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -0,0 +1,209 @@
+/*
+ * 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#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)}.
+ *
+ * <p class="caution"><strong>Note:</strong> Beginning with API 30
+ * ({@link android.os.Build.VERSION_CODES#R}), JobScheduler will throttle runaway applications.
+ * Calling {@link #schedule(JobInfo)} and other such methods with very high frequency can have a
+ * high cost and so, to make sure the system doesn't get overwhelmed, JobScheduler will begin
+ * to throttle apps, regardless of target SDK version.
+ */
+@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)} if a job wasn't scheduled successfully. Scheduling
+ * can fail for a variety of reasons, including, but not limited to:
+ * <ul>
+ * <li>an invalid parameter was supplied (eg. the run-time for your job is too short, or the
+ * system can't resolve the requisite {@link JobService} in your package)</li>
+ * <li>the app has too many jobs scheduled</li>
+ * <li>the app has tried to schedule too many jobs in a short amount of time</li>
+ * </ul>
+ * Attempting to schedule the job again immediately after receiving this result will not
+ * guarantee a successful schedule.
+ */
+ 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.
+ *
+ * <p class="caution"><strong>Note:</strong> Scheduling a job can have a high cost, even if it's
+ * rescheduling the same job and the job didn't execute, especially on platform versions before
+ * version {@link android.os.Build.VERSION_CODES#Q}. As such, the system may throttle calls to
+ * this API if calls are made too frequently in a short amount of time.
+ *
+ * @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#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..0c4fcb4ec1b0
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java
@@ -0,0 +1,56 @@
+/*
+ * 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.annotation.SystemApi;
+import android.app.JobSchedulerImpl;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+import android.os.DeviceIdleManager;
+import android.os.IDeviceIdleController;
+import android.os.PowerWhitelistManager;
+
+/**
+ * Class holding initialization code for the job scheduler module.
+ *
+ * @hide
+ */
+@SystemApi
+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).
+ */
+ 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)));
+ SystemServiceRegistry.registerContextAwareService(
+ Context.POWER_WHITELIST_MANAGER, PowerWhitelistManager.class,
+ PowerWhitelistManager::new);
+ }
+}
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..0c45cbf6dc11
--- /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.compat.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..f863718d6ce7
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.Context;
+
+/**
+ * Access to the service that keeps track of device idleness and drives low power mode based on
+ * that.
+ *
+ * @hide
+ */
+@TestApi
+@SystemService(Context.DEVICE_IDLE_CONTROLLER)
+public class DeviceIdleManager {
+ private final Context mContext;
+ private final IDeviceIdleController mService;
+
+ /**
+ * @hide
+ */
+ public DeviceIdleManager(@NonNull Context context, @NonNull IDeviceIdleController service) {
+ mContext = context;
+ mService = service;
+ }
+
+ IDeviceIdleController getService() {
+ return mService;
+ }
+
+ /**
+ * @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) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @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) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
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..643d47ca5c6a
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl
@@ -0,0 +1,52 @@
+/**
+ * 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);
+ long whitelistAppTemporarily(String name, int userId, String reason);
+ void exitIdle(String reason);
+ int setPreIdleTimeoutMode(int Mode);
+ void resetPreIdleTimeoutMode();
+}
diff --git a/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java b/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java
new file mode 100644
index 000000000000..d54d857ffcd6
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java
@@ -0,0 +1,189 @@
+/*
+ * 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.os;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.Context;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Interface to access and modify the power save whitelist.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+@SystemService(Context.POWER_WHITELIST_MANAGER)
+public class PowerWhitelistManager {
+ private final Context mContext;
+ // Proxy to DeviceIdleController for now
+ // TODO: migrate to PowerWhitelistController
+ private final IDeviceIdleController mService;
+
+ /**
+ * Indicates that an unforeseen event has occurred and the app should be whitelisted to handle
+ * it.
+ */
+ public static final int EVENT_UNSPECIFIED = 0;
+
+ /**
+ * Indicates that an SMS event has occurred and the app should be whitelisted to handle it.
+ */
+ public static final int EVENT_SMS = 1;
+
+ /**
+ * Indicates that an MMS event has occurred and the app should be whitelisted to handle it.
+ */
+ public static final int EVENT_MMS = 2;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"EVENT_"}, value = {
+ EVENT_UNSPECIFIED,
+ EVENT_SMS,
+ EVENT_MMS,
+ })
+ public @interface WhitelistEvent {
+ }
+
+ /**
+ * @hide
+ */
+ public PowerWhitelistManager(@NonNull Context context) {
+ mContext = context;
+ mService = context.getSystemService(DeviceIdleManager.class).getService();
+ }
+
+ /**
+ * Add the specified package to the permanent power save whitelist.
+ */
+ @RequiresPermission(android.Manifest.permission.DEVICE_POWER)
+ public void addToWhitelist(@NonNull String packageName) {
+ addToWhitelist(Collections.singletonList(packageName));
+ }
+
+ /**
+ * Add the specified packages to the permanent power save whitelist.
+ */
+ @RequiresPermission(android.Manifest.permission.DEVICE_POWER)
+ public void addToWhitelist(@NonNull List<String> packageNames) {
+ try {
+ mService.addPowerSaveWhitelistApps(packageNames);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get a list of app IDs of app that are whitelisted. This does not include temporarily
+ * whitelisted apps.
+ *
+ * @param includingIdle Set to true if the app should be whitelisted from device idle as well
+ * as other power save restrictions
+ * @hide
+ */
+ @NonNull
+ public int[] getWhitelistedAppIds(boolean includingIdle) {
+ try {
+ if (includingIdle) {
+ return mService.getAppIdWhitelist();
+ } else {
+ return mService.getAppIdWhitelistExceptIdle();
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns true if the app is whitelisted from power save restrictions. This does not include
+ * temporarily whitelisted apps.
+ *
+ * @param includingIdle Set to true if the app should be whitelisted from device
+ * idle as well as other power save restrictions
+ * @hide
+ */
+ public boolean isWhitelisted(@NonNull String packageName, boolean includingIdle) {
+ try {
+ if (includingIdle) {
+ return mService.isPowerSaveWhitelistApp(packageName);
+ } else {
+ return mService.isPowerSaveWhitelistExceptIdleApp(packageName);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Add an app to the temporary whitelist for a short amount of time.
+ *
+ * @param packageName The package to add to the temp whitelist
+ * @param durationMs How long to keep the app on the temp whitelist for (in milliseconds)
+ */
+ @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST)
+ public void whitelistAppTemporarily(@NonNull String packageName, long durationMs) {
+ String reason = "from:" + UserHandle.formatUid(Binder.getCallingUid());
+ try {
+ mService.addPowerSaveTempWhitelistApp(packageName, durationMs, mContext.getUserId(),
+ reason);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Add an app to the temporary whitelist for a short amount of time for a specific reason.
+ *
+ * @param packageName The package to add to the temp whitelist
+ * @param event The reason to add the app to the temp whitelist
+ * @param reason A human-readable reason explaining why the app is temp whitelisted. Only used
+ * for logging purposes
+ * @return The duration (in milliseconds) that the app is whitelisted for
+ */
+ @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST)
+ public long whitelistAppTemporarilyForEvent(@NonNull String packageName,
+ @WhitelistEvent int event, @NonNull String reason) {
+ try {
+ switch (event) {
+ case EVENT_MMS:
+ return mService.addPowerSaveTempWhitelistAppForMms(
+ packageName, mContext.getUserId(), reason);
+ case EVENT_SMS:
+ return mService.addPowerSaveTempWhitelistAppForSms(
+ packageName, mContext.getUserId(), reason);
+ case EVENT_UNSPECIFIED:
+ default:
+ return mService.whitelistAppTemporarily(
+ packageName, mContext.getUserId(), reason);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
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..7833a037463c
--- /dev/null
+++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
@@ -0,0 +1,124 @@
+/*
+ * 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.annotation.NonNull;
+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();
+
+ /** Returns the package responsible for backing up media on the device. */
+ @NonNull
+ String getMediaBackupPackage();
+
+ /**
+ * 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 dumpDebug(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..e15f0f37fc62
--- /dev/null
+++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
@@ -0,0 +1,168 @@
+package com.android.server.usage;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.usage.AppStandbyInfo;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager.StandbyBuckets;
+import android.app.usage.UsageStatsManager.SystemForcedReasons;
+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);
+
+ /**
+ * Callback to inform listeners that the parole state has changed. This means apps are
+ * allowed to do work even if they're idle or in a low bucket.
+ */
+ public void onParoleStateChanged(boolean isParoleOn) {
+ // No-op by default
+ }
+
+ /**
+ * 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, 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.
+ * This happens if the device is plugged in or otherwise temporarily allowed to make exceptions.
+ * Called by interface impls.
+ */
+ boolean isAppIdleFiltered(String packageName, int appId, int userId,
+ long elapsedRealtime);
+
+ /**
+ * @return true if currently app idle parole mode is on.
+ */
+ boolean isInParole();
+
+ 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);
+
+ /**
+ * Changes an app's standby bucket to the provided value. The caller can only set the standby
+ * bucket for a different app than itself.
+ * If attempting to automatically place an app in the RESTRICTED bucket, use
+ * {@link #restrictApp(String, int, int)} instead.
+ */
+ void setAppStandbyBucket(@NonNull String packageName, int bucket, int userId, int callingUid,
+ int callingPid);
+
+ /**
+ * Changes the app standby bucket for multiple apps at once.
+ */
+ void setAppStandbyBuckets(@NonNull List<AppStandbyInfo> appBuckets, int userId, int callingUid,
+ int callingPid);
+
+ /**
+ * Put the specified app in the
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
+ * bucket. If it has been used by the user recently, the restriction will delayed until an
+ * appropriate time.
+ *
+ * @param restrictReason The restrictReason for restricting the app. Should be one of the
+ * UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_* reasons.
+ */
+ void restrictApp(@NonNull String packageName, int userId,
+ @SystemForcedReasons int restrictReason);
+
+ void addActiveDeviceAdmin(String adminPkg, int userId);
+
+ void setActiveAdminApps(Set<String> adminPkgs, int userId);
+
+ void onAdminDataAvailable();
+
+ void clearCarrierPrivilegedApps();
+
+ void flushToDisk();
+
+ 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 dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs);
+
+ 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..69a9fd844729
--- /dev/null
+++ b/apex/jobscheduler/service/Android.bp
@@ -0,0 +1,16 @@
+// Job Scheduler Service jar, which will eventually be put in the jobscheduler mainline apex.
+// service-jobscheduler needs to be added to PRODUCT_SYSTEM_SERVER_JARS.
+java_library {
+ name: "service-jobscheduler",
+ installable: true,
+
+ srcs: [
+ "java/**/*.java",
+ ],
+
+ libs: [
+ "app-compat-annotations",
+ "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..316306df4f48
--- /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);
+ }
+ status = getStationaryStatus();
+ mRunningStats.reset();
+ 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..ac58f3d6a94d
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -0,0 +1,4638 @@
+/*
+ * 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");
+ }
+ }
+ };
+
+ /** AlarmListener to start monitoring motion if there are registered stationary listeners. */
+ private final AlarmManager.OnAlarmListener mMotionRegistrationAlarmListener = () -> {
+ synchronized (DeviceIdleController.this) {
+ if (mStationaryListeners.size() > 0) {
+ startMonitoringMotionLocked();
+ }
+ }
+ };
+
+ 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) {
+ // One_shot sensors (which call onTrigger) are unregistered when onTrigger is called
+ active = false;
+ motionLocked();
+ }
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ synchronized (DeviceIdleController.this) {
+ // Since one_shot sensors are unregistered when onTrigger is called, unregister
+ // listeners here so that the MotionListener is in a consistent state when it calls
+ // out to motionLocked.
+ mSensorManager.unregisterListener(this, mMotionSensor);
+ active = false;
+ motionLocked();
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+
+ public boolean registerLocked() {
+ boolean success;
+ if (mMotionSensor.getReportingMode() == Sensor.REPORTING_MODE_ONE_SHOT) {
+ success = mSensorManager.requestTriggerSensor(mMotionListener, mMotionSensor);
+ } else {
+ success = mSensorManager.registerListener(
+ mMotionListener, mMotionSensor, SensorManager.SENSOR_DELAY_NORMAL);
+ }
+ if (success) {
+ active = true;
+ 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 ? 30 * 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 long whitelistAppTemporarily(String packageName, int userId, String reason)
+ throws RemoteException {
+ // At least 10 seconds.
+ long duration = Math.max(10_000L, mConstants.MAX_TEMP_APP_WHITELIST_DURATION / 2);
+ addPowerSaveTempWhitelistAppChecked(packageName, duration, userId, reason);
+ return duration;
+ }
+
+ @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());
+ }
+
+ Sensor getMotionSensor() {
+ final SensorManager sensorManager = getSensorManager();
+ Sensor motionSensor = null;
+ int sigMotionSensorId = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_autoPowerModeAnyMotionSensor);
+ if (sigMotionSensorId > 0) {
+ motionSensor = sensorManager.getDefaultSensor(sigMotionSensorId, true);
+ }
+ if (motionSensor == null && mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_autoPowerModePreferWristTilt)) {
+ motionSensor = sensorManager.getDefaultSensor(
+ Sensor.TYPE_WRIST_TILT_GESTURE, true);
+ }
+ if (motionSensor == null) {
+ // As a last ditch, fall back to SMD.
+ motionSensor = sensorManager.getDefaultSensor(
+ Sensor.TYPE_SIGNIFICANT_MOTION, true);
+ }
+ return motionSensor;
+ }
+
+ 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) {
+ mMotionSensor = mInjector.getMotionSensor();
+ }
+
+ 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();
+ // We need to re-register the motion listener, but we don't want the sensors to be
+ // constantly active or to churn the CPU by registering too early, register after some
+ // delay.
+ scheduleMotionRegistrationAlarmLocked();
+ }
+ 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 && mStationaryListeners.size() == 0) {
+ if (mMotionListener.active) {
+ mMotionListener.unregisterLocked();
+ cancelMotionTimeoutAlarmLocked();
+ }
+ cancelMotionRegistrationAlarmLocked();
+ }
+ }
+
+ 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);
+ }
+
+ private void cancelMotionRegistrationAlarmLocked() {
+ mAlarmManager.cancel(mMotionRegistrationAlarmListener);
+ }
+
+ 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 scheduleMotionRegistrationAlarmLocked() {
+ if (DEBUG) Slog.d(TAG, "scheduleMotionRegistrationAlarmLocked");
+ long nextMotionRegistrationAlarmTime =
+ mInjector.getElapsedRealtime() + mConstants.MOTION_INACTIVE_TIMEOUT / 2;
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextMotionRegistrationAlarmTime,
+ "DeviceIdleController.motion_registration", mMotionRegistrationAlarmListener,
+ 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/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/TEST_MAPPING
new file mode 100644
index 000000000000..d99830dc47c9
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/TEST_MAPPING
@@ -0,0 +1,23 @@
+{
+ "presubmit": [
+ {
+ "name": "FrameworksMockingServicesTests",
+ "file_patterns": [
+ "DeviceIdleController\\.java"
+ ],
+ "options": [
+ {"include-filter": "com.android.server.DeviceIdleControllerTest"},
+ {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"}
+ ]
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "FrameworksMockingServicesTests",
+ "options": [
+ {"include-filter": "com.android.server"}
+ ]
+ }
+ ]
+} \ No newline at end of file
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/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/deviceidle/TEST_MAPPING
new file mode 100644
index 000000000000..b76c582cf287
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/deviceidle/TEST_MAPPING
@@ -0,0 +1,20 @@
+{
+ "presubmit": [
+ {
+ "name": "FrameworksMockingServicesTests",
+ "options": [
+ {"include-filter": "com.android.server.DeviceIdleControllerTest"},
+ {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"}
+ ]
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "FrameworksMockingServicesTests",
+ "options": [
+ {"include-filter": "com.android.server"}
+ ]
+ }
+ ]
+} \ No newline at end of file
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..435384dd2319
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -0,0 +1,726 @@
+/*
+ * 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;
+ }
+
+ if (minPriorityForPreemption > jobPriority) {
+ // Step down the preemption threshold - wind up replacing
+ // the lowest-priority running job
+ minPriorityForPreemption = jobPriority;
+ 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..d05034797f3d
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java
@@ -0,0 +1,655 @@
+/*
+ * 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
+ .getReasonCodeDescription(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.getReasonCodeDescription(
+ (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..871e40fc9dfe
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -0,0 +1,3492 @@
+/*
+ * 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.ApplicationInfo;
+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.LimitExceededException;
+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.ArrayMap;
+import android.util.KeyValueListParser;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.R;
+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.FrameworkStatsLog;
+import com.android.internal.util.IndentingPrintWriter;
+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.RestrictingController;
+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 com.android.server.utils.quota.Categorizer;
+import com.android.server.utils.quota.Category;
+import com.android.server.utils.quota.CountQuotaTracker;
+
+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.Objects;
+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;
+ /**
+ * List of controllers that will apply to all jobs in the RESTRICTED bucket. This is a subset of
+ * {@link #mControllers}.
+ */
+ private final List<RestrictingController> mRestrictiveControllers;
+ /** 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;
+
+ @NonNull
+ private final String mSystemGalleryPackage;
+
+ private final CountQuotaTracker mQuotaTracker;
+ private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()";
+ private static final String QUOTA_TRACKER_SCHEDULE_LOGGED =
+ ".schedulePersisted out-of-quota logged";
+ private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED = new Category(
+ ".schedulePersisted()");
+ private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED = new Category(
+ ".schedulePersisted out-of-quota logged");
+ private static final Categorizer QUOTA_CATEGORIZER = (userId, packageName, tag) -> {
+ if (QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG.equals(tag)) {
+ return QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED;
+ }
+ return QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED;
+ };
+
+ /**
+ * 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;
+ @VisibleForTesting
+ AppStateTracker mAppStateTracker;
+ final UsageStatsManagerInternal mUsageStats;
+ private final AppStandbyInternal mAppStandbyInternal;
+
+ /**
+ * 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();
+
+ /**
+ * Cache of debuggable app status.
+ */
+ final ArrayMap<String, Boolean> mDebuggableApps = new ArrayMap<>();
+
+ /**
+ * 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;
+ // Putting RESTRICTED_INDEX after NEVER_INDEX to make it easier for proto dumping
+ // (ScheduledJobStateChanged and JobStatusDumpProto).
+ public static final int RESTRICTED_INDEX = 5;
+
+ // -- 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();
+ }
+ updateQuotaTracker();
+ } catch (IllegalArgumentException e) {
+ // Failed to parse the settings string, log this and move on
+ // with defaults.
+ Slog.e(TAG, "Bad jobscheduler settings", e);
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void updateQuotaTracker() {
+ mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS);
+ mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED,
+ mConstants.API_QUOTA_SCHEDULE_COUNT,
+ mConstants.API_QUOTA_SCHEDULE_WINDOW_MS);
+ }
+
+ 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.
+ // TODO(124466289): remove deprecated flags when we migrate to DeviceConfig
+ private static final String DEPRECATED_KEY_MIN_IDLE_COUNT = "min_idle_count";
+ private static final String DEPRECATED_KEY_MIN_CHARGING_COUNT = "min_charging_count";
+ private static final String DEPRECATED_KEY_MIN_BATTERY_NOT_LOW_COUNT =
+ "min_battery_not_low_count";
+ private static final String DEPRECATED_KEY_MIN_STORAGE_NOT_LOW_COUNT =
+ "min_storage_not_low_count";
+ private static final String DEPRECATED_KEY_MIN_CONNECTIVITY_COUNT =
+ "min_connectivity_count";
+ private static final String DEPRECATED_KEY_MIN_CONTENT_COUNT = "min_content_count";
+ private static final String DEPRECATED_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 DEPRECATED_KEY_MAX_STANDARD_RESCHEDULE_COUNT
+ = "max_standard_reschedule_count";
+ private static final String DEPRECATED_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 String KEY_ENABLE_API_QUOTAS = "enable_api_quotas";
+ private static final String KEY_API_QUOTA_SCHEDULE_COUNT = "aq_schedule_count";
+ private static final String KEY_API_QUOTA_SCHEDULE_WINDOW_MS = "aq_schedule_window_ms";
+ private static final String KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION =
+ "aq_schedule_throw_exception";
+ private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT =
+ "aq_schedule_return_failure";
+
+ 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 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;
+ private static final boolean DEFAULT_ENABLE_API_QUOTAS = true;
+ private static final int DEFAULT_API_QUOTA_SCHEDULE_COUNT = 250;
+ private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS;
+ private static final boolean DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION = true;
+ private static final boolean DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false;
+
+ /**
+ * 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 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;
+
+ /**
+ * Whether to enable quota limits on APIs.
+ */
+ public boolean ENABLE_API_QUOTAS = DEFAULT_ENABLE_API_QUOTAS;
+ /**
+ * The maximum number of schedule() calls an app can make in a set amount of time.
+ */
+ public int API_QUOTA_SCHEDULE_COUNT = DEFAULT_API_QUOTA_SCHEDULE_COUNT;
+ /**
+ * The time window that {@link #API_QUOTA_SCHEDULE_COUNT} should be evaluated over.
+ */
+ public long API_QUOTA_SCHEDULE_WINDOW_MS = DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS;
+ /**
+ * Whether to throw an exception when an app hits its schedule quota limit.
+ */
+ public boolean API_QUOTA_SCHEDULE_THROW_EXCEPTION =
+ DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION;
+ /**
+ * Whether or not to return a failure result when an app hits its schedule quota limit.
+ */
+ public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT =
+ DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT;
+
+ 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_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);
+
+ 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);
+
+ ENABLE_API_QUOTAS = mParser.getBoolean(KEY_ENABLE_API_QUOTAS,
+ DEFAULT_ENABLE_API_QUOTAS);
+ // Set a minimum value on the quota limit so it's not so low that it interferes with
+ // legitimate use cases.
+ API_QUOTA_SCHEDULE_COUNT = Math.max(250,
+ mParser.getInt(KEY_API_QUOTA_SCHEDULE_COUNT, DEFAULT_API_QUOTA_SCHEDULE_COUNT));
+ API_QUOTA_SCHEDULE_WINDOW_MS = mParser.getDurationMillis(
+ KEY_API_QUOTA_SCHEDULE_WINDOW_MS, DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS);
+ API_QUOTA_SCHEDULE_THROW_EXCEPTION = mParser.getBoolean(
+ KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION,
+ DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION);
+ API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = mParser.getBoolean(
+ KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT,
+ DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT);
+ }
+
+ void dump(IndentingPrintWriter pw) {
+ pw.println("Settings:");
+ pw.increaseIndent();
+ 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_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.printPair(KEY_ENABLE_API_QUOTAS, ENABLE_API_QUOTAS).println();
+ pw.printPair(KEY_API_QUOTA_SCHEDULE_COUNT, API_QUOTA_SCHEDULE_COUNT).println();
+ pw.printPair(KEY_API_QUOTA_SCHEDULE_WINDOW_MS, API_QUOTA_SCHEDULE_WINDOW_MS).println();
+ pw.printPair(KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION,
+ API_QUOTA_SCHEDULE_THROW_EXCEPTION).println();
+ pw.printPair(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT,
+ API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println();
+
+ pw.decreaseIndent();
+ }
+
+ void dump(ProtoOutputStream proto) {
+ 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.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);
+
+ proto.write(ConstantsProto.ENABLE_API_QUOTAS, ENABLE_API_QUOTAS);
+ proto.write(ConstantsProto.API_QUOTA_SCHEDULE_COUNT, API_QUOTA_SCHEDULE_COUNT);
+ proto.write(ConstantsProto.API_QUOTA_SCHEDULE_WINDOW_MS, API_QUOTA_SCHEDULE_WINDOW_MS);
+ proto.write(ConstantsProto.API_QUOTA_SCHEDULE_THROW_EXCEPTION,
+ API_QUOTA_SCHEDULE_THROW_EXCEPTION);
+ proto.write(ConstantsProto.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT,
+ API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT);
+ }
+ }
+
+ final Constants mConstants;
+ final ConstantsObserver mConstantsObserver;
+
+ private static final Comparator<JobStatus> sPendingJobComparator = (o1, o2) -> {
+ // Jobs with an override state set (via adb) should be put first as tests/developers
+ // expect the jobs to run immediately.
+ if (o1.overrideState != o2.overrideState) {
+ // Higher override state (OVERRIDE_FULL) should be before lower state (OVERRIDE_SOFT)
+ return o2.overrideState - o1.overrideState;
+ }
+ 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);
+ }
+ mDebuggableApps.remove(pkgName);
+ }
+ }
+ } 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) {
+ final String servicePkg = job.getService().getPackageName();
+ if (job.isPersisted() && (packageName == null || packageName.equals(servicePkg))) {
+ // Only limit schedule calls for persisted jobs scheduled by the app itself.
+ final String pkg =
+ packageName == null ? job.getService().getPackageName() : packageName;
+ if (!mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG)) {
+ if (mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_LOGGED)) {
+ // Don't log too frequently
+ Slog.wtf(TAG, userId + "-" + pkg + " has called schedule() too many times");
+ mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_LOGGED);
+ }
+ mAppStandbyInternal.restrictApp(
+ pkg, userId, UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY);
+ if (mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION) {
+ final boolean isDebuggable;
+ synchronized (mLock) {
+ if (!mDebuggableApps.containsKey(packageName)) {
+ try {
+ final ApplicationInfo appInfo = AppGlobals.getPackageManager()
+ .getApplicationInfo(pkg, 0, userId);
+ if (appInfo != null) {
+ mDebuggableApps.put(packageName,
+ (appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0);
+ } else {
+ return JobScheduler.RESULT_FAILURE;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ isDebuggable = mDebuggableApps.get(packageName);
+ }
+ if (isDebuggable) {
+ // Only throw the exception for debuggable apps.
+ throw new LimitExceededException(
+ "schedule()/enqueue() called more than "
+ + mQuotaTracker.getLimit(
+ QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED)
+ + " times in the past "
+ + mQuotaTracker.getWindowSizeMs(
+ QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED)
+ + "ms. See the documentation for more information.");
+ }
+ }
+ if (mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT) {
+ return JobScheduler.RESULT_FAILURE;
+ }
+ }
+ mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_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);
+ }
+
+ FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED,
+ uId, null, jobStatus.getBatteryName(),
+ FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__SCHEDULED,
+ JobProtoEnums.STOP_REASON_CANCELLED, jobStatus.getStandbyBucket(),
+ jobStatus.getJobId(),
+ jobStatus.hasChargingConstraint(),
+ jobStatus.hasBatteryNotLowConstraint(),
+ jobStatus.hasStorageNotLowConstraint(),
+ jobStatus.hasTimingDelayConstraint(),
+ jobStatus.hasDeadlineConstraint(),
+ jobStatus.hasIdleConstraint(),
+ jobStatus.hasConnectivityConstraint(),
+ jobStatus.hasContentTriggerConstraint());
+
+ // 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, sPendingJobComparator);
+ 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();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onRestrictedBucketChanged(List<JobStatus> jobs) {
+ final int len = jobs.size();
+ if (len == 0) {
+ Slog.wtf(TAG, "onRestrictedBucketChanged called with no jobs");
+ return;
+ }
+ synchronized (mLock) {
+ for (int i = 0; i < len; ++i) {
+ JobStatus js = jobs.get(i);
+ for (int j = mRestrictiveControllers.size() - 1; j >= 0; --j) {
+ // Effective standby bucket can change after this in some situations so use
+ // the real bucket so that the job is tracked by the controllers.
+ if (js.getStandbyBucket() == RESTRICTED_INDEX) {
+ mRestrictiveControllers.get(j).startTrackingRestrictedJobLocked(js);
+ } else {
+ mRestrictiveControllers.get(j).stopTrackingRestrictedJobLocked(js);
+ }
+ }
+ }
+ }
+ 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 = Objects.requireNonNull(
+ 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);
+ mQuotaTracker = new CountQuotaTracker(context, QUOTA_CATEGORIZER);
+ mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED,
+ mConstants.API_QUOTA_SCHEDULE_COUNT,
+ mConstants.API_QUOTA_SCHEDULE_WINDOW_MS);
+ // Log at most once per minute.
+ mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED, 1, 60_000);
+
+ mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
+ mAppStandbyInternal.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>();
+ final ConnectivityController connectivityController = new ConnectivityController(this);
+ mControllers.add(connectivityController);
+ mControllers.add(new TimeController(this));
+ final IdleController idleController = new IdleController(this);
+ mControllers.add(idleController);
+ 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);
+
+ mRestrictiveControllers = new ArrayList<>();
+ mRestrictiveControllers.add(mBatteryController);
+ mRestrictiveControllers.add(connectivityController);
+ mRestrictiveControllers.add(idleController);
+
+ // Create restrictions
+ mJobRestrictions = new ArrayList<>();
+ mJobRestrictions.add(new ThermalStatusRestriction(this));
+
+ mSystemGalleryPackage = Objects.requireNonNull(
+ context.getString(R.string.config_systemGallery));
+
+ // 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 = Objects.requireNonNull(
+ 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;
+
+ 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, sPendingJobComparator);
+ } 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()) {
+ // If a restricted job doesn't have dynamic constraints satisfied, assume that's
+ // the reason the job is being stopped, instead of because of other constraints
+ // not being satisfied.
+ if (running.getEffectiveStandbyBucket() == RESTRICTED_INDEX
+ && !running.areDynamicConstraintsSatisfied()) {
+ serviceContext.cancelExecutingJobLocked(
+ JobParameters.REASON_RESTRICTED_BUCKET,
+ "cancelled due to restricted bucket");
+ } else {
+ 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.getReasonCodeDescription(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(sPendingJobComparator);
+ }
+
+ 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 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) {
+ }
+
+ final boolean shouldForceBatchJob;
+ // Restricted jobs must always be batched
+ if (job.getEffectiveStandbyBucket() == RESTRICTED_INDEX) {
+ shouldForceBatchJob = true;
+ } else if (job.getNumFailures() > 0) {
+ shouldForceBatchJob = false;
+ } else {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final boolean batchDelayExpired = job.getFirstForceBatchedTimeElapsed() > 0
+ && nowElapsed - job.getFirstForceBatchedTimeElapsed()
+ >= mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS;
+ shouldForceBatchJob =
+ mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT > 1
+ && job.getEffectiveStandbyBucket() != ACTIVE_INDEX
+ && !batchDelayExpired;
+ }
+
+ if (shouldForceBatchJob) {
+ // Force batching non-ACTIVE jobs. Don't include them in the other counts.
+ forceBatchedCount++;
+ if (job.getFirstForceBatchedTimeElapsed() == 0) {
+ job.setFirstForceBatchedTimeElapsed(sElapsedRealtimeClock.millis());
+ }
+ } else {
+ unbatchedCount++;
+ }
+ runnableJobs.add(job);
+ } else {
+ evaluateControllerStatesLocked(job);
+ }
+ }
+
+ public void postProcess() {
+ if (unbatchedCount > 0
+ || forceBatchedCount >= mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT) {
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Running jobs.");
+ }
+ noteJobsPending(runnableJobs);
+ mPendingJobs.addAll(runnableJobs);
+ if (mPendingJobs.size() > 1) {
+ mPendingJobs.sort(sPendingJobComparator);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Not running anything.");
+ }
+ }
+
+ // Be ready for next time
+ reset();
+ }
+
+ @VisibleForTesting
+ void reset() {
+ 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);
+ final boolean backingUp = mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0;
+
+ if (DEBUG) {
+ Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
+ + " exists=" + jobExists + " userStarted=" + userStarted
+ + " backingUp=" + backingUp);
+ }
+
+ // These are also fairly cheap to check, though they typically will not
+ // be conditions we fail.
+ if (!jobExists || !userStarted || backingUp) {
+ 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);
+ final boolean backingUp = mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0;
+
+ if (DEBUG) {
+ Slog.v(TAG, "areComponentsInPlaceLocked: " + job.toShortString()
+ + " exists=" + jobExists + " userStarted=" + userStarted
+ + " backingUp=" + backingUp);
+ }
+
+ // These are also fairly cheap to check, though they typically will not
+ // be conditions we fail.
+ if (!jobExists || !userStarted || backingUp) {
+ 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 String getMediaBackupPackage() {
+ return mSystemGalleryPackage;
+ }
+
+ @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);
+ FrameworkStatsLog.write_non_chained(
+ FrameworkStatsLog.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_RARE) {
+ return RESTRICTED_INDEX;
+ } else if (bucket > UsageStatsManager.STANDBY_BUCKET_FREQUENT) {
+ return RARE_INDEX;
+ } else if (bucket > UsageStatsManager.STANDBY_BUCKET_WORKING_SET) {
+ return FREQUENT_INDEX;
+ } else if (bucket > UsageStatsManager.STANDBY_BUCKET_ACTIVE) {
+ return WORKING_INDEX;
+ } else {
+ return ACTIVE_INDEX;
+ }
+ }
+
+ // Static to support external callers
+ public static int standbyBucketForPackage(String packageName, int userId, long elapsedNow) {
+ UsageStatsManagerInternal usageStats = LocalServices.getService(
+ UsageStatsManagerInternal.class);
+ int bucket = usageStats != null
+ ? usageStats.getAppStandbyBucket(packageName, userId, elapsedNow)
+ : 0;
+
+ bucket = standbyBucketToBucketIndex(bucket);
+
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, packageName + "/" + userId + " standby bucket index: " + bucket);
+ }
+ return bucket;
+ }
+
+ /**
+ * Binder stub trampoline implementation
+ */
+ final class JobSchedulerStub extends IJobScheduler.Stub {
+ /** Cache determination of whether a given app can persist jobs
+ * key is uid of the calling app; value is undetermined/true/false
+ */
+ private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>();
+
+ // Enforce that only the app itself (or shared uid participant) can schedule a
+ // job that runs one of the app's services, as well as verifying that the
+ // named service properly requires the BIND_JOB_SERVICE permission
+ private void enforceValidJobRequest(int uid, JobInfo job) {
+ final IPackageManager pm = AppGlobals.getPackageManager();
+ final ComponentName service = job.getService();
+ try {
+ ServiceInfo si = pm.getServiceInfo(service,
+ PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+ UserHandle.getUserId(uid));
+ if (si == null) {
+ throw new IllegalArgumentException("No such service " + service);
+ }
+ if (si.applicationInfo.uid != uid) {
+ throw new IllegalArgumentException("uid " + uid +
+ " cannot schedule job in " + service.getPackageName());
+ }
+ if (!JobService.PERMISSION_BIND.equals(si.permission)) {
+ throw new IllegalArgumentException("Scheduled service " + service
+ + " does not require android.permission.BIND_JOB_SERVICE permission");
+ }
+ } catch (RemoteException e) {
+ // Can't happen; the Package Manager is in this same process
+ }
+ }
+
+ private boolean canPersistJobs(int pid, int uid) {
+ // If we get this far we're good to go; all we need to do now is check
+ // whether the app is allowed to persist its scheduled work.
+ final boolean canPersist;
+ synchronized (mPersistCache) {
+ Boolean cached = mPersistCache.get(uid);
+ if (cached != null) {
+ canPersist = cached.booleanValue();
+ } else {
+ // Persisting jobs is tantamount to running at boot, so we permit
+ // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED
+ // permission
+ int result = getContext().checkPermission(
+ android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid);
+ canPersist = (result == PackageManager.PERMISSION_GRANTED);
+ mPersistCache.put(uid, canPersist);
+ }
+ }
+ return canPersist;
+ }
+
+ private void validateJobFlags(JobInfo job, int callingUid) {
+ if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) {
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG);
+ }
+ if ((job.getFlags() & JobInfo.FLAG_EXEMPT_FROM_APP_STANDBY) != 0) {
+ if (callingUid != Process.SYSTEM_UID) {
+ throw new SecurityException("Job has invalid flags");
+ }
+ if (job.isPeriodic()) {
+ Slog.wtf(TAG, "Periodic jobs mustn't have"
+ + " FLAG_EXEMPT_FROM_APP_STANDBY. Job=" + job);
+ }
+ }
+ }
+
+ // IJobScheduler implementation
+ @Override
+ public int schedule(JobInfo job) throws RemoteException {
+ if (DEBUG) {
+ Slog.d(TAG, "Scheduling job: " + job.toString());
+ }
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(uid);
+
+ enforceValidJobRequest(uid, job);
+ if (job.isPersisted()) {
+ if (!canPersistJobs(pid, uid)) {
+ throw new IllegalArgumentException("Error: requested job be persisted without"
+ + " holding RECEIVE_BOOT_COMPLETED permission.");
+ }
+ }
+
+ validateJobFlags(job, uid);
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, userId,
+ null);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ // IJobScheduler implementation
+ @Override
+ public int enqueue(JobInfo job, JobWorkItem work) throws RemoteException {
+ if (DEBUG) {
+ Slog.d(TAG, "Enqueueing job: " + job.toString() + " work: " + work);
+ }
+ final int uid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(uid);
+
+ enforceValidJobRequest(uid, job);
+ if (job.isPersisted()) {
+ throw new IllegalArgumentException("Can't enqueue work for persisted jobs");
+ }
+ if (work == null) {
+ throw new NullPointerException("work is null");
+ }
+
+ validateJobFlags(job, uid);
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.scheduleAsPackage(job, work, uid, null, userId,
+ null);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag)
+ throws RemoteException {
+ final int callerUid = Binder.getCallingUid();
+ if (DEBUG) {
+ Slog.d(TAG, "Caller uid " + callerUid + " scheduling job: " + job.toString()
+ + " on behalf of " + packageName + "/");
+ }
+
+ if (packageName == null) {
+ throw new NullPointerException("Must specify a package for scheduleAsPackage()");
+ }
+
+ int mayScheduleForOthers = getContext().checkCallingOrSelfPermission(
+ android.Manifest.permission.UPDATE_DEVICE_STATS);
+ if (mayScheduleForOthers != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Caller uid " + callerUid
+ + " not permitted to schedule jobs for other apps");
+ }
+
+ validateJobFlags(job, callerUid);
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.scheduleAsPackage(job, null, callerUid,
+ packageName, userId, tag);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public ParceledListSlice<JobInfo> getAllPendingJobs() throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return new ParceledListSlice<>(JobSchedulerService.this.getPendingJobs(uid));
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public JobInfo getPendingJob(int jobId) throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.getPendingJob(uid, jobId);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public void cancelAll() throws RemoteException {
+ final int uid = Binder.getCallingUid();
+ long ident = Binder.clearCallingIdentity();
+ try {
+ JobSchedulerService.this.cancelJobsForUid(uid,
+ "cancelAll() called by app, callingUid=" + uid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public void cancel(int jobId) throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ JobSchedulerService.this.cancelJob(uid, jobId, uid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * "dumpsys" infrastructure
+ */
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, pw)) return;
+
+ int filterUid = -1;
+ boolean proto = false;
+ if (!ArrayUtils.isEmpty(args)) {
+ int opti = 0;
+ while (opti < args.length) {
+ String arg = args[opti];
+ if ("-h".equals(arg)) {
+ dumpHelp(pw);
+ return;
+ } else if ("-a".equals(arg)) {
+ // Ignore, we always dump all.
+ } else if ("--proto".equals(arg)) {
+ proto = true;
+ } else if (arg.length() > 0 && arg.charAt(0) == '-') {
+ pw.println("Unknown option: " + arg);
+ return;
+ } else {
+ break;
+ }
+ opti++;
+ }
+ if (opti < args.length) {
+ String pkg = args[opti];
+ try {
+ filterUid = getContext().getPackageManager().getPackageUid(pkg,
+ PackageManager.MATCH_ANY_USER);
+ } catch (NameNotFoundException ignored) {
+ pw.println("Invalid package: " + pkg);
+ return;
+ }
+ }
+ }
+
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ if (proto) {
+ JobSchedulerService.this.dumpInternalProto(fd, filterUid);
+ } else {
+ JobSchedulerService.this.dumpInternal(new IndentingPrintWriter(pw, " "),
+ filterUid);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ }
+
+ @Override
+ public 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 satisfied, boolean force) {
+ Slog.d(TAG, "executeRunCommand(): " + pkgName + "/" + userId
+ + " " + jobId + " s=" + satisfied + " 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
+ : (satisfied ? JobStatus.OVERRIDE_SORTING : JobStatus.OVERRIDE_SOFT);
+
+ // Re-evaluate constraints after the override is set in case one of the overridden
+ // constraints was preventing another constraint from thinking it needed to update.
+ for (int c = mControllers.size() - 1; c >= 0; --c) {
+ mControllers.get(c).reevaluateStateLocked(uid);
+ }
+
+ if (!js.isConstraintsSatisfied()) {
+ js.overrideState = JobStatus.OVERRIDE_NONE;
+ 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 resetExecutionQuota(@NonNull String pkgName, int userId) {
+ mQuotaController.clearAppStats(userId, pkgName);
+ }
+
+ void resetScheduleQuota() {
+ mQuotaTracker.clear();
+ }
+
+ 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();
+
+ mQuotaTracker.dump(pw);
+ 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.print(" " + JobParameters.getReasonCodeDescription(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);
+ }
+
+ mQuotaTracker.dump(proto, JobSchedulerServiceDumpProto.QUOTA_TRACKER);
+
+ 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().dumpDebug(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..1e7206287566
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -0,0 +1,496 @@
+/*
+ * 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 "reset-execution-quota":
+ return resetExecutionQuota(pw);
+ case "reset-schedule-quota":
+ return resetScheduleQuota(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;
+ boolean satisfied = false;
+ int userId = UserHandle.USER_SYSTEM;
+
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-f":
+ case "--force":
+ force = true;
+ break;
+
+ case "-s":
+ case "--satisfied":
+ satisfied = true;
+ break;
+
+ case "-u":
+ case "--user":
+ userId = Integer.parseInt(getNextArgRequired());
+ break;
+
+ default:
+ pw.println("Error: unknown option '" + opt + "'");
+ return -1;
+ }
+ }
+
+ if (force && satisfied) {
+ pw.println("Cannot specify both --force and --satisfied");
+ 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, satisfied, 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 resetExecutionQuota(PrintWriter pw) throws Exception {
+ checkPermission("reset execution quota");
+
+ 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 long ident = Binder.clearCallingIdentity();
+ try {
+ mInternal.resetExecutionQuota(pkgName, userId);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ return 0;
+ }
+
+ private int resetScheduleQuota(PrintWriter pw) throws Exception {
+ checkPermission("reset schedule quota");
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mInternal.resetScheduleQuota();
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ return 0;
+ }
+
+ 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] [-s | --satisfied] [-u | --user USER_ID] PACKAGE JOB_ID");
+ pw.println(" Trigger immediate execution of a specific scheduled job. For historical");
+ pw.println(" reasons, some constraints, such as battery, are ignored when this");
+ pw.println(" command is called. If you don't want any constraints to be ignored,");
+ pw.println(" include the -s flag.");
+ pw.println(" Options:");
+ pw.println(" -f or --force: run the job even if technical constraints such as");
+ pw.println(" connectivity are not currently met. This is incompatible with -f ");
+ pw.println(" and so an error will be reported if both are given.");
+ pw.println(" -s or --satisfied: run the job only if all constraints are met.");
+ pw.println(" This is incompatible with -f and so an error will be reported");
+ pw.println(" if both are given.");
+ 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..565ed959aeb4
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -0,0 +1,876 @@
+/*
+ * 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.JobProtoEnums;
+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.internal.util.FrameworkStatsLog;
+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);
+ FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED,
+ job.getSourceUid(), null, job.getBatteryName(),
+ FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__STARTED,
+ JobProtoEnums.STOP_REASON_UNKNOWN, job.getStandbyBucket(), job.getJobId(),
+ job.hasChargingConstraint(),
+ job.hasBatteryNotLowConstraint(),
+ job.hasStorageNotLowConstraint(),
+ job.hasTimingDelayConstraint(),
+ job.hasDeadlineConstraint(),
+ job.hasIdleConstraint(),
+ job.hasConnectivityConstraint(),
+ job.hasContentTriggerConstraint());
+ try {
+ mBatteryStats.noteJobStart(job.getBatteryName(), job.getSourceUid());
+ } 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);
+ FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED,
+ completedJob.getSourceUid(), null, completedJob.getBatteryName(),
+ FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__FINISHED,
+ mParams.getStopReason(), completedJob.getStandbyBucket(), completedJob.getJobId(),
+ completedJob.hasChargingConstraint(),
+ completedJob.hasBatteryNotLowConstraint(),
+ completedJob.hasStorageNotLowConstraint(),
+ completedJob.hasTimingDelayConstraint(),
+ completedJob.hasDeadlineConstraint(),
+ completedJob.hasIdleConstraint(),
+ completedJob.hasConnectivityConstraint(),
+ completedJob.hasContentTriggerConstraint());
+ try {
+ mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), mRunningJob.getSourceUid(),
+ mParams.getStopReason());
+ } 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..cb3c43714111
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
@@ -0,0 +1,52 @@
+/*
+ * 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 android.annotation.NonNull;
+
+import com.android.server.job.controllers.JobStatus;
+
+import java.util.List;
+
+/**
+ * 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);
+
+ /**
+ * Called when these jobs are added or removed from the
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
+ */
+ void onRestrictedBucketChanged(@NonNull List<JobStatus> jobs);
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
new file mode 100644
index 000000000000..484fec31e594
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
@@ -0,0 +1,45 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsJobSchedulerTestCases",
+ "options": [
+ {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+ {"exclude-annotation": "androidx.test.filters.LargeTest"}
+ ]
+ },
+ {
+ "name": "FrameworksMockingServicesTests",
+ "options": [
+ {"include-filter": "com.android.server.job"},
+ {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"}
+ ]
+ },
+ {
+ "name": "FrameworksServicesTests",
+ "options": [
+ {"include-filter": "com.android.server.job"},
+ {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"}
+ ]
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "CtsJobSchedulerTestCases"
+ },
+ {
+ "name": "FrameworksMockingServicesTests",
+ "options": [
+ {"include-filter": "com.android.server.job"}
+ ]
+ },
+ {
+ "name": "FrameworksServicesTests",
+ "options": [
+ {"include-filter": "com.android.server.job"}
+ ]
+ }
+ ]
+}
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..1645bcb928c1
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
@@ -0,0 +1,250 @@
+/*
+ * 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.NEVER_INDEX;
+
+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.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.Objects;
+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 = Objects.requireNonNull(
+ 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);
+ }
+ if (isActive && jobStatus.getStandbyBucket() == NEVER_INDEX) {
+ Slog.wtf(TAG, "App " + packageName + " became active but still in NEVER bucket");
+ }
+ 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..461ef21af7ee
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
@@ -0,0 +1,294 @@
+/*
+ * 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 RestrictingController {
+ 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 startTrackingRestrictedJobLocked(JobStatus jobStatus) {
+ maybeStartTrackingJobLocked(jobStatus, null);
+ }
+
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) {
+ if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) {
+ mTrackedTasks.remove(taskStatus);
+ }
+ }
+
+ @Override
+ public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) {
+ if (!jobStatus.hasPowerConstraint()) {
+ maybeStopTrackingJobLocked(jobStatus, null, false);
+ }
+ }
+
+ 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..bb94275fc409
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -0,0 +1,702 @@
+/*
+ * 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 static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
+
+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 RestrictingController 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);
+ }
+ }
+
+ @Override
+ public void startTrackingRestrictedJobLocked(JobStatus jobStatus) {
+ // Don't need to start tracking the job. If the job needed network, it would already be
+ // tracked.
+ if (jobStatus.hasConnectivityConstraint()) {
+ updateConstraintsSatisfied(jobStatus);
+ }
+ }
+
+ @Override
+ public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) {
+ // Shouldn't stop tracking the job here. If the job was tracked, it still needs network,
+ // even after being unrestricted.
+ if (jobStatus.hasConnectivityConstraint()) {
+ updateConstraintsSatisfied(jobStatus);
+ }
+ }
+
+ /**
+ * Returns true if the job's requested network is available. This DOES NOT necessarily 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) {
+ final NetworkCapabilities required;
+ // A restricted job that's out of quota MUST use an unmetered network.
+ if (jobStatus.getEffectiveStandbyBucket() == RESTRICTED_INDEX
+ && !jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) {
+ required = new NetworkCapabilities(
+ jobStatus.getJob().getRequiredNetwork().networkCapabilities)
+ .addCapability(NET_CAPABILITY_NOT_METERED);
+ } else {
+ required = jobStatus.getJob().getRequiredNetwork().networkCapabilities;
+ }
+
+ return required.satisfiedByNetworkCapabilities(capabilities);
+ }
+
+ private static boolean isRelaxedSatisfied(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities, Constants constants) {
+ // Only consider doing this for unrestricted prefetching jobs
+ if (!jobStatus.getJob().isPrefetch() || jobStatus.getStandbyBucket() == RESTRICTED_INDEX) {
+ 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.dumpDebug(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.dumpDebug(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..5fcd774189ac
--- /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..c0b3204192d6
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
@@ -0,0 +1,159 @@
+/*
+ * 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;
+
+/**
+ * Simple controller that tracks whether the device is idle or not. Idleness depends on the device
+ * type and is not related to device-idle (Doze mode) despite the similar naming.
+ *
+ * @see CarIdlenessTracker
+ * @see DeviceIdlenessTracker
+ * @see IdlenessTracker
+ */
+public final class IdleController extends RestrictingController 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 startTrackingRestrictedJobLocked(JobStatus jobStatus) {
+ maybeStartTrackingJobLocked(jobStatus, null);
+ }
+
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
+ boolean forUpdate) {
+ if (taskStatus.clearTrackingController(JobStatus.TRACKING_IDLE)) {
+ mTrackedTasks.remove(taskStatus);
+ }
+ }
+
+ @Override
+ public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) {
+ if (!jobStatus.hasIdleConstraint()) {
+ maybeStopTrackingJobLocked(jobStatus, null, false);
+ }
+ }
+
+ /**
+ * 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..d7be2595e88b
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -0,0 +1,2038 @@
+/*
+ * 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.NEVER_INDEX;
+import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
+import static com.android.server.job.JobSchedulerService.WORKING_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.provider.MediaStore;
+import android.text.format.DateFormat;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FrameworkStatsLog;
+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 {
+ private static final String TAG = "JobScheduler.JobStatus";
+ 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 additional set of dynamic constraints that must be met if the job's effective bucket is
+ * {@link JobSchedulerService#RESTRICTED_INDEX}. Connectivity can be ignored if the job doesn't
+ * need network.
+ */
+ private static final int DYNAMIC_RESTRICTED_CONSTRAINTS =
+ CONSTRAINT_BATTERY_NOT_LOW
+ | CONSTRAINT_CHARGING
+ | CONSTRAINT_CONNECTIVITY
+ | CONSTRAINT_IDLE;
+
+ /**
+ * Standard media URIs that contain the media files that might be important to the user.
+ * @see #mHasMediaBackupExemption
+ */
+ private static final Uri[] MEDIA_URIS_FOR_STANDBY_EXEMPTION = {
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+ };
+
+ /**
+ * 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;
+
+ // No override.
+ public static final int OVERRIDE_NONE = 0;
+ // Override to improve sorting order. Does not affect constraint evaluation.
+ public static final int OVERRIDE_SORTING = 1;
+ // Soft override: ignore constraints like time that don't affect API availability
+ public static final int OVERRIDE_SOFT = 2;
+ // Full override: ignore all constraints including API-affecting like connectivity
+ public static final int OVERRIDE_FULL = 3;
+
+ /** 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 maximum 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 of constraints that must be satisfied for the job if/because it's in the RESTRICTED
+ * bucket.
+ */
+ private int mDynamicConstraints = 0;
+
+ /**
+ * Indicates whether the job is responsible for backing up media, so we can be lenient in
+ * applying standby throttling.
+ *
+ * Doesn't exempt jobs with a deadline constraint, as they can be started without any content or
+ * network changes, in which case this exemption does not make sense.
+ *
+ * TODO(b/149519887): Use a more explicit signal, maybe an API flag, that the scheduling package
+ * needs to provide at the time of scheduling a job.
+ */
+ private final boolean mHasMediaBackupExemption;
+
+ // 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 = JobStatus.OVERRIDE_NONE;
+
+ // 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;
+
+ /** The job's dynamic requirements have been satisfied. */
+ private boolean mReadyDynamicSatisfied;
+
+ /** 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;
+
+ boolean requiresNetwork = false;
+ int requiredConstraints = job.getConstraintFlags();
+ if (job.getRequiredNetwork() != null) {
+ requiredConstraints |= CONSTRAINT_CONNECTIVITY;
+ requiresNetwork = true;
+ }
+ if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME) {
+ requiredConstraints |= CONSTRAINT_TIMING_DELAY;
+ }
+ if (latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) {
+ requiredConstraints |= CONSTRAINT_DEADLINE;
+ }
+ boolean exemptedMediaUrisOnly = false;
+ if (job.getTriggerContentUris() != null) {
+ requiredConstraints |= CONSTRAINT_CONTENT_TRIGGER;
+ exemptedMediaUrisOnly = true;
+ for (JobInfo.TriggerContentUri uri : job.getTriggerContentUris()) {
+ if (!ArrayUtils.contains(MEDIA_URIS_FOR_STANDBY_EXEMPTION, uri.getUri())) {
+ exemptedMediaUrisOnly = false;
+ break;
+ }
+ }
+ }
+ this.requiredConstraints = requiredConstraints;
+ mRequiredConstraintsOfInterest = requiredConstraints & CONSTRAINTS_OF_INTEREST;
+ mReadyNotDozing = (job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
+ if (standbyBucket == RESTRICTED_INDEX) {
+ addDynamicConstraints(DYNAMIC_RESTRICTED_CONSTRAINTS);
+ } else {
+ mReadyDynamicSatisfied = false;
+ }
+
+ 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);
+ }
+ final JobSchedulerInternal jsi = LocalServices.getService(JobSchedulerInternal.class);
+ mHasMediaBackupExemption = !job.hasLateConstraint() && exemptedMediaUrisOnly
+ && requiresNetwork && this.sourcePackageName.equals(jsi.getMediaBackupPackage());
+ }
+
+ /** 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);
+ 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;
+ }
+ final int actualBucket = getStandbyBucket();
+ if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX
+ && mHasMediaBackupExemption) {
+ // Cap it at WORKING_INDEX as media back up jobs are important to the user, and the
+ // source package may not have been used directly in a while.
+ return Math.min(WORKING_INDEX, actualBucket);
+ }
+ return actualBucket;
+ }
+
+ /** Returns the real standby bucket of the job. */
+ public int getStandbyBucket() {
+ return standbyBucket;
+ }
+
+ public void setStandbyBucket(int newBucket) {
+ if (newBucket == RESTRICTED_INDEX) {
+ // Adding to the bucket.
+ addDynamicConstraints(DYNAMIC_RESTRICTED_CONSTRAINTS);
+ } else if (standbyBucket == RESTRICTED_INDEX) {
+ // Removing from the RESTRICTED bucket.
+ removeDynamicConstraints(DYNAMIC_RESTRICTED_CONSTRAINTS);
+ }
+
+ 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() {
+ // No need to check mDynamicConstraints since connectivity will only be in that list if
+ // it's already in the requiredConstraints list.
+ return (requiredConstraints&CONSTRAINT_CONNECTIVITY) != 0;
+ }
+
+ public boolean hasChargingConstraint() {
+ return hasConstraint(CONSTRAINT_CHARGING);
+ }
+
+ public boolean hasBatteryNotLowConstraint() {
+ return hasConstraint(CONSTRAINT_BATTERY_NOT_LOW);
+ }
+
+ /** Returns true if the job requires charging OR battery not low. */
+ boolean hasPowerConstraint() {
+ return hasConstraint(CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW);
+ }
+
+ public boolean hasStorageNotLowConstraint() {
+ return hasConstraint(CONSTRAINT_STORAGE_NOT_LOW);
+ }
+
+ public boolean hasTimingDelayConstraint() {
+ return hasConstraint(CONSTRAINT_TIMING_DELAY);
+ }
+
+ public boolean hasDeadlineConstraint() {
+ return hasConstraint(CONSTRAINT_DEADLINE);
+ }
+
+ public boolean hasIdleConstraint() {
+ return hasConstraint(CONSTRAINT_IDLE);
+ }
+
+ public boolean hasContentTriggerConstraint() {
+ // No need to check mDynamicConstraints since content trigger will only be in that list if
+ // it's already in the requiredConstraints list.
+ return (requiredConstraints&CONSTRAINT_CONTENT_TRIGGER) != 0;
+ }
+
+ /**
+ * Checks both {@link #requiredConstraints} and {@link #mDynamicConstraints} to see if this job
+ * requires the specified constraint.
+ */
+ private boolean hasConstraint(int constraint) {
+ return (requiredConstraints & constraint) != 0 || (mDynamicConstraints & constraint) != 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;
+ }
+ if (DEBUG) {
+ Slog.v(TAG,
+ "Constraint " + constraint + " is " + (!state ? "NOT " : "") + "satisfied for "
+ + toShortString());
+ }
+ satisfiedConstraints = (satisfiedConstraints&~constraint) | (state ? constraint : 0);
+ mSatisfiedConstraintsOfInterest = satisfiedConstraints & CONSTRAINTS_OF_INTEREST;
+ mReadyDynamicSatisfied = mDynamicConstraints != 0
+ && mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints);
+ if (STATS_LOG_ENABLED && (STATSD_CONSTRAINTS_TO_LOG & constraint) != 0) {
+ FrameworkStatsLog.write_non_chained(
+ FrameworkStatsLog.SCHEDULED_JOB_CONSTRAINT_CHANGED,
+ sourceUid, null, getBatteryName(), getProtoConstraint(constraint),
+ state ? FrameworkStatsLog.SCHEDULED_JOB_CONSTRAINT_CHANGED__STATE__SATISFIED
+ : FrameworkStatsLog
+ .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;
+ }
+
+ /**
+ * Indicates that this job cannot run without the specified constraints. This is evaluated
+ * separately from the job's explicitly requested constraints and MUST be satisfied before
+ * the job can run if the app doesn't have quota.
+ */
+ private void addDynamicConstraints(int constraints) {
+ if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) {
+ // Quota should never be used as a dynamic constraint.
+ Slog.wtf(TAG, "Tried to set quota as a dynamic constraint");
+ constraints &= ~CONSTRAINT_WITHIN_QUOTA;
+ }
+
+ // Connectivity and content trigger are special since they're only valid to add if the
+ // job has requested network or specific content URIs. Adding these constraints to jobs
+ // that don't need them doesn't make sense.
+ if (!hasConnectivityConstraint()) {
+ constraints &= ~CONSTRAINT_CONNECTIVITY;
+ }
+ if (!hasContentTriggerConstraint()) {
+ constraints &= ~CONSTRAINT_CONTENT_TRIGGER;
+ }
+
+ mDynamicConstraints |= constraints;
+ mReadyDynamicSatisfied = mDynamicConstraints != 0
+ && mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints);
+ }
+
+ /**
+ * Removes dynamic constraints from a job, meaning that the requirements are not required for
+ * the job to run (if the job itself hasn't requested the constraint. This is separate from
+ * the job's explicitly requested constraints and does not remove those requested constraints.
+ *
+ */
+ private void removeDynamicConstraints(int constraints) {
+ mDynamicConstraints &= ~constraints;
+ mReadyDynamicSatisfied = mDynamicConstraints != 0
+ && mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints);
+ }
+
+ 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;
+ mReadyDynamicSatisfied = mDynamicConstraints != 0
+ && mDynamicConstraints == (satisfied & mDynamicConstraints);
+ 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;
+ default:
+ mReadyDynamicSatisfied = mDynamicConstraints != 0
+ && mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints);
+ break;
+ }
+ return toReturn;
+ }
+
+ private boolean isReady(int satisfiedConstraints) {
+ // Quota and dynamic constraints trump all other constraints.
+ // NEVER jobs are not supposed to run at all. Since we're using quota to allow parole
+ // sessions (exempt from dynamic restrictions), we need the additional check to ensure
+ // that NEVER jobs don't run.
+ // TODO: cleanup quota and standby bucket management so we don't need the additional checks
+ if ((!mReadyWithinQuota && !mReadyDynamicSatisfied)
+ || getEffectiveStandbyBucket() == NEVER_INDEX) {
+ return false;
+ }
+ // Deadline constraint trumps other constraints besides quota and dynamic (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));
+ }
+
+ /** All constraints besides implicit and deadline. */
+ 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;
+
+ /** Returns true whenever all dynamically set constraints are satisfied. */
+ public boolean areDynamicConstraintsSatisfied() {
+ return mReadyDynamicSatisfied;
+ }
+
+ /**
+ * @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().dumpDebug(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";
+ case 5:
+ return "RESTRICTED";
+ 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().isDefinitelyEmpty()) {
+ pw.print(prefix); pw.print(" Extras: ");
+ pw.println(job.getExtras().toShortString());
+ }
+ if (job.getTransientExtras() != null && !job.getTransientExtras().isDefinitelyEmpty()) {
+ 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();
+ pw.print(prefix);
+ pw.print("Dynamic constraints:");
+ dumpConstraints(pw, mDynamicConstraints);
+ 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);
+ }
+ pw.print(prefix);
+ pw.print(" readyDynamicSatisfied: ");
+ pw.println(mReadyDynamicSatisfied);
+
+ 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().dumpDebug(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().isDefinitelyEmpty()) {
+ job.getExtras().dumpDebug(proto, JobStatusDumpProto.JobInfo.EXTRAS);
+ }
+ if (job.getTransientExtras() != null && !job.getTransientExtras().isDefinitelyEmpty()) {
+ job.getTransientExtras().dumpDebug(proto, JobStatusDumpProto.JobInfo.TRANSIENT_EXTRAS);
+ }
+ if (job.getClipData() != null) {
+ job.getClipData().dumpDebug(proto, JobStatusDumpProto.JobInfo.CLIP_DATA);
+ }
+ if (uriPerms != null) {
+ uriPerms.dump(proto, JobStatusDumpProto.JobInfo.GRANTED_URI_PERMISSIONS);
+ }
+ if (job.getRequiredNetwork() != null) {
+ job.getRequiredNetwork().dumpDebug(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);
+ dumpConstraints(proto, JobStatusDumpProto.DYNAMIC_CONSTRAINTS, mDynamicConstraints);
+ 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.write(JobStatusDumpProto.ImplicitConstraints.IS_DYNAMIC_SATISFIED,
+ mReadyDynamicSatisfied);
+ 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.dumpDebug(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..d108f0b698f7
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -0,0 +1,2911 @@
+/*
+ * 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.RESTRICTED_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.Pair;
+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.GuardedBy;
+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.PriorityQueue;
+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 dumpDebug(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 + ", "
+ + "rateLimitJobCountExpirationTime=" + jobRateLimitExpirationTimeElapsed + ", "
+ + "rateLimitJobCountWindow=" + jobCountInRateLimitingWindow + ", "
+ + "rateLimitSessionCountExpirationTime="
+ + sessionRateLimitExpirationTimeElapsed + ", "
+ + "rateLimitSessionCountWindow=" + 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<>();
+
+ /**
+ * Listener to track and manage when each package comes back within quota.
+ */
+ @GuardedBy("mLock")
+ private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener();
+
+ /** 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,
+ 0, // NEVER
+ QcConstants.DEFAULT_WINDOW_SIZE_RESTRICTED_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,
+ 0, // NEVER
+ QcConstants.DEFAULT_MAX_JOB_COUNT_RESTRICTED
+ };
+
+ /**
+ * 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,
+ 0, // NEVER
+ QcConstants.DEFAULT_MAX_SESSION_COUNT_RESTRICTED,
+ };
+
+ /**
+ * 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;
+ }
+ clearAppStats(UserHandle.getUserId(uid), packageName);
+ mForegroundUids.delete(uid);
+ mUidToPackageCache.remove(uid);
+ }
+
+ @Override
+ public void onUserRemovedLocked(int userId) {
+ mTrackedJobs.delete(userId);
+ mPkgTimers.delete(userId);
+ mTimingSessions.delete(userId);
+ mInQuotaAlarmListener.removeAlarmsLocked(userId);
+ mExecutionStatsCache.delete(userId);
+ mUidToPackageCache.clear();
+ }
+
+ /** Drop all historical stats and stop tracking any active sessions for the specified app. */
+ public void clearAppStats(int userId, @NonNull String packageName) {
+ mTrackedJobs.delete(userId, packageName);
+ Timer timer = mPkgTimers.get(userId, packageName);
+ if (timer != null) {
+ if (timer.isActive()) {
+ Slog.e(TAG, "clearAppStats called before Timer turned off.");
+ timer.dropEverythingLocked();
+ }
+ mPkgTimers.delete(userId, packageName);
+ }
+ mTimingSessions.delete(userId, packageName);
+ mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
+ mExecutionStatsCache.delete(userId, packageName);
+ }
+
+ 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()) {
+ // Restricted jobs require additional constraints when charging, so don't immediately
+ // mark quota as free when charging.
+ if (standbyBucket != RESTRICTED_INDEX) {
+ 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;
+ if (stats.jobCountLimit == 0 || stats.sessionCountLimit == 0) {
+ // App won't be in quota until configuration changes.
+ stats.inQuotaTimeElapsed = Long.MAX_VALUE;
+ } else {
+ 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()) {
+ // Exclude active sessions from the session count so that new jobs aren't prevented
+ // from starting due to an app hitting the session limit.
+ 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);
+ }
+ if (stats.bgJobCountInWindow >= stats.jobCountLimit) {
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ nowElapsed + stats.windowSizeMs);
+ }
+ }
+
+ 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 class TimerChargingUpdateFunctor implements Consumer<Timer> {
+ private long mNowElapsed;
+ private boolean mIsCharging;
+
+ private void setStatus(long nowElapsed, boolean isCharging) {
+ mNowElapsed = nowElapsed;
+ mIsCharging = isCharging;
+ }
+
+ @Override
+ public void accept(Timer timer) {
+ if (JobSchedulerService.standbyBucketForPackage(timer.mPkg.packageName,
+ timer.mPkg.userId, mNowElapsed) != RESTRICTED_INDEX) {
+ // Restricted jobs need additional constraints even when charging, so don't
+ // immediately say that quota is free.
+ timer.onStateChangedLocked(mNowElapsed, mIsCharging);
+ }
+ }
+ }
+
+ private final TimerChargingUpdateFunctor
+ mTimerChargingUpdateFunctor = new TimerChargingUpdateFunctor();
+
+ private void handleNewChargingStateLocked() {
+ mTimerChargingUpdateFunctor.setStatus(sElapsedRealtimeClock.millis(),
+ mChargeTracker.isCharging());
+ if (DEBUG) {
+ Slog.d(TAG, "handleNewChargingStateLocked: " + mChargeTracker.isCharging());
+ }
+ // Deal with Timers first.
+ mPkgTimers.forEach(mTimerChargingUpdateFunctor);
+ // 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 {
+ mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
+ }
+ 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)) {
+ mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
+ } 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);
+
+ 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.");
+ }
+ mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
+ return;
+ }
+
+ // 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);
+ }
+ if (inQuotaTimeElapsed <= sElapsedRealtimeClock.millis()) {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ Slog.wtf(TAG,
+ "In quota time is " + (nowElapsed - inQuotaTimeElapsed) + "ms old. Now="
+ + nowElapsed + ", inQuotaTime=" + inQuotaTimeElapsed + ": " + stats);
+ inQuotaTimeElapsed = nowElapsed + 5 * MINUTE_IN_MILLIS;
+ }
+ mInQuotaAlarmListener.addAlarmLocked(userId, packageName, inQuotaTimeElapsed);
+ }
+
+ 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() {
+ final int standbyBucket = JobSchedulerService.standbyBucketForPackage(mPkg.packageName,
+ mPkg.userId, sElapsedRealtimeClock.millis());
+ return (standbyBucket == RESTRICTED_INDEX || !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.dumpDebug(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);
+ }
+ List<JobStatus> restrictedChanges = new ArrayList<>();
+ 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);
+ // Effective standby bucket can change after this in some situations so
+ // use the real bucket so that the job is tracked by the controllers.
+ if ((bucketIndex == RESTRICTED_INDEX
+ || js.getStandbyBucket() == RESTRICTED_INDEX)
+ && bucketIndex != js.getStandbyBucket()) {
+ restrictedChanges.add(js);
+ }
+ js.setStandbyBucket(bucketIndex);
+ }
+ Timer timer = mPkgTimers.get(userId, packageName);
+ if (timer != null && timer.isActive()) {
+ timer.rescheduleCutoff();
+ }
+ if (maybeUpdateConstraintForPkgLocked(userId, packageName)) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ if (restrictedChanges.size() > 0) {
+ mStateChangedListener.onRestrictedBucketChanged(restrictedChanges);
+ }
+ });
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+ }
+ }
+
+ static class AlarmQueue extends PriorityQueue<Pair<Package, Long>> {
+ AlarmQueue() {
+ super(1, (o1, o2) -> (int) (o1.second - o2.second));
+ }
+
+ /**
+ * Remove any instances of the Package from the queue.
+ *
+ * @return true if an instance was removed, false otherwise.
+ */
+ boolean remove(@NonNull Package pkg) {
+ boolean removed = false;
+ Pair[] alarms = toArray(new Pair[size()]);
+ for (int i = alarms.length - 1; i >= 0; --i) {
+ if (pkg.equals(alarms[i].first)) {
+ remove(alarms[i]);
+ removed = true;
+ }
+ }
+ return removed;
+ }
+ }
+
+ /** Track when UPTCs are expected to come back into quota. */
+ private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener {
+ @GuardedBy("mLock")
+ private final AlarmQueue mAlarmQueue = new AlarmQueue();
+ /** The next time the alarm is set to go off, in the elapsed realtime timebase. */
+ @GuardedBy("mLock")
+ private long mTriggerTimeElapsed = 0;
+ /** The minimum amount of time between quota check alarms. */
+ @GuardedBy("mLock")
+ private long mMinQuotaCheckDelayMs = QcConstants.DEFAULT_MIN_QUOTA_CHECK_DELAY_MS;
+
+ @GuardedBy("mLock")
+ void addAlarmLocked(int userId, @NonNull String pkgName, long inQuotaTimeElapsed) {
+ final Package pkg = new Package(userId, pkgName);
+ mAlarmQueue.remove(pkg);
+ mAlarmQueue.offer(new Pair<>(pkg, inQuotaTimeElapsed));
+ setNextAlarmLocked();
+ }
+
+ @GuardedBy("mLock")
+ void setMinQuotaCheckDelayMs(long minDelayMs) {
+ mMinQuotaCheckDelayMs = minDelayMs;
+ }
+
+ @GuardedBy("mLock")
+ void removeAlarmLocked(@NonNull Package pkg) {
+ if (mAlarmQueue.remove(pkg)) {
+ setNextAlarmLocked();
+ }
+ }
+
+ @GuardedBy("mLock")
+ void removeAlarmLocked(int userId, @NonNull String packageName) {
+ removeAlarmLocked(new Package(userId, packageName));
+ }
+
+ @GuardedBy("mLock")
+ void removeAlarmsLocked(int userId) {
+ boolean removed = false;
+ Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
+ for (int i = alarms.length - 1; i >= 0; --i) {
+ final Package pkg = (Package) alarms[i].first;
+ if (userId == pkg.userId) {
+ mAlarmQueue.remove(alarms[i]);
+ removed = true;
+ }
+ }
+ if (removed) {
+ setNextAlarmLocked();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void setNextAlarmLocked() {
+ setNextAlarmLocked(sElapsedRealtimeClock.millis());
+ }
+
+ @GuardedBy("mLock")
+ private void setNextAlarmLocked(long earliestTriggerElapsed) {
+ if (mAlarmQueue.size() > 0) {
+ final Pair<Package, Long> alarm = mAlarmQueue.peek();
+ final long nextTriggerTimeElapsed = Math.max(earliestTriggerElapsed, alarm.second);
+ // Only schedule the alarm if one of the following is true:
+ // 1. There isn't one currently scheduled
+ // 2. The new alarm is significantly earlier than the previous alarm. 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.
+ if (mTriggerTimeElapsed == 0
+ || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS
+ || mTriggerTimeElapsed < nextTriggerTimeElapsed) {
+ if (DEBUG) {
+ Slog.d(TAG, "Scheduling start alarm at " + nextTriggerTimeElapsed
+ + " for app " + alarm.first);
+ }
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed,
+ ALARM_TAG_QUOTA_CHECK, this, mHandler);
+ mTriggerTimeElapsed = nextTriggerTimeElapsed;
+ }
+ } else {
+ mAlarmManager.cancel(this);
+ mTriggerTimeElapsed = 0;
+ }
+ }
+
+ @Override
+ public void onAlarm() {
+ synchronized (mLock) {
+ while (mAlarmQueue.size() > 0) {
+ final Pair<Package, Long> alarm = mAlarmQueue.peek();
+ if (alarm.second <= sElapsedRealtimeClock.millis()) {
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE, alarm.first.userId, 0,
+ alarm.first.packageName).sendToTarget();
+ mAlarmQueue.remove(alarm);
+ } else {
+ break;
+ }
+ }
+ setNextAlarmLocked(sElapsedRealtimeClock.millis() + mMinQuotaCheckDelayMs);
+ }
+ }
+
+ @GuardedBy("mLock")
+ void dumpLocked(IndentingPrintWriter pw) {
+ pw.println("In quota alarms:");
+ pw.increaseIndent();
+
+ if (mAlarmQueue.size() == 0) {
+ pw.println("NOT WAITING");
+ } else {
+ Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
+ for (int i = 0; i < alarms.length; ++i) {
+ final Package pkg = (Package) alarms[i].first;
+ pw.print(pkg);
+ pw.print(": ");
+ pw.print(alarms[i].second);
+ pw.println();
+ }
+ }
+
+ pw.decreaseIndent();
+ }
+
+ @GuardedBy("mLock")
+ void dumpLocked(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(
+ StateControllerProto.QuotaController.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED,
+ mTriggerTimeElapsed);
+
+ Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
+ for (int i = 0; i < alarms.length; ++i) {
+ final long aToken = proto.start(
+ StateControllerProto.QuotaController.InQuotaAlarmListener.ALARMS);
+
+ final Package pkg = (Package) alarms[i].first;
+ pkg.dumpDebug(proto,
+ StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.PKG);
+ proto.write(
+ StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED,
+ (Long) alarms[i].second);
+
+ proto.end(aToken);
+ }
+
+ proto.end(token);
+ }
+ }
+
+ @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_WINDOW_SIZE_RESTRICTED_MS = "window_size_restricted_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_MAX_JOB_COUNT_RESTRICTED = "max_job_count_restricted";
+ 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_RESTRICTED =
+ "max_session_count_restricted";
+ 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 String KEY_MIN_QUOTA_CHECK_DELAY_MS = "min_quota_check_delay_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_WINDOW_SIZE_RESTRICTED_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_JOB_COUNT_RESTRICTED = 10;
+ 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_RESTRICTED = 1; // 1/day
+ 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
+ private static final long DEFAULT_MIN_QUOTA_CHECK_DELAY_MS = MINUTE_IN_MILLIS;
+
+ /** 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 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_RESTRICTED_MS = DEFAULT_WINDOW_SIZE_RESTRICTED_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 maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int MAX_JOB_COUNT_RESTRICTED = DEFAULT_MAX_JOB_COUNT_RESTRICTED;
+
+ /** 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 an app can run within this particular
+ * standby bucket's window size.
+ */
+ public int MAX_SESSION_COUNT_RESTRICTED = DEFAULT_MAX_SESSION_COUNT_RESTRICTED;
+
+ /**
+ * 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;
+
+ /** The minimum amount of time between quota check alarms. */
+ public long MIN_QUOTA_CHECK_DELAY_MS = DEFAULT_MIN_QUOTA_CHECK_DELAY_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);
+ WINDOW_SIZE_RESTRICTED_MS = mParser.getDurationMillis(
+ KEY_WINDOW_SIZE_RESTRICTED_MS, DEFAULT_WINDOW_SIZE_RESTRICTED_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);
+ MAX_JOB_COUNT_RESTRICTED = mParser.getInt(
+ KEY_MAX_JOB_COUNT_RESTRICTED, DEFAULT_MAX_JOB_COUNT_RESTRICTED);
+ 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_RESTRICTED = mParser.getInt(
+ KEY_MAX_SESSION_COUNT_RESTRICTED, DEFAULT_MAX_SESSION_COUNT_RESTRICTED);
+ 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);
+ MIN_QUOTA_CHECK_DELAY_MS = mParser.getDurationMillis(KEY_MIN_QUOTA_CHECK_DELAY_MS,
+ DEFAULT_MIN_QUOTA_CHECK_DELAY_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;
+ }
+ // Fit in the range [allowed time (10 mins), 1 week].
+ long newRestrictedPeriodMs = Math.max(mAllowedTimePerPeriodMs,
+ Math.min(7 * 24 * 60 * MINUTE_IN_MILLIS, WINDOW_SIZE_RESTRICTED_MS));
+ if (mBucketPeriodsMs[RESTRICTED_INDEX] != newRestrictedPeriodMs) {
+ mBucketPeriodsMs[RESTRICTED_INDEX] = newRestrictedPeriodMs;
+ 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 newRestrictedMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT,
+ MAX_JOB_COUNT_RESTRICTED);
+ if (mMaxBucketJobCounts[RESTRICTED_INDEX] != newRestrictedMaxJobCount) {
+ mMaxBucketJobCounts[RESTRICTED_INDEX] = newRestrictedMaxJobCount;
+ 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;
+ }
+ int newRestrictedMaxSessionCount = Math.max(0, MAX_SESSION_COUNT_RESTRICTED);
+ if (mMaxBucketSessionCounts[RESTRICTED_INDEX] != newRestrictedMaxSessionCount) {
+ mMaxBucketSessionCounts[RESTRICTED_INDEX] = newRestrictedMaxSessionCount;
+ 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;
+ }
+ // Don't set changed to true for this one since we don't need to re-evaluate
+ // execution stats or constraint status. Limit the delay to the range [0, 15]
+ // minutes.
+ mInQuotaAlarmListener.setMinQuotaCheckDelayMs(
+ Math.min(15 * MINUTE_IN_MILLIS, Math.max(0, MIN_QUOTA_CHECK_DELAY_MS)));
+
+ 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_WINDOW_SIZE_RESTRICTED_MS, WINDOW_SIZE_RESTRICTED_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_MAX_JOB_COUNT_RESTRICTED, MAX_JOB_COUNT_RESTRICTED).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_RESTRICTED, MAX_SESSION_COUNT_RESTRICTED).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.printPair(KEY_MIN_QUOTA_CHECK_DELAY_MS, MIN_QUOTA_CHECK_DELAY_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.RESTRICTED_WINDOW_SIZE_MS,
+ WINDOW_SIZE_RESTRICTED_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.MAX_JOB_COUNT_RESTRICTED,
+ MAX_JOB_COUNT_RESTRICTED);
+ 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_RESTRICTED,
+ MAX_SESSION_COUNT_RESTRICTED);
+ 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.write(ConstantsProto.QuotaController.MIN_QUOTA_CHECK_DELAY_MS,
+ MIN_QUOTA_CHECK_DELAY_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();
+ mInQuotaAlarmListener.dumpLocked(pw);
+ 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);
+ }
+ }
+
+ proto.end(psToken);
+ }
+ }
+
+ mInQuotaAlarmListener.dumpLocked(proto,
+ StateControllerProto.QuotaController.IN_QUOTA_ALARM_LISTENER);
+
+ 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/RestrictingController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/RestrictingController.java
new file mode 100644
index 000000000000..5c637bb92ccc
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/RestrictingController.java
@@ -0,0 +1,41 @@
+/*
+ * 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.controllers;
+
+import com.android.server.job.JobSchedulerService;
+
+/**
+ * Controller that can also handle jobs in the
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
+ */
+public abstract class RestrictingController extends StateController {
+ RestrictingController(JobSchedulerService service) {
+ super(service);
+ }
+
+ /**
+ * Start tracking a job that has been added to the
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
+ */
+ public abstract void startTrackingRestrictedJobLocked(JobStatus jobStatus);
+
+ /**
+ * Stop tracking a job that has been removed from the
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
+ */
+ public abstract void stopTrackingRestrictedJobLocked(JobStatus jobStatus);
+}
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..e2c8f649fdb7
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java
@@ -0,0 +1,238 @@
+/*
+ * 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.app.UiModeManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.PowerManager;
+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;
+ private PowerManager mPowerManager;
+
+ // 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 boolean mInCarMode;
+ 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;
+ mInCarMode = 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);
+ mPowerManager = context.getSystemService(PowerManager.class);
+
+ 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);
+
+ // Car mode
+ filter.addAction(UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED);
+ filter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED);
+
+ 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);
+ pw.print(" mInCarMode: ");
+ pw.println(mInCarMode);
+ }
+
+ @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.write(
+ StateControllerProto.IdleController.IdlenessTracker.DeviceIdlenessTracker.IN_CAR_MODE,
+ mInCarMode);
+
+ proto.end(diToken);
+ proto.end(token);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (DEBUG) {
+ Slog.v(TAG, "Received action: " + action);
+ }
+ switch (action) {
+ case Intent.ACTION_DOCK_ACTIVE:
+ if (!mScreenOn) {
+ // Ignore this intent during screen off
+ return;
+ }
+ // Intentional fallthrough
+ case Intent.ACTION_DREAMING_STOPPED:
+ if (!mPowerManager.isInteractive()) {
+ // Ignore this intent if the device isn't interactive.
+ return;
+ }
+ // Intentional fallthrough
+ case Intent.ACTION_SCREEN_ON:
+ mScreenOn = true;
+ mDockIdle = false;
+ if (DEBUG) {
+ Slog.v(TAG, "exiting idle");
+ }
+ cancelIdlenessCheck();
+ if (mIdle) {
+ mIdle = false;
+ mIdleListener.reportNewIdleState(mIdle);
+ }
+ break;
+ case Intent.ACTION_SCREEN_OFF:
+ case Intent.ACTION_DREAMING_STARTED:
+ case 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;
+ }
+ maybeScheduleIdlenessCheck(action);
+ break;
+ case UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED:
+ mInCarMode = true;
+ cancelIdlenessCheck();
+ if (mIdle) {
+ mIdle = false;
+ mIdleListener.reportNewIdleState(mIdle);
+ }
+ break;
+ case UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED:
+ mInCarMode = false;
+ maybeScheduleIdlenessCheck(action);
+ break;
+ case ActivityManagerService.ACTION_TRIGGER_IDLE:
+ handleIdleTrigger();
+ break;
+ }
+ }
+
+ private void maybeScheduleIdlenessCheck(String reason) {
+ if ((!mScreenOn || mDockIdle) && !mInCarMode) {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final long when = nowElapsed + mInactivityIdleThreshold;
+ if (DEBUG) {
+ Slog.v(TAG, "Scheduling idle : " + reason + " now:" + nowElapsed + " when=" + when);
+ }
+ mAlarm.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ when, mIdleWindowSlop, "JS idleness", mIdleAlarmListener, null);
+ }
+ }
+
+ private void cancelIdlenessCheck() {
+ mAlarm.cancel(mIdleAlarmListener);
+ }
+
+ private void handleIdleTrigger() {
+ // idle time starts now. Do not set mIdle if screen is on.
+ if (!mIdle && (!mScreenOn || mDockIdle) && !mInCarMode) {
+ 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 + " car=" + mInCarMode);
+ }
+ }
+ }
+} \ 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..70155ee84720
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
@@ -0,0 +1,820 @@
+/**
+ * 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_BY_USER;
+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_MASK;
+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_RESTRICTED;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
+
+import static com.android.server.usage.AppStandbyController.isUserUsage;
+
+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.CollectionUtils;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.FrameworkStatsLog;
+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;
+import java.util.List;
+
+/**
+ * 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;
+
+ /**
+ * The bucket beyond which apps are considered idle. Any apps in this bucket or lower are
+ * considered idle while those in higher buckets are not considered idle.
+ */
+ static final int IDLE_BUCKET_CUTOFF = STANDBY_BUCKET_RARE;
+
+ @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 app was last used by the user
+ private static final String ATTR_LAST_USED_BY_USER_ELAPSED = "lastUsedByUserElapsedTime";
+ // 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";
+ // Elapsed timebase time when the app was last marked for restriction.
+ private static final String ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED =
+ "lastRestrictionAttemptElapsedTime";
+ // Reason why the app was last marked for restriction.
+ private static final String ATTR_LAST_RESTRICTION_ATTEMPT_REASON =
+ "lastRestrictionAttemptReason";
+
+ // 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 (including system usage), using elapsed timebase
+ long lastUsedElapsedTime;
+ // Last time the user used the app, using elapsed timebase
+ long lastUsedByUserElapsedTime;
+ // 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;
+ // The last time an agent attempted to put the app into the RESTRICTED bucket.
+ long lastRestrictAttemptElapsedTime;
+ // The last reason the app was marked to be put into the RESTRICTED bucket.
+ int lastRestrictReason;
+ }
+
+ 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 {@code appUsageHistory}
+ */
+ AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName, int userId,
+ int newBucket, int usageReason, long elapsedRealtime, long timeout) {
+ int bucketingReason = REASON_MAIN_USAGE | usageReason;
+ final boolean isUserUsage = isUserUsage(bucketingReason);
+
+ if (appUsageHistory.currentBucket == STANDBY_BUCKET_RESTRICTED && !isUserUsage) {
+ // Only user usage should bring an app out of the RESTRICTED bucket.
+ newBucket = STANDBY_BUCKET_RESTRICTED;
+ bucketingReason = appUsageHistory.bucketingReason;
+ } else {
+ // 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);
+ if (isUserUsage) {
+ appUsageHistory.lastUsedByUserElapsedTime = appUsageHistory.lastUsedElapsedTime;
+ }
+ appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
+ }
+
+ if (appUsageHistory.currentBucket > newBucket) {
+ appUsageHistory.currentBucket = newBucket;
+ logAppStandbyBucketChanged(packageName, userId, newBucket, bucketingReason);
+ }
+ appUsageHistory.bucketingReason = bucketingReason;
+
+ 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, userId, 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 >= IDLE_BUCKET_CUTOFF;
+ }
+
+ 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);
+ final boolean changed = appUsageHistory.currentBucket != bucket;
+ 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 (changed) {
+ logAppStandbyBucketChanged(packageName, userId, bucket, reason);
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Notes an attempt to put the app in the {@link UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
+ * bucket.
+ *
+ * @param packageName The package name of the app that is being restricted
+ * @param userId The ID of the user in which the app is being restricted
+ * @param elapsedRealtime The time the attempt was made, in the (unadjusted) elapsed realtime
+ * timebase
+ * @param reason The reason for the restriction attempt
+ */
+ void noteRestrictionAttempt(String packageName, int userId, long elapsedRealtime, int reason) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory =
+ getPackageHistory(userHistory, packageName, elapsedRealtime, true);
+ appUsageHistory.lastRestrictAttemptElapsedTime = getElapsedTime(elapsedRealtime);
+ appUsageHistory.lastRestrictReason = reason;
+ }
+
+ /**
+ * 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) {
+ final int newBucket;
+ final int reason;
+ if (idle) {
+ newBucket = IDLE_BUCKET_CUTOFF;
+ reason = REASON_MAIN_FORCED_BY_USER;
+ } else {
+ newBucket = STANDBY_BUCKET_ACTIVE;
+ // This is to pretend that the app was just used, don't freeze the state anymore.
+ reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION;
+ }
+ setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket, reason, false);
+
+ return newBucket;
+ }
+
+ 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;
+ }
+
+ /**
+ * Log a standby bucket change to statsd, and also logcat if debug logging is enabled.
+ */
+ private void logAppStandbyBucketChanged(String packageName, int userId, int bucket,
+ int reason) {
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.APP_STANDBY_BUCKET_CHANGED,
+ packageName, userId, bucket,
+ (reason & REASON_MAIN_MASK), (reason & REASON_SUB_MASK));
+ if (DEBUG) {
+ Slog.d(TAG, "Moved " + packageName + " to bucket=" + bucket
+ + ", reason=0x0" + Integer.toHexString(reason));
+ }
+ }
+
+ @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.lastUsedByUserElapsedTime = getLongValue(parser,
+ ATTR_LAST_USED_BY_USER_ELAPSED,
+ appUsageHistory.lastUsedElapsedTime);
+ 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) {
+ Slog.wtf(TAG, "Unable to read bucketing reason", nfe);
+ }
+ }
+ appUsageHistory.lastRestrictAttemptElapsedTime =
+ getLongValue(parser, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED, 0);
+ String lastRestrictReason = parser.getAttributeValue(
+ null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON);
+ if (lastRestrictReason != null) {
+ try {
+ appUsageHistory.lastRestrictReason =
+ Integer.parseInt(lastRestrictReason, 16);
+ } catch (NumberFormatException nfe) {
+ Slog.wtf(TAG, "Unable to read last restrict reason", 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() {
+ final int size = mIdleHistory.size();
+ for (int i = 0; i < size; i++) {
+ writeAppIdleTimes(mIdleHistory.keyAt(i));
+ }
+ }
+
+ 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_LAST_USED_BY_USER_ELAPSED,
+ Long.toString(history.lastUsedByUserElapsedTime));
+ 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));
+ }
+ if (history.lastRestrictAttemptElapsedTime > 0) {
+ xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED,
+ Long.toString(history.lastRestrictAttemptElapsedTime));
+ }
+ xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON,
+ Integer.toHexString(history.lastRestrictReason));
+ 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 dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs) {
+ final int numUsers = userIds.length;
+ for (int i = 0; i < numUsers; i++) {
+ idpw.println();
+ dumpUser(idpw, userIds[i], pkgs);
+ }
+ }
+
+ private void dumpUser(IndentingPrintWriter idpw, int userId, List<String> pkgs) {
+ idpw.print("User ");
+ idpw.print(userId);
+ 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 (!CollectionUtils.isEmpty(pkgs) && !pkgs.contains(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(" usedByUser=");
+ TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedByUserElapsedTime,
+ 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);
+ if (appUsageHistory.lastRestrictAttemptElapsedTime > 0) {
+ idpw.print(" lastRestrictAttempt=");
+ TimeUtils.formatDuration(
+ totalElapsedTime - appUsageHistory.lastRestrictAttemptElapsedTime, idpw);
+ idpw.print(" lastRestrictReason="
+ + UsageStatsManager.reasonToString(appUsageHistory.lastRestrictReason));
+ }
+ 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..0cadbfd23dba
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -0,0 +1,2432 @@
+/**
+ * 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_BY_SYSTEM;
+import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER;
+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_DEFAULT_APP_UPDATE;
+import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY;
+import static android.app.usage.UsageStatsManager.REASON_SUB_MASK;
+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_RESTRICTED;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
+
+import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+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.app.usage.UsageStatsManager.SystemForcedReasons;
+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.CrossProfileAppsInternal;
+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.NetworkScoreManager;
+import android.os.BatteryManager;
+import android.os.BatteryStats;
+import android.os.Build;
+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.Trace;
+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 android.widget.Toast;
+
+import com.android.internal.R;
+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.pm.parsing.pkg.AndroidPackage;
+import com.android.server.usage.AppIdleHistory.AppUsageHistory;
+
+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.Collections;
+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";
+ // Do not submit with true.
+ 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;
+
+ /**
+ * The minimum amount of time the screen must have been on before an app can time out from its
+ * current bucket to the next bucket.
+ */
+ private static final long[] SCREEN_TIME_THRESHOLDS = {
+ 0,
+ 0,
+ COMPRESS_TIME ? 2 * ONE_MINUTE : 1 * ONE_HOUR,
+ COMPRESS_TIME ? 4 * ONE_MINUTE : 2 * ONE_HOUR,
+ COMPRESS_TIME ? 8 * ONE_MINUTE : 6 * ONE_HOUR
+ };
+
+ /** The minimum allowed values for each index in {@link #SCREEN_TIME_THRESHOLDS}. */
+ private static final long[] MINIMUM_SCREEN_TIME_THRESHOLDS = COMPRESS_TIME
+ ? new long[SCREEN_TIME_THRESHOLDS.length]
+ : new long[]{
+ 0,
+ 0,
+ 0,
+ 30 * ONE_MINUTE,
+ ONE_HOUR
+ };
+
+ /**
+ * The minimum amount of elapsed time that must have passed before an app can time out from its
+ * current bucket to the next bucket.
+ */
+ private 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,
+ COMPRESS_TIME ? 32 * ONE_MINUTE : 30 * ONE_DAY
+ };
+
+ /** The minimum allowed values for each index in {@link #ELAPSED_TIME_THRESHOLDS}. */
+ private static final long[] MINIMUM_ELAPSED_TIME_THRESHOLDS = COMPRESS_TIME
+ ? new long[ELAPSED_TIME_THRESHOLDS.length]
+ : new long[]{
+ 0,
+ ONE_HOUR,
+ ONE_HOUR,
+ 2 * ONE_HOUR,
+ 4 * ONE_DAY
+ };
+
+ private static final int[] THRESHOLD_BUCKETS = {
+ STANDBY_BUCKET_ACTIVE,
+ STANDBY_BUCKET_WORKING_SET,
+ STANDBY_BUCKET_FREQUENT,
+ STANDBY_BUCKET_RARE,
+ STANDBY_BUCKET_RESTRICTED
+ };
+
+ /** 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;
+
+ private static final int HEADLESS_APP_CHECK_FLAGS =
+ PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.GET_ACTIVITIES | PackageManager.MATCH_DISABLED_COMPONENTS;
+
+ // 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 final 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<>();
+
+ /**
+ * Set of system apps that are headless (don't have any declared activities, enabled or
+ * disabled). Presence in this map indicates that the app is a headless system app.
+ */
+ @GuardedBy("mHeadlessSystemApps")
+ private final ArraySet<String> mHeadlessSystemApps = new ArraySet<>();
+
+ private final CountDownLatch mAdminDataAvailableLatch = new CountDownLatch(1);
+
+ // Cache the active network scorer queried from the network scorer service
+ private volatile String mCachedNetworkScorer = null;
+ // The last time the network scorer service was queried
+ private volatile long mCachedNetworkScorerAtMillis = 0L;
+ // How long before querying the network scorer again. During this time, subsequent queries will
+ // get the cached value
+ private static final long NETWORK_SCORER_CACHE_DURATION_MILLIS = 5000L;
+
+ // 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_PAROLE_STATE_CHANGED = 9;
+ 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;
+ /**
+ * The minimum amount of time the screen must have been on before an app can time out from its
+ * current bucket to the next bucket.
+ */
+ long[] mAppStandbyScreenThresholds = SCREEN_TIME_THRESHOLDS;
+ /**
+ * The minimum amount of elapsed time that must have passed before an app can time out from its
+ * current bucket to the next bucket.
+ */
+ 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;
+ /**
+ * User usage that would elevate an app's standby bucket will also elevate the standby bucket of
+ * cross profile connected apps. Explicit standby bucket setting via
+ * {@link #setAppStandbyBucket(String, int, int, int, int)} will not be propagated.
+ */
+ boolean mLinkCrossProfileApps;
+ /**
+ * Whether we should allow apps into the
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket or not.
+ * If false, any attempts to put an app into the bucket will put the app into the
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RARE} bucket instead.
+ */
+ private boolean mAllowRestrictedBucket;
+
+ private volatile boolean mAppIdleEnabled;
+ private boolean mIsCharging;
+ 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;
+
+ private AppWidgetManager mAppWidgetManager;
+ 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();
+
+ DeviceStateReceiver deviceStateReceiver = new DeviceStateReceiver();
+ IntentFilter deviceStates = new IntentFilter(BatteryManager.ACTION_CHARGING);
+ deviceStates.addAction(BatteryManager.ACTION_DISCHARGING);
+ deviceStates.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+ mContext.registerReceiver(deviceStateReceiver, deviceStates);
+
+ 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) {
+ synchronized (mAppIdleLock) {
+ if (mAppIdleEnabled != enabled) {
+ final boolean oldParoleState = isInParole();
+ mAppIdleEnabled = enabled;
+ if (isInParole() != oldParoleState) {
+ postParoleStateChanged();
+ }
+ }
+ }
+
+ }
+
+ @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);
+
+ mInjector.registerDisplayListener(mDisplayListener, mHandler);
+ synchronized (mAppIdleLock) {
+ mAppIdleHistory.updateDisplay(isDisplayOn(), mInjector.elapsedRealtime());
+ }
+
+ mSystemServicesReady = true;
+
+ // Offload to handler thread to avoid boot time impact.
+ mHandler.post(AppStandbyController.this::updatePowerWhitelistCache);
+
+ boolean userFileExists;
+ synchronized (mAppIdleLock) {
+ userFileExists = mAppIdleHistory.userFileExists(UserHandle.USER_SYSTEM);
+ }
+
+ if (mPendingInitializeDefaults || !userFileExists) {
+ initializeDefaultsForSystemApps(UserHandle.USER_SYSTEM);
+ }
+
+ if (mPendingOneTimeCheckIdleStates) {
+ postOneTimeCheckIdleStates();
+ }
+ } else if (phase == PHASE_BOOT_COMPLETED) {
+ setChargingState(mInjector.isCharging());
+
+ // Offload to handler thread after boot completed to avoid boot time impact. This means
+ // that headless system apps may be put in a lower bucket until boot has completed.
+ mHandler.post(this::loadHeadlessSystemAppCache);
+ }
+ }
+
+ 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)) {
+ final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName,
+ userId);
+ synchronized (mAppIdleLock) {
+ reportNoninteractiveUsageCrossUserLocked(packageName, userId,
+ STANDBY_BUCKET_ACTIVE, REASON_SUB_USAGE_SYNC_ADAPTER,
+ elapsedRealtime, mSyncAdapterTimeoutMillis, linkedProfiles);
+ }
+ }
+ } 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();
+ final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName, userId);
+ synchronized (mAppIdleLock) {
+ reportNoninteractiveUsageCrossUserLocked(packageName, userId, bucketToPromote,
+ usageReason, elapsedRealtime, durationMillis, linkedProfiles);
+ }
+ }
+
+ 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) {
+ final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName, userId);
+ // Bring the app out of the never bucket
+ reportNoninteractiveUsageCrossUserLocked(packageName, userId,
+ STANDBY_BUCKET_WORKING_SET, REASON_SUB_USAGE_UNEXEMPTED_SYNC_SCHEDULED,
+ elapsedRealtime, mUnexemptedSyncScheduledTimeoutMillis, linkedProfiles);
+ }
+ }
+ }
+
+ private void reportExemptedSyncStart(String packageName, int userId) {
+ if (!mAppIdleEnabled) return;
+
+ final long elapsedRealtime = mInjector.elapsedRealtime();
+ final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName, userId);
+ synchronized (mAppIdleLock) {
+ reportNoninteractiveUsageCrossUserLocked(packageName, userId, STANDBY_BUCKET_ACTIVE,
+ REASON_SUB_USAGE_EXEMPTED_SYNC_START, elapsedRealtime,
+ mExemptedSyncStartTimeoutMillis, linkedProfiles);
+ }
+ }
+
+ /**
+ * Helper method to report indirect user usage of an app and handle reporting the usage
+ * against cross profile connected apps. <br>
+ * Use {@link #reportNoninteractiveUsageLocked(String, int, int, int, long, long)} if
+ * cross profile connected apps do not need to be handled.
+ */
+ private void reportNoninteractiveUsageCrossUserLocked(String packageName, int userId,
+ int bucket, int subReason, long elapsedRealtime, long nextCheckDelay,
+ List<UserHandle> otherProfiles) {
+ reportNoninteractiveUsageLocked(packageName, userId, bucket, subReason, elapsedRealtime,
+ nextCheckDelay);
+ final int size = otherProfiles.size();
+ for (int profileIndex = 0; profileIndex < size; profileIndex++) {
+ final int otherUserId = otherProfiles.get(profileIndex).getIdentifier();
+ reportNoninteractiveUsageLocked(packageName, otherUserId, bucket, subReason,
+ elapsedRealtime, nextCheckDelay);
+ }
+ }
+
+ /**
+ * Helper method to report indirect user usage of an app. <br>
+ * Use
+ * {@link #reportNoninteractiveUsageCrossUserLocked(String, int, int, int, long, long, List)}
+ * if cross profile connected apps need to be handled.
+ */
+ private void reportNoninteractiveUsageLocked(String packageName, int userId, int bucket,
+ int subReason, long elapsedRealtime, long nextCheckDelay) {
+ final AppUsageHistory appUsage = mAppIdleHistory.reportUsage(packageName, userId, bucket,
+ subReason, 0, elapsedRealtime + nextCheckDelay);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, packageName),
+ nextCheckDelay);
+ maybeInformListeners(packageName, userId, elapsedRealtime, appUsage.currentBucket,
+ appUsage.bucketingReason, false);
+ }
+
+ @VisibleForTesting
+ void setChargingState(boolean isCharging) {
+ synchronized (mAppIdleLock) {
+ if (mIsCharging != isCharging) {
+ if (DEBUG) Slog.d(TAG, "Setting mIsCharging to " + isCharging);
+ mIsCharging = isCharging;
+ postParoleStateChanged();
+ }
+ }
+ }
+
+ @Override
+ public boolean isInParole() {
+ return !mAppIdleEnabled || mIsCharging;
+ }
+
+ private void postParoleStateChanged() {
+ if (DEBUG) Slog.d(TAG, "Posting MSG_PAROLE_STATE_CHANGED");
+ mHandler.removeMessages(MSG_PAROLE_STATE_CHANGED);
+ mHandler.sendEmptyMessage(MSG_PAROLE_STATE_CHANGED);
+ }
+
+ @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 int minBucket = getAppMinBucket(packageName,
+ UserHandle.getAppId(uid),
+ userId);
+ if (DEBUG) {
+ Slog.d(TAG, " Checking idle state for " + packageName
+ + " minBucket=" + minBucket);
+ }
+ if (minBucket <= STANDBY_BUCKET_ACTIVE) {
+ // No extra processing needed for ACTIVE or higher since apps can't drop into lower
+ // buckets.
+ synchronized (mAppIdleLock) {
+ mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,
+ minBucket, REASON_MAIN_DEFAULT);
+ }
+ maybeInformListeners(packageName, userId, elapsedRealtime,
+ minBucket, 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_BY_USER) {
+ return;
+ }
+ final int oldBucket = app.currentBucket;
+ if (oldBucket == STANDBY_BUCKET_NEVER) {
+ // None of this should bring an app out of the NEVER bucket.
+ return;
+ }
+ 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 (app.lastRestrictAttemptElapsedTime > app.lastUsedByUserElapsedTime
+ && elapsedTimeAdjusted - app.lastUsedByUserElapsedTime
+ >= mInjector.getAutoRestrictedBucketDelayMs()) {
+ newBucket = STANDBY_BUCKET_RESTRICTED;
+ reason = app.lastRestrictReason;
+ if (DEBUG) {
+ Slog.d(TAG, "Bringing down to RESTRICTED due to timeout");
+ }
+ }
+ if (newBucket == STANDBY_BUCKET_RESTRICTED && !mAllowRestrictedBucket) {
+ newBucket = STANDBY_BUCKET_RARE;
+ // Leave the reason alone.
+ if (DEBUG) {
+ Slog.d(TAG, "Bringing up from RESTRICTED to RARE due to off switch");
+ }
+ }
+ if (newBucket > minBucket) {
+ newBucket = minBucket;
+ // Leave the reason alone.
+ if (DEBUG) {
+ Slog.d(TAG, "Bringing up from " + newBucket + " to " + minBucket
+ + " due to min bucketing");
+ }
+ }
+ 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, int userId) {
+ if (!mAppIdleEnabled) return;
+ final int eventType = event.getEventType();
+ 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 String pkg = event.getPackageName();
+ final List<UserHandle> linkedProfiles = getCrossProfileTargets(pkg, userId);
+ synchronized (mAppIdleLock) {
+ final long elapsedRealtime = mInjector.elapsedRealtime();
+ reportEventLocked(pkg, eventType, elapsedRealtime, userId);
+
+ final int size = linkedProfiles.size();
+ for (int profileIndex = 0; profileIndex < size; profileIndex++) {
+ final int linkedUserId = linkedProfiles.get(profileIndex).getIdentifier();
+ reportEventLocked(pkg, eventType, elapsedRealtime, linkedUserId);
+ }
+ }
+ }
+ }
+
+ private void reportEventLocked(String pkg, int eventType, long elapsedRealtime, int userId) {
+ // 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);
+
+ final AppUsageHistory appHistory = mAppIdleHistory.getAppUsageHistory(
+ pkg, userId, elapsedRealtime);
+ final int prevBucket = appHistory.currentBucket;
+ final int prevBucketReason = appHistory.bucketingReason;
+ final long nextCheckDelay;
+ 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, userId,
+ STANDBY_BUCKET_WORKING_SET, subReason,
+ 0, elapsedRealtime + mNotificationSeenTimeoutMillis);
+ nextCheckDelay = mNotificationSeenTimeoutMillis;
+ } else if (eventType == UsageEvents.Event.SYSTEM_INTERACTION) {
+ mAppIdleHistory.reportUsage(appHistory, pkg, userId,
+ STANDBY_BUCKET_ACTIVE, subReason,
+ 0, elapsedRealtime + mSystemInteractionTimeoutMillis);
+ nextCheckDelay = 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, userId,
+ STANDBY_BUCKET_ACTIVE, subReason,
+ 0, elapsedRealtime + mInitialForegroundServiceStartTimeoutMillis);
+ nextCheckDelay = mInitialForegroundServiceStartTimeoutMillis;
+ } else {
+ mAppIdleHistory.reportUsage(appHistory, pkg, userId,
+ STANDBY_BUCKET_ACTIVE, subReason,
+ elapsedRealtime, elapsedRealtime + mStrongUsageTimeoutMillis);
+ nextCheckDelay = mStrongUsageTimeoutMillis;
+ }
+ if (appHistory.currentBucket != prevBucket) {
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkg),
+ nextCheckDelay);
+ final boolean userStartedInteracting =
+ appHistory.currentBucket == STANDBY_BUCKET_ACTIVE
+ && (prevBucketReason & REASON_MAIN_MASK) != REASON_MAIN_USAGE;
+ maybeInformListeners(pkg, userId, elapsedRealtime,
+ appHistory.currentBucket, reason, userStartedInteracting);
+ }
+
+ if (previouslyIdle) {
+ notifyBatteryStats(pkg, userId, false);
+ }
+ }
+
+ /**
+ * Note: don't call this with the lock held since it makes calls to other system services.
+ */
+ private @NonNull List<UserHandle> getCrossProfileTargets(String pkg, int userId) {
+ synchronized (mAppIdleLock) {
+ if (!mLinkCrossProfileApps) return Collections.emptyList();
+ }
+ return mInjector.getValidCrossProfileTargets(pkg, userId);
+ }
+
+ 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_BY_USER, 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);
+ }
+
+ @StandbyBuckets
+ private int getAppMinBucket(String packageName, int userId) {
+ try {
+ final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
+ return getAppMinBucket(packageName, UserHandle.getAppId(uid), userId);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Not a valid package for this user, nothing to do
+ return STANDBY_BUCKET_NEVER;
+ }
+ }
+
+ /**
+ * Return the lowest bucket this app should ever enter.
+ */
+ @StandbyBuckets
+ private int getAppMinBucket(String packageName, int appId, int userId) {
+ if (packageName == null) return STANDBY_BUCKET_NEVER;
+ // If not enabled at all, of course nobody is ever idle.
+ if (!mAppIdleEnabled) {
+ return STANDBY_BUCKET_EXEMPTED;
+ }
+ if (appId < Process.FIRST_APPLICATION_UID) {
+ // System uids never go idle.
+ return STANDBY_BUCKET_EXEMPTED;
+ }
+ 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 STANDBY_BUCKET_EXEMPTED;
+ }
+ if (mSystemServicesReady) {
+ // 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.isNonIdleWhitelisted(packageName)) {
+ return STANDBY_BUCKET_EXEMPTED;
+ }
+
+ if (isActiveDeviceAdmin(packageName, userId)) {
+ return STANDBY_BUCKET_EXEMPTED;
+ }
+
+ if (isActiveNetworkScorer(packageName)) {
+ return STANDBY_BUCKET_EXEMPTED;
+ }
+
+ if (mAppWidgetManager != null
+ && mInjector.isBoundWidgetPackage(mAppWidgetManager, packageName, userId)) {
+ return STANDBY_BUCKET_ACTIVE;
+ }
+
+ if (isDeviceProvisioningPackage(packageName)) {
+ return STANDBY_BUCKET_EXEMPTED;
+ }
+ }
+
+ // Check this last, as it can be the most expensive check
+ if (isCarrierApp(packageName)) {
+ return STANDBY_BUCKET_EXEMPTED;
+ }
+
+ if (isHeadlessSystemApp(packageName)) {
+ return STANDBY_BUCKET_ACTIVE;
+ }
+
+ return STANDBY_BUCKET_NEVER;
+ }
+
+ private boolean isHeadlessSystemApp(String packageName) {
+ synchronized (mHeadlessSystemApps) {
+ return mHeadlessSystemApps.contains(packageName);
+ }
+ }
+
+ @Override
+ public boolean isAppIdleFiltered(String packageName, int appId, int userId,
+ long elapsedRealtime) {
+ if (getAppMinBucket(packageName, appId, userId) < AppIdleHistory.IDLE_BUCKET_CUTOFF) {
+ return false;
+ } else {
+ synchronized (mAppIdleLock) {
+ if (!mAppIdleEnabled || mIsCharging) {
+ return false;
+ }
+ }
+ return isAppIdleUnfiltered(packageName, userId, elapsedRealtime);
+ }
+ }
+
+ static boolean isUserUsage(int reason) {
+ if ((reason & REASON_MAIN_MASK) == REASON_MAIN_USAGE) {
+ final int subReason = reason & REASON_SUB_MASK;
+ return subReason == REASON_SUB_USAGE_USER_INTERACTION
+ || subReason == REASON_SUB_USAGE_MOVE_TO_FOREGROUND;
+ }
+ return false;
+ }
+
+ @Override
+ public int[] getIdleUidsForUser(int userId) {
+ if (!mAppIdleEnabled) {
+ return new int[0];
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "getIdleUidsForUser");
+
+ 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++;
+ }
+ }
+
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+
+ 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);
+ }
+ }
+
+ @VisibleForTesting
+ int getAppStandbyBucketReason(String packageName, int userId, long elapsedRealtime) {
+ synchronized (mAppIdleLock) {
+ return mAppIdleHistory.getAppStandbyReason(packageName, userId, elapsedRealtime);
+ }
+ }
+
+ @Override
+ public List<AppStandbyInfo> getAppStandbyBuckets(int userId) {
+ synchronized (mAppIdleLock) {
+ return mAppIdleHistory.getAppStandbyBuckets(userId, mAppIdleEnabled);
+ }
+ }
+
+ @Override
+ public void restrictApp(@NonNull String packageName, int userId,
+ @SystemForcedReasons int restrictReason) {
+ // If the package is not installed, don't allow the bucket to be set.
+ if (!mInjector.isPackageInstalled(packageName, 0, userId)) {
+ Slog.e(TAG, "Tried to restrict uninstalled app: " + packageName);
+ return;
+ }
+
+ final int reason = REASON_MAIN_FORCED_BY_SYSTEM | (REASON_SUB_MASK & restrictReason);
+ final long nowElapsed = mInjector.elapsedRealtime();
+ final int bucket = mAllowRestrictedBucket ? STANDBY_BUCKET_RESTRICTED : STANDBY_BUCKET_RARE;
+ setAppStandbyBucket(packageName, userId, bucket, reason, nowElapsed, false);
+ }
+
+ @Override
+ public void setAppStandbyBucket(@NonNull String packageName, int bucket, int userId,
+ int callingUid, int callingPid) {
+ setAppStandbyBuckets(
+ Collections.singletonList(new AppStandbyInfo(packageName, bucket)),
+ userId, callingUid, callingPid);
+ }
+
+ @Override
+ public void setAppStandbyBuckets(@NonNull List<AppStandbyInfo> appBuckets, int userId,
+ int callingUid, int callingPid) {
+ userId = ActivityManager.handleIncomingUser(
+ callingPid, callingUid, userId, false, true, "setAppStandbyBucket", null);
+ final boolean shellCaller = callingUid == Process.ROOT_UID
+ || callingUid == Process.SHELL_UID;
+ final int reason;
+ // The Settings app runs in the system UID but in a separate process. Assume
+ // things coming from other processes are due to the user.
+ if ((UserHandle.isSameApp(callingUid, Process.SYSTEM_UID) && callingPid != Process.myPid())
+ || shellCaller) {
+ reason = REASON_MAIN_FORCED_BY_USER;
+ } else if (UserHandle.isCore(callingUid)) {
+ reason = REASON_MAIN_FORCED_BY_SYSTEM;
+ } else {
+ reason = REASON_MAIN_PREDICTED;
+ }
+ final int packageFlags = PackageManager.MATCH_ANY_USER
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE;
+ final int numApps = appBuckets.size();
+ final long elapsedRealtime = mInjector.elapsedRealtime();
+ for (int i = 0; i < numApps; ++i) {
+ final AppStandbyInfo bucketInfo = appBuckets.get(i);
+ final String packageName = bucketInfo.mPackageName;
+ final int bucket = bucketInfo.mStandbyBucket;
+ if (bucket < STANDBY_BUCKET_ACTIVE || bucket > STANDBY_BUCKET_NEVER) {
+ throw new IllegalArgumentException("Cannot set the standby bucket to " + bucket);
+ }
+ final int packageUid = mInjector.getPackageManagerInternal()
+ .getPackageUid(packageName, packageFlags, userId);
+ // Caller cannot set their own standby state
+ if (packageUid == callingUid) {
+ throw new IllegalArgumentException("Cannot set your own standby bucket");
+ }
+ if (packageUid < 0) {
+ throw new IllegalArgumentException(
+ "Cannot set standby bucket for non existent package (" + packageName + ")");
+ }
+ setAppStandbyBucket(packageName, userId, bucket, reason, elapsedRealtime, shellCaller);
+ }
+ }
+
+ @VisibleForTesting
+ void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket,
+ int reason) {
+ setAppStandbyBucket(
+ packageName, userId, newBucket, reason, mInjector.elapsedRealtime(), false);
+ }
+
+ private void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket,
+ int reason, long elapsedRealtime, boolean resetTimeout) {
+ if (!mAppIdleEnabled) return;
+
+ synchronized (mAppIdleLock) {
+ // If the package is not installed, don't allow the bucket to be set.
+ if (!mInjector.isPackageInstalled(packageName, 0, userId)) {
+ Slog.e(TAG, "Tried to set bucket of uninstalled app: " + packageName);
+ return;
+ }
+ if (newBucket == STANDBY_BUCKET_RESTRICTED && !mAllowRestrictedBucket) {
+ newBucket = STANDBY_BUCKET_RARE;
+ }
+ 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;
+ }
+
+ final boolean wasForcedBySystem =
+ (app.bucketingReason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_SYSTEM;
+
+ // If the bucket was forced, don't allow prediction to override
+ if (predicted
+ && ((app.bucketingReason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_USER
+ || wasForcedBySystem)) {
+ return;
+ }
+
+ final boolean isForcedBySystem =
+ (reason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_SYSTEM;
+
+ if (app.currentBucket == newBucket && wasForcedBySystem && isForcedBySystem) {
+ mAppIdleHistory
+ .noteRestrictionAttempt(packageName, userId, elapsedRealtime, reason);
+ // Keep track of all restricting reasons
+ reason = REASON_MAIN_FORCED_BY_SYSTEM
+ | (app.bucketingReason & REASON_SUB_MASK)
+ | (reason & REASON_SUB_MASK);
+ mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,
+ newBucket, reason, resetTimeout);
+ return;
+ }
+
+ final boolean isForcedByUser =
+ (reason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_USER;
+
+ if (app.currentBucket == STANDBY_BUCKET_RESTRICTED) {
+ if ((app.bucketingReason & REASON_MAIN_MASK) == REASON_MAIN_TIMEOUT) {
+ if (predicted && newBucket >= STANDBY_BUCKET_RARE) {
+ // Predicting into RARE or below means we don't expect the user to use the
+ // app anytime soon, so don't elevate it from RESTRICTED.
+ return;
+ }
+ } else if (!isUserUsage(reason) && !isForcedByUser) {
+ // If the current bucket is RESTRICTED, only user force or usage should bring
+ // it out, unless the app was put into the bucket due to timing out.
+ return;
+ }
+ }
+
+ if (newBucket == STANDBY_BUCKET_RESTRICTED) {
+ mAppIdleHistory
+ .noteRestrictionAttempt(packageName, userId, elapsedRealtime, reason);
+
+ if (isForcedByUser) {
+ // Only user force can bypass the delay restriction. If the user forced the
+ // app into the RESTRICTED bucket, then a toast confirming the action
+ // shouldn't be surprising.
+ if (Build.IS_DEBUGGABLE) {
+ Toast.makeText(mContext,
+ // Since AppStandbyController sits low in the lock hierarchy,
+ // make sure not to call out with the lock held.
+ mHandler.getLooper(),
+ mContext.getResources().getString(
+ R.string.as_app_forced_to_restricted_bucket, packageName),
+ Toast.LENGTH_SHORT)
+ .show();
+ } else {
+ Slog.i(TAG, packageName + " restricted by user");
+ }
+ } else {
+ final long timeUntilRestrictPossibleMs = app.lastUsedByUserElapsedTime
+ + mInjector.getAutoRestrictedBucketDelayMs() - elapsedRealtime;
+ if (timeUntilRestrictPossibleMs > 0) {
+ Slog.w(TAG, "Tried to restrict recently used app: " + packageName
+ + " due to " + reason);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(
+ MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, packageName),
+ timeUntilRestrictPossibleMs);
+ 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");
+ }
+ } else if (newBucket == STANDBY_BUCKET_RARE
+ && mAllowRestrictedBucket
+ && getBucketForLocked(packageName, userId, elapsedRealtime)
+ == STANDBY_BUCKET_RESTRICTED) {
+ // Prediction doesn't think the app will be used anytime soon and
+ // it's been long enough that it could just time out into restricted,
+ // so time it out there instead. Using TIMEOUT will allow prediction
+ // to raise the bucket when it needs to.
+ newBucket = STANDBY_BUCKET_RESTRICTED;
+ reason = REASON_MAIN_TIMEOUT;
+ if (DEBUG) {
+ Slog.d(TAG,
+ "Prediction to RARE overridden by timeout into RESTRICTED");
+ }
+ }
+ }
+
+ // Make sure we don't put the app in a lower bucket than it's supposed to be in.
+ newBucket = Math.min(newBucket, getAppMinBucket(packageName, userId));
+ 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.getCarrierPrivilegedPackagesForAllActiveSubscriptions();
+ mHaveCarrierPrivilegedApps = true;
+ if (DEBUG) {
+ Slog.d(TAG, "apps with carrier privilege " + mCarrierPrivilegedApps);
+ }
+ }
+
+ private boolean isActiveNetworkScorer(String packageName) {
+ // Validity of network scorer cache is limited to a few seconds. Fetch it again
+ // if longer since query.
+ // This is a temporary optimization until there's a callback mechanism for changes to network scorer.
+ final long now = SystemClock.elapsedRealtime();
+ if (mCachedNetworkScorer == null
+ || mCachedNetworkScorerAtMillis < now - NETWORK_SCORER_CACHE_DURATION_MILLIS) {
+ mCachedNetworkScorer = mInjector.getActiveNetworkScorer();
+ mCachedNetworkScorerAtMillis = now;
+ }
+ return packageName != null && packageName.equals(mCachedNetworkScorer);
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+
+ private void informParoleStateChanged() {
+ final boolean paroled = isInParole();
+ synchronized (mPackageAccessListeners) {
+ for (AppIdleStateChangeListener listener : mPackageAccessListeners) {
+ listener.onParoleStateChanged(paroled);
+ }
+ }
+ }
+
+
+ @Override
+ public void flushToDisk() {
+ synchronized (mAppIdleLock) {
+ mAppIdleHistory.writeAppIdleTimes();
+ mAppIdleHistory.writeAppIdleDurations();
+ }
+ }
+
+ private boolean isDisplayOn() {
+ return mInjector.isDefaultDisplayOn();
+ }
+
+ @VisibleForTesting
+ void clearAppIdleForPackage(String packageName, int userId) {
+ synchronized (mAppIdleLock) {
+ mAppIdleHistory.clearUsage(packageName, userId);
+ }
+ }
+
+ /**
+ * Remove an app from the {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
+ * bucket if it was forced into the bucket by the system because it was buggy.
+ */
+ @VisibleForTesting
+ void maybeUnrestrictBuggyApp(String packageName, int userId) {
+ synchronized (mAppIdleLock) {
+ final long elapsedRealtime = mInjector.elapsedRealtime();
+ final AppIdleHistory.AppUsageHistory app =
+ mAppIdleHistory.getAppUsageHistory(packageName, userId, elapsedRealtime);
+ if (app.currentBucket != STANDBY_BUCKET_RESTRICTED
+ || (app.bucketingReason & REASON_MAIN_MASK) != REASON_MAIN_FORCED_BY_SYSTEM) {
+ return;
+ }
+
+ final int newBucket;
+ final int newReason;
+ if ((app.bucketingReason & REASON_SUB_MASK) == REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY) {
+ // If bugginess was the only reason the app should be restricted, then lift it out.
+ newBucket = STANDBY_BUCKET_RARE;
+ newReason = REASON_MAIN_DEFAULT | REASON_SUB_DEFAULT_APP_UPDATE;
+ } else {
+ // There's another reason the app was restricted. Remove the buggy bit and call
+ // it a day.
+ newBucket = STANDBY_BUCKET_RESTRICTED;
+ newReason = app.bucketingReason & ~REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY;
+ }
+ mAppIdleHistory.setAppStandbyBucket(
+ packageName, userId, elapsedRealtime, newBucket, newReason);
+ }
+ }
+
+ private void updatePowerWhitelistCache() {
+ if (mInjector.getBootPhase() < PHASE_SYSTEM_SERVICES_READY) {
+ return;
+ }
+ mInjector.updatePowerWhitelistCache();
+ postCheckIdleStates(UserHandle.USER_ALL);
+ }
+
+ private class PackageReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ final String pkgName = intent.getData().getSchemeSpecificPart();
+ final int userId = getSendingUserId();
+ if (Intent.ACTION_PACKAGE_ADDED.equals(action)
+ || Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+ clearCarrierPrivilegedApps();
+ // ACTION_PACKAGE_ADDED is called even for system app downgrades.
+ evaluateSystemAppException(pkgName, userId);
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkgName)
+ .sendToTarget();
+ }
+ if ((Intent.ACTION_PACKAGE_REMOVED.equals(action) ||
+ Intent.ACTION_PACKAGE_ADDED.equals(action))) {
+ if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ maybeUnrestrictBuggyApp(pkgName, userId);
+ } else {
+ clearAppIdleForPackage(pkgName, userId);
+ }
+ }
+ }
+ }
+
+ private void evaluateSystemAppException(String packageName, int userId) {
+ if (!mSystemServicesReady) {
+ // The app will be evaluated in when services are ready.
+ return;
+ }
+ try {
+ PackageInfo pi = mPackageManager.getPackageInfoAsUser(
+ packageName, HEADLESS_APP_CHECK_FLAGS, userId);
+ evaluateSystemAppException(pi);
+ } catch (PackageManager.NameNotFoundException e) {
+ synchronized (mHeadlessSystemApps) {
+ mHeadlessSystemApps.remove(packageName);
+ }
+ }
+ }
+
+ /** Returns true if the exception status changed. */
+ private boolean evaluateSystemAppException(@Nullable PackageInfo pkgInfo) {
+ if (pkgInfo == null || pkgInfo.applicationInfo == null
+ || (!pkgInfo.applicationInfo.isSystemApp()
+ && !pkgInfo.applicationInfo.isUpdatedSystemApp())) {
+ return false;
+ }
+ synchronized (mHeadlessSystemApps) {
+ if (pkgInfo.activities == null || pkgInfo.activities.length == 0) {
+ // Headless system app.
+ return mHeadlessSystemApps.add(pkgInfo.packageName);
+ } else {
+ return mHeadlessSystemApps.remove(pkgInfo.packageName);
+ }
+ }
+ }
+
+ /** Call on a system version update to temporarily reset system app buckets. */
+ @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);
+ }
+ }
+
+ /** Call on system boot to get the initial set of headless system apps. */
+ private void loadHeadlessSystemAppCache() {
+ Slog.d(TAG, "Loading headless system app cache. appIdleEnabled=" + mAppIdleEnabled);
+ final List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(
+ HEADLESS_APP_CHECK_FLAGS, UserHandle.USER_SYSTEM);
+ final int packageCount = packages.size();
+ for (int i = 0; i < packageCount; i++) {
+ PackageInfo pkgInfo = packages.get(i);
+ if (pkgInfo != null && evaluateSystemAppException(pkgInfo)) {
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE,
+ UserHandle.USER_SYSTEM, -1, pkgInfo.packageName)
+ .sendToTarget();
+ }
+ }
+ }
+
+ @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 dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs) {
+ synchronized (mAppIdleLock) {
+ mAppIdleHistory.dumpUsers(idpw, userIds, pkgs);
+ }
+ }
+
+ @Override
+ public void dumpState(String[] args, PrintWriter pw) {
+ synchronized (mAppIdleLock) {
+ pw.println("Carrier privileged apps (have=" + mHaveCarrierPrivilegedApps
+ + "): " + mCarrierPrivilegedApps);
+ }
+
+ 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.print(" mAllowRestrictedBucket=");
+ pw.print(mAllowRestrictedBucket);
+ pw.print(" mIsCharging=");
+ pw.print(mIsCharging);
+ pw.println();
+ pw.print("mScreenThresholds="); pw.println(Arrays.toString(mAppStandbyScreenThresholds));
+ pw.print("mElapsedThresholds="); pw.println(Arrays.toString(mAppStandbyElapsedThresholds));
+ pw.println();
+
+ pw.println("mHeadlessSystemApps=[");
+ synchronized (mHeadlessSystemApps) {
+ for (int i = mHeadlessSystemApps.size() - 1; i >= 0; --i) {
+ pw.print(" ");
+ pw.print(mHeadlessSystemApps.valueAt(i));
+ pw.println(",");
+ }
+ }
+ pw.println("]");
+ pw.println();
+
+ mInjector.dump(pw);
+ }
+
+ /**
+ * 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 IBatteryStats mBatteryStats;
+ private BatteryManager mBatteryManager;
+ private PackageManagerInternal mPackageManagerInternal;
+ private DisplayManager mDisplayManager;
+ private PowerManager mPowerManager;
+ private IDeviceIdleController mDeviceIdleController;
+ private CrossProfileAppsInternal mCrossProfileAppsInternal;
+ int mBootPhase;
+ /**
+ * The minimum amount of time required since the last user interaction before an app can be
+ * automatically placed in the RESTRICTED bucket.
+ */
+ long mAutoRestrictedBucketDelayMs = ONE_DAY;
+ /**
+ * Cached set of apps that are power whitelisted, including those not whitelisted from idle.
+ */
+ @GuardedBy("mPowerWhitelistedApps")
+ private final ArraySet<String> mPowerWhitelistedApps = new ArraySet<>();
+
+ 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);
+ mBatteryManager = mContext.getSystemService(BatteryManager.class);
+ mCrossProfileAppsInternal = LocalServices.getService(
+ CrossProfileAppsInternal.class);
+
+ final ActivityManager activityManager =
+ (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+ if (activityManager.isLowRamDevice() || ActivityManager.isSmallBatteryDevice()) {
+ mAutoRestrictedBucketDelayMs = 12 * ONE_HOUR;
+ }
+ }
+ 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 isCharging() {
+ return mBatteryManager.isCharging();
+ }
+
+ boolean isNonIdleWhitelisted(String packageName) {
+ if (mBootPhase < PHASE_SYSTEM_SERVICES_READY) {
+ return false;
+ }
+ synchronized (mPowerWhitelistedApps) {
+ return mPowerWhitelistedApps.contains(packageName);
+ }
+ }
+
+ void updatePowerWhitelistCache() {
+ try {
+ // Don't call out to DeviceIdleController with the lock held.
+ final String[] whitelistedPkgs =
+ mDeviceIdleController.getFullPowerWhitelistExceptIdle();
+ synchronized (mPowerWhitelistedApps) {
+ mPowerWhitelistedApps.clear();
+ final int len = whitelistedPkgs.length;
+ for (int i = 0; i < len; ++i) {
+ mPowerWhitelistedApps.add(whitelistedPkgs[i]);
+ }
+ }
+ } catch (RemoteException e) {
+ // Should not happen.
+ Slog.wtf(TAG, "Failed to get power whitelist", e);
+ }
+ }
+
+ boolean isRestrictedBucketEnabled() {
+ return Global.getInt(mContext.getContentResolver(),
+ Global.ENABLE_RESTRICTED_BUCKET,
+ Global.DEFAULT_ENABLE_RESTRICTED_BUCKET) == 1;
+ }
+
+ File getDataSystemDirectory() {
+ return Environment.getDataSystemDirectory();
+ }
+
+ /**
+ * Return the minimum amount of time that must have passed since the last user usage before
+ * an app can be automatically put into the
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
+ */
+ long getAutoRestrictedBucketDelayMs() {
+ return mAutoRestrictedBucketDelayMs;
+ }
+
+ void noteEvent(int event, String packageName, int uid) throws RemoteException {
+ mBatteryStats.noteEvent(event, packageName, uid);
+ }
+
+ PackageManagerInternal getPackageManagerInternal() {
+ return mPackageManagerInternal;
+ }
+
+ 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();
+ }
+
+ public List<UserHandle> getValidCrossProfileTargets(String pkg, int userId) {
+ final int uid = mPackageManagerInternal.getPackageUidInternal(pkg, 0, userId);
+ final AndroidPackage aPkg = mPackageManagerInternal.getPackage(uid);
+ if (uid < 0
+ || aPkg == null
+ || !aPkg.isCrossProfile()
+ || !mCrossProfileAppsInternal
+ .verifyUidHasInteractAcrossProfilePermission(pkg, uid)) {
+ if (uid >= 0 && aPkg == null) {
+ Slog.wtf(TAG, "Null package retrieved for UID " + uid);
+ }
+ return Collections.emptyList();
+ }
+ return mCrossProfileAppsInternal.getTargetUserProfiles(pkg, userId);
+ }
+
+ void dump(PrintWriter pw) {
+ pw.println("mPowerWhitelistedApps=[");
+ synchronized (mPowerWhitelistedApps) {
+ for (int i = mPowerWhitelistedApps.size() - 1; i >= 0; --i) {
+ pw.print(" ");
+ pw.print(mPowerWhitelistedApps.valueAt(i));
+ pw.println(",");
+ }
+ }
+ pw.println("]");
+ pw.println();
+ }
+ }
+
+ 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_PAROLE_STATE_CHANGED:
+ if (DEBUG) Slog.d(TAG, "Parole state: " + isInParole());
+ informParoleStateChanged();
+ 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.arg2 > 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 class DeviceStateReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case BatteryManager.ACTION_CHARGING:
+ setChargingState(true);
+ break;
+ case BatteryManager.ACTION_DISCHARGING:
+ setChargingState(false);
+ break;
+ case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
+ if (mSystemServicesReady) {
+ mHandler.post(AppStandbyController.this::updatePowerWhitelistCache);
+ }
+ break;
+ }
+ }
+ }
+
+ 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";
+ private static final String KEY_AUTO_RESTRICTED_BUCKET_DELAY_MS =
+ "auto_restricted_bucket_delay_ms";
+ private static final String KEY_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS =
+ "cross_profile_apps_share_standby_buckets";
+ 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;
+ public static final long DEFAULT_AUTO_RESTRICTED_BUCKET_DELAY_MS = ONE_DAY;
+ public static final boolean DEFAULT_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS = true;
+
+ 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.ENABLE_RESTRICTED_BUCKET),
+ 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, MINIMUM_SCREEN_TIME_THRESHOLDS);
+
+ String elapsedThresholdsValue = mParser.getString(KEY_ELAPSED_TIME_THRESHOLDS,
+ null);
+ mAppStandbyElapsedThresholds = parseLongArray(elapsedThresholdsValue,
+ ELAPSED_TIME_THRESHOLDS, MINIMUM_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);
+
+ 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);
+
+ mInjector.mAutoRestrictedBucketDelayMs = Math.max(
+ COMPRESS_TIME ? ONE_MINUTE : 2 * ONE_HOUR,
+ mParser.getDurationMillis(KEY_AUTO_RESTRICTED_BUCKET_DELAY_MS,
+ COMPRESS_TIME
+ ? ONE_MINUTE : DEFAULT_AUTO_RESTRICTED_BUCKET_DELAY_MS));
+
+ mLinkCrossProfileApps = mParser.getBoolean(
+ KEY_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS,
+ DEFAULT_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS);
+
+ mAllowRestrictedBucket = mInjector.isRestrictedBucketEnabled();
+ }
+
+ // 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, long[] minValues) {
+ if (values == null) return defaults;
+ if (values.isEmpty()) {
+ // Reset to defaults
+ return defaults;
+ } else {
+ String[] thresholds = values.split("/");
+ if (thresholds.length == THRESHOLD_BUCKETS.length) {
+ if (minValues.length != THRESHOLD_BUCKETS.length) {
+ Slog.wtf(TAG, "minValues array is the wrong size");
+ // Use zeroes as the minimums.
+ minValues = new long[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] = Math.max(minValues[i],
+ Duration.parse(thresholds[i]).toMillis());
+ } else {
+ array[i] = Math.max(minValues[i], Long.parseLong(thresholds[i]));
+ }
+ } catch (NumberFormatException|DateTimeParseException e) {
+ return defaults;
+ }
+ }
+ return array;
+ } else {
+ return defaults;
+ }
+ }
+ }
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING
new file mode 100644
index 000000000000..c5dc51cc9c24
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING
@@ -0,0 +1,31 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsUsageStatsTestCases",
+ "options": [
+ {"include-filter": "android.app.usage.cts.UsageStatsTest"},
+ {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"}
+ ]
+ },
+ {
+ "name": "FrameworksServicesTests",
+ "options": [
+ {"include-filter": "com.android.server.usage"},
+ {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"}
+ ]
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "CtsUsageStatsTestCases"
+ },
+ {
+ "name": "FrameworksServicesTests",
+ "options": [
+ {"include-filter": "com.android.server.usage"}
+ ]
+ }
+ ]
+} \ No newline at end of file
diff --git a/apex/media/OWNERS b/apex/media/OWNERS
new file mode 100644
index 000000000000..9b853c5dd7d8
--- /dev/null
+++ b/apex/media/OWNERS
@@ -0,0 +1,4 @@
+andrewlewis@google.com
+aquilescanta@google.com
+marcone@google.com
+sungsoo@google.com
diff --git a/apex/media/aidl/Android.bp b/apex/media/aidl/Android.bp
new file mode 100644
index 000000000000..409a04897f56
--- /dev/null
+++ b/apex/media/aidl/Android.bp
@@ -0,0 +1,35 @@
+//
+// Copyright 2020 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: "stable-mediasession2-aidl-srcs",
+ srcs: ["stable/**/*.aidl"],
+ path: "stable",
+}
+
+filegroup {
+ name: "private-mediasession2-aidl-srcs",
+ srcs: ["private/**/I*.aidl"],
+ path: "private",
+}
+
+filegroup {
+ name: "mediasession2-aidl-srcs",
+ srcs: [
+ ":private-mediasession2-aidl-srcs",
+ ":stable-mediasession2-aidl-srcs",
+ ],
+}
diff --git a/apex/media/aidl/private/android/media/Controller2Link.aidl b/apex/media/aidl/private/android/media/Controller2Link.aidl
new file mode 100644
index 000000000000..64edafcb11fc
--- /dev/null
+++ b/apex/media/aidl/private/android/media/Controller2Link.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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.media;
+
+parcelable Controller2Link;
diff --git a/apex/media/aidl/private/android/media/IMediaController2.aidl b/apex/media/aidl/private/android/media/IMediaController2.aidl
new file mode 100644
index 000000000000..42c6e70529ec
--- /dev/null
+++ b/apex/media/aidl/private/android/media/IMediaController2.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright 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.media;
+
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.media.Session2Command;
+
+/**
+ * Interface from MediaSession2 to MediaController2.
+ * <p>
+ * Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
+ * and holds calls from session to make session owner(s) frozen.
+ * @hide
+ */
+ // Code for AML only
+oneway interface IMediaController2 {
+ void notifyConnected(int seq, in Bundle connectionResult) = 0;
+ void notifyDisconnected(int seq) = 1;
+ void notifyPlaybackActiveChanged(int seq, boolean playbackActive) = 2;
+ void sendSessionCommand(int seq, in Session2Command command, in Bundle args,
+ in ResultReceiver resultReceiver) = 3;
+ void cancelSessionCommand(int seq) = 4;
+ // Next Id : 5
+}
diff --git a/apex/media/aidl/private/android/media/IMediaSession2.aidl b/apex/media/aidl/private/android/media/IMediaSession2.aidl
new file mode 100644
index 000000000000..26e717b39afc
--- /dev/null
+++ b/apex/media/aidl/private/android/media/IMediaSession2.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright 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.media;
+
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.media.Controller2Link;
+import android.media.Session2Command;
+
+/**
+ * Interface from MediaController2 to MediaSession2.
+ * <p>
+ * Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
+ * and holds calls from session to make session owner(s) frozen.
+ * @hide
+ */
+ // Code for AML only
+oneway interface IMediaSession2 {
+ void connect(in Controller2Link caller, int seq, in Bundle connectionRequest) = 0;
+ void disconnect(in Controller2Link caller, int seq) = 1;
+ void sendSessionCommand(in Controller2Link caller, int seq, in Session2Command sessionCommand,
+ in Bundle args, in ResultReceiver resultReceiver) = 2;
+ void cancelSessionCommand(in Controller2Link caller, int seq) = 3;
+ // Next Id : 4
+}
diff --git a/apex/media/aidl/private/android/media/IMediaSession2Service.aidl b/apex/media/aidl/private/android/media/IMediaSession2Service.aidl
new file mode 100644
index 000000000000..10ac1be0a36e
--- /dev/null
+++ b/apex/media/aidl/private/android/media/IMediaSession2Service.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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.media;
+
+import android.os.Bundle;
+import android.media.Controller2Link;
+
+/**
+ * Interface from MediaController2 to MediaSession2Service.
+ * <p>
+ * Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
+ * and holds calls from controller to make controller owner(s) frozen.
+ * @hide
+ */
+oneway interface IMediaSession2Service {
+ void connect(in Controller2Link caller, int seq, in Bundle connectionRequest) = 0;
+ // Next Id : 1
+}
diff --git a/apex/media/aidl/private/android/media/Session2Command.aidl b/apex/media/aidl/private/android/media/Session2Command.aidl
new file mode 100644
index 000000000000..43a7b123ed29
--- /dev/null
+++ b/apex/media/aidl/private/android/media/Session2Command.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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.media;
+
+parcelable Session2Command;
diff --git a/apex/media/aidl/stable/android/media/Session2Token.aidl b/apex/media/aidl/stable/android/media/Session2Token.aidl
new file mode 100644
index 000000000000..c5980e9e77fd
--- /dev/null
+++ b/apex/media/aidl/stable/android/media/Session2Token.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.media;
+
+parcelable Session2Token;
diff --git a/apex/media/framework/Android.bp b/apex/media/framework/Android.bp
new file mode 100644
index 000000000000..ce4b030467a7
--- /dev/null
+++ b/apex/media/framework/Android.bp
@@ -0,0 +1,110 @@
+// Copyright (C) 2020 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: "updatable-media",
+
+ srcs: [
+ ":updatable-media-srcs",
+ ],
+
+ permitted_packages: [
+ "android.media",
+ ],
+
+ optimize: {
+ enabled: true,
+ shrink: true,
+ proguard_flags_files: ["updatable-media-proguard.flags"],
+ },
+
+ installable: true,
+
+ sdk_version: "module_current",
+ libs: [
+ "framework_media_annotation",
+ ],
+
+ static_libs: [
+ "exoplayer2-extractor"
+ ],
+ jarjar_rules: "jarjar_rules.txt",
+
+ plugins: ["java_api_finder"],
+
+ hostdex: true, // for hiddenapi check
+ apex_available: [
+ "com.android.media",
+ "test_com.android.media",
+ ],
+ min_sdk_version: "29",
+}
+
+filegroup {
+ name: "updatable-media-srcs",
+ srcs: [
+ ":mediaparser-srcs",
+ ":mediasession2-java-srcs",
+ ":mediasession2-aidl-srcs",
+ ],
+}
+
+filegroup {
+ name: "mediasession2-java-srcs",
+ srcs: [
+ "java/android/media/Controller2Link.java",
+ "java/android/media/MediaConstants.java",
+ "java/android/media/MediaController2.java",
+ "java/android/media/MediaSession2.java",
+ "java/android/media/MediaSession2Service.java",
+ "java/android/media/Session2Command.java",
+ "java/android/media/Session2CommandGroup.java",
+ "java/android/media/Session2Link.java",
+ "java/android/media/Session2Token.java",
+ ],
+ path: "java",
+}
+
+filegroup {
+ name: "mediaparser-srcs",
+ srcs: [
+ "java/android/media/MediaParser.java"
+ ],
+ path: "java",
+}
+
+java_sdk_library {
+ name: "framework-media",
+ defaults: ["framework-module-defaults"],
+
+ // This is only used to define the APIs for updatable-media.
+ api_only: true,
+
+ srcs: [
+ ":updatable-media-srcs",
+ ],
+
+ libs: [
+ "framework_media_annotation",
+ ],
+ impl_library_visibility: ["//frameworks/av/apex:__subpackages__"],
+}
+
+
+java_library {
+ name: "framework_media_annotation",
+ srcs: [":framework-media-annotation-srcs"],
+ installable: false,
+ sdk_version: "core_current",
+}
diff --git a/apex/media/framework/TEST_MAPPING b/apex/media/framework/TEST_MAPPING
new file mode 100644
index 000000000000..ec2d2e2e756c
--- /dev/null
+++ b/apex/media/framework/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsMediaParserTestCases"
+ }
+ ]
+}
diff --git a/apex/media/framework/api/current.txt b/apex/media/framework/api/current.txt
new file mode 100644
index 000000000000..0cc8e52f6411
--- /dev/null
+++ b/apex/media/framework/api/current.txt
@@ -0,0 +1,226 @@
+// Signature format: 2.0
+package android.media {
+
+ public class MediaController2 implements java.lang.AutoCloseable {
+ method public void cancelSessionCommand(@NonNull Object);
+ method public void close();
+ method @Nullable public android.media.Session2Token getConnectedToken();
+ method public boolean isPlaybackActive();
+ method @NonNull public Object sendSessionCommand(@NonNull android.media.Session2Command, @Nullable android.os.Bundle);
+ }
+
+ public static final class MediaController2.Builder {
+ ctor public MediaController2.Builder(@NonNull android.content.Context, @NonNull android.media.Session2Token);
+ method @NonNull public android.media.MediaController2 build();
+ method @NonNull public android.media.MediaController2.Builder setConnectionHints(@NonNull android.os.Bundle);
+ method @NonNull public android.media.MediaController2.Builder setControllerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaController2.ControllerCallback);
+ }
+
+ public abstract static class MediaController2.ControllerCallback {
+ ctor public MediaController2.ControllerCallback();
+ method public void onCommandResult(@NonNull android.media.MediaController2, @NonNull Object, @NonNull android.media.Session2Command, @NonNull android.media.Session2Command.Result);
+ method public void onConnected(@NonNull android.media.MediaController2, @NonNull android.media.Session2CommandGroup);
+ method public void onDisconnected(@NonNull android.media.MediaController2);
+ method public void onPlaybackActiveChanged(@NonNull android.media.MediaController2, boolean);
+ method @Nullable public android.media.Session2Command.Result onSessionCommand(@NonNull android.media.MediaController2, @NonNull android.media.Session2Command, @Nullable android.os.Bundle);
+ }
+
+ public final class MediaParser {
+ method public boolean advance(@NonNull android.media.MediaParser.SeekableInputReader) throws java.io.IOException;
+ method @NonNull public static android.media.MediaParser create(@NonNull android.media.MediaParser.OutputConsumer, @NonNull java.lang.String...);
+ method @NonNull public static android.media.MediaParser createByName(@NonNull String, @NonNull android.media.MediaParser.OutputConsumer);
+ method @NonNull public String getParserName();
+ method @NonNull public static java.util.List<java.lang.String> getParserNames(@NonNull android.media.MediaFormat);
+ method public void release();
+ method public void seek(@NonNull android.media.MediaParser.SeekPoint);
+ method @NonNull public android.media.MediaParser setParameter(@NonNull String, @NonNull Object);
+ method public boolean supportsParameter(@NonNull String);
+ field public static final String PARAMETER_ADTS_ENABLE_CBR_SEEKING = "android.media.mediaparser.adts.enableCbrSeeking";
+ field public static final String PARAMETER_AMR_ENABLE_CBR_SEEKING = "android.media.mediaparser.amr.enableCbrSeeking";
+ field public static final String PARAMETER_FLAC_DISABLE_ID3 = "android.media.mediaparser.flac.disableId3";
+ field public static final String PARAMETER_MATROSKA_DISABLE_CUES_SEEKING = "android.media.mediaparser.matroska.disableCuesSeeking";
+ field public static final String PARAMETER_MP3_DISABLE_ID3 = "android.media.mediaparser.mp3.disableId3";
+ field public static final String PARAMETER_MP3_ENABLE_CBR_SEEKING = "android.media.mediaparser.mp3.enableCbrSeeking";
+ field public static final String PARAMETER_MP3_ENABLE_INDEX_SEEKING = "android.media.mediaparser.mp3.enableIndexSeeking";
+ field public static final String PARAMETER_MP4_IGNORE_EDIT_LISTS = "android.media.mediaparser.mp4.ignoreEditLists";
+ field public static final String PARAMETER_MP4_IGNORE_TFDT_BOX = "android.media.mediaparser.mp4.ignoreTfdtBox";
+ field public static final String PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES = "android.media.mediaparser.mp4.treatVideoFramesAsKeyframes";
+ field public static final String PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES = "android.media.mediaparser.ts.allowNonIdrAvcKeyframes";
+ field public static final String PARAMETER_TS_DETECT_ACCESS_UNITS = "android.media.mediaparser.ts.ignoreDetectAccessUnits";
+ field public static final String PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS = "android.media.mediaparser.ts.enableHdmvDtsAudioStreams";
+ field public static final String PARAMETER_TS_IGNORE_AAC_STREAM = "android.media.mediaparser.ts.ignoreAacStream";
+ field public static final String PARAMETER_TS_IGNORE_AVC_STREAM = "android.media.mediaparser.ts.ignoreAvcStream";
+ field public static final String PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM = "android.media.mediaparser.ts.ignoreSpliceInfoStream";
+ field public static final String PARAMETER_TS_MODE = "android.media.mediaparser.ts.mode";
+ field public static final String PARSER_NAME_AC3 = "android.media.mediaparser.Ac3Parser";
+ field public static final String PARSER_NAME_AC4 = "android.media.mediaparser.Ac4Parser";
+ field public static final String PARSER_NAME_ADTS = "android.media.mediaparser.AdtsParser";
+ field public static final String PARSER_NAME_AMR = "android.media.mediaparser.AmrParser";
+ field public static final String PARSER_NAME_FLAC = "android.media.mediaparser.FlacParser";
+ field public static final String PARSER_NAME_FLV = "android.media.mediaparser.FlvParser";
+ field public static final String PARSER_NAME_FMP4 = "android.media.mediaparser.FragmentedMp4Parser";
+ field public static final String PARSER_NAME_MATROSKA = "android.media.mediaparser.MatroskaParser";
+ field public static final String PARSER_NAME_MP3 = "android.media.mediaparser.Mp3Parser";
+ field public static final String PARSER_NAME_MP4 = "android.media.mediaparser.Mp4Parser";
+ field public static final String PARSER_NAME_OGG = "android.media.mediaparser.OggParser";
+ field public static final String PARSER_NAME_PS = "android.media.mediaparser.PsParser";
+ field public static final String PARSER_NAME_TS = "android.media.mediaparser.TsParser";
+ field public static final String PARSER_NAME_UNKNOWN = "android.media.mediaparser.UNKNOWN";
+ field public static final String PARSER_NAME_WAV = "android.media.mediaparser.WavParser";
+ field public static final int SAMPLE_FLAG_DECODE_ONLY = -2147483648; // 0x80000000
+ field public static final int SAMPLE_FLAG_ENCRYPTED = 1073741824; // 0x40000000
+ field public static final int SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA = 268435456; // 0x10000000
+ field public static final int SAMPLE_FLAG_KEY_FRAME = 1; // 0x1
+ field public static final int SAMPLE_FLAG_LAST_SAMPLE = 536870912; // 0x20000000
+ }
+
+ public static interface MediaParser.InputReader {
+ method public long getLength();
+ method public long getPosition();
+ method public int read(@NonNull byte[], int, int) throws java.io.IOException;
+ }
+
+ public static interface MediaParser.OutputConsumer {
+ method public void onSampleCompleted(int, long, int, int, int, @Nullable android.media.MediaCodec.CryptoInfo);
+ method public void onSampleDataFound(int, @NonNull android.media.MediaParser.InputReader) throws java.io.IOException;
+ method public void onSeekMapFound(@NonNull android.media.MediaParser.SeekMap);
+ method public void onTrackCountFound(int);
+ method public void onTrackDataFound(int, @NonNull android.media.MediaParser.TrackData);
+ }
+
+ public static final class MediaParser.ParsingException extends java.io.IOException {
+ }
+
+ public static final class MediaParser.SeekMap {
+ method public long getDurationMicros();
+ method @NonNull public android.util.Pair<android.media.MediaParser.SeekPoint,android.media.MediaParser.SeekPoint> getSeekPoints(long);
+ method public boolean isSeekable();
+ field public static final int UNKNOWN_DURATION = -2147483648; // 0x80000000
+ }
+
+ public static final class MediaParser.SeekPoint {
+ field @NonNull public static final android.media.MediaParser.SeekPoint START;
+ field public final long position;
+ field public final long timeMicros;
+ }
+
+ public static interface MediaParser.SeekableInputReader extends android.media.MediaParser.InputReader {
+ method public void seekToPosition(long);
+ }
+
+ public static final class MediaParser.TrackData {
+ field @Nullable public final android.media.DrmInitData drmInitData;
+ field @NonNull public final android.media.MediaFormat mediaFormat;
+ }
+
+ public static final class MediaParser.UnrecognizedInputFormatException extends java.io.IOException {
+ }
+
+ public class MediaSession2 implements java.lang.AutoCloseable {
+ method public void broadcastSessionCommand(@NonNull android.media.Session2Command, @Nullable android.os.Bundle);
+ method public void cancelSessionCommand(@NonNull android.media.MediaSession2.ControllerInfo, @NonNull Object);
+ method public void close();
+ method @NonNull public java.util.List<android.media.MediaSession2.ControllerInfo> getConnectedControllers();
+ method @NonNull public String getId();
+ method @NonNull public android.media.Session2Token getToken();
+ method public boolean isPlaybackActive();
+ method @NonNull public Object sendSessionCommand(@NonNull android.media.MediaSession2.ControllerInfo, @NonNull android.media.Session2Command, @Nullable android.os.Bundle);
+ method public void setPlaybackActive(boolean);
+ }
+
+ public static final class MediaSession2.Builder {
+ ctor public MediaSession2.Builder(@NonNull android.content.Context);
+ method @NonNull public android.media.MediaSession2 build();
+ method @NonNull public android.media.MediaSession2.Builder setExtras(@NonNull android.os.Bundle);
+ method @NonNull public android.media.MediaSession2.Builder setId(@NonNull String);
+ method @NonNull public android.media.MediaSession2.Builder setSessionActivity(@Nullable android.app.PendingIntent);
+ method @NonNull public android.media.MediaSession2.Builder setSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaSession2.SessionCallback);
+ }
+
+ public static final class MediaSession2.ControllerInfo {
+ method @NonNull public android.os.Bundle getConnectionHints();
+ method @NonNull public String getPackageName();
+ method @NonNull public android.media.session.MediaSessionManager.RemoteUserInfo getRemoteUserInfo();
+ method public int getUid();
+ }
+
+ public abstract static class MediaSession2.SessionCallback {
+ ctor public MediaSession2.SessionCallback();
+ method public void onCommandResult(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo, @NonNull Object, @NonNull android.media.Session2Command, @NonNull android.media.Session2Command.Result);
+ method @Nullable public android.media.Session2CommandGroup onConnect(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo);
+ method public void onDisconnected(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo);
+ method public void onPostConnect(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo);
+ method @Nullable public android.media.Session2Command.Result onSessionCommand(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo, @NonNull android.media.Session2Command, @Nullable android.os.Bundle);
+ }
+
+ public abstract class MediaSession2Service extends android.app.Service {
+ ctor public MediaSession2Service();
+ method public final void addSession(@NonNull android.media.MediaSession2);
+ method @NonNull public final java.util.List<android.media.MediaSession2> getSessions();
+ method @CallSuper @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent);
+ method @Nullable public abstract android.media.MediaSession2 onGetSession(@NonNull android.media.MediaSession2.ControllerInfo);
+ method @Nullable public abstract android.media.MediaSession2Service.MediaNotification onUpdateNotification(@NonNull android.media.MediaSession2);
+ method public final void removeSession(@NonNull android.media.MediaSession2);
+ field public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service";
+ }
+
+ public static class MediaSession2Service.MediaNotification {
+ ctor public MediaSession2Service.MediaNotification(int, @NonNull android.app.Notification);
+ method @NonNull public android.app.Notification getNotification();
+ method public int getNotificationId();
+ }
+
+ public final class Session2Command implements android.os.Parcelable {
+ ctor public Session2Command(int);
+ ctor public Session2Command(@NonNull String, @Nullable android.os.Bundle);
+ method public int describeContents();
+ method public int getCommandCode();
+ method @Nullable public String getCustomAction();
+ method @Nullable public android.os.Bundle getCustomExtras();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field public static final int COMMAND_CODE_CUSTOM = 0; // 0x0
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.Session2Command> CREATOR;
+ }
+
+ public static final class Session2Command.Result {
+ ctor public Session2Command.Result(int, @Nullable android.os.Bundle);
+ method public int getResultCode();
+ method @Nullable public android.os.Bundle getResultData();
+ field public static final int RESULT_ERROR_UNKNOWN_ERROR = -1; // 0xffffffff
+ field public static final int RESULT_INFO_SKIPPED = 1; // 0x1
+ field public static final int RESULT_SUCCESS = 0; // 0x0
+ }
+
+ public final class Session2CommandGroup implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public java.util.Set<android.media.Session2Command> getCommands();
+ method public boolean hasCommand(@NonNull android.media.Session2Command);
+ method public boolean hasCommand(int);
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.Session2CommandGroup> CREATOR;
+ }
+
+ public static final class Session2CommandGroup.Builder {
+ ctor public Session2CommandGroup.Builder();
+ ctor public Session2CommandGroup.Builder(@NonNull android.media.Session2CommandGroup);
+ method @NonNull public android.media.Session2CommandGroup.Builder addCommand(@NonNull android.media.Session2Command);
+ method @NonNull public android.media.Session2CommandGroup build();
+ method @NonNull public android.media.Session2CommandGroup.Builder removeCommand(@NonNull android.media.Session2Command);
+ }
+
+ public final class Session2Token implements android.os.Parcelable {
+ ctor public Session2Token(@NonNull android.content.Context, @NonNull android.content.ComponentName);
+ method public int describeContents();
+ method @NonNull public android.os.Bundle getExtras();
+ method @NonNull public String getPackageName();
+ method @Nullable public String getServiceName();
+ method public int getType();
+ method public int getUid();
+ method public void writeToParcel(android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.Session2Token> CREATOR;
+ field public static final int TYPE_SESSION = 0; // 0x0
+ field public static final int TYPE_SESSION_SERVICE = 1; // 0x1
+ }
+
+}
+
diff --git a/apex/media/framework/api/module-lib-current.txt b/apex/media/framework/api/module-lib-current.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/media/framework/api/module-lib-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/media/framework/api/module-lib-removed.txt b/apex/media/framework/api/module-lib-removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/media/framework/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/media/framework/api/removed.txt b/apex/media/framework/api/removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/media/framework/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/media/framework/api/system-current.txt b/apex/media/framework/api/system-current.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/media/framework/api/system-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/media/framework/api/system-removed.txt b/apex/media/framework/api/system-removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/media/framework/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/media/framework/jarjar_rules.txt b/apex/media/framework/jarjar_rules.txt
new file mode 100644
index 000000000000..d89d9d3343d1
--- /dev/null
+++ b/apex/media/framework/jarjar_rules.txt
@@ -0,0 +1 @@
+rule com.google.android.exoplayer2.** android.media.internal.exo.@1
diff --git a/apex/media/framework/java/android/media/BufferingParams.java b/apex/media/framework/java/android/media/BufferingParams.java
new file mode 100644
index 000000000000..04af02874bbd
--- /dev/null
+++ b/apex/media/framework/java/android/media/BufferingParams.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 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.media;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Structure for source buffering management params.
+ *
+ * Used by {@link MediaPlayer#getBufferingParams()} and
+ * {@link MediaPlayer#setBufferingParams(BufferingParams)}
+ * to control source buffering behavior.
+ *
+ * <p>There are two stages of source buffering in {@link MediaPlayer}: initial buffering
+ * (when {@link MediaPlayer} is being prepared) and rebuffering (when {@link MediaPlayer}
+ * is playing back source). {@link BufferingParams} includes corresponding marks for each
+ * stage of source buffering. The marks are time based (in milliseconds).
+ *
+ * <p>{@link MediaPlayer} source component has default marks which can be queried by
+ * calling {@link MediaPlayer#getBufferingParams()} before any change is made by
+ * {@link MediaPlayer#setBufferingParams()}.
+ * <ul>
+ * <li><strong>initial buffering:</strong> initialMarkMs is used when
+ * {@link MediaPlayer} is being prepared. When cached data amount exceeds this mark
+ * {@link MediaPlayer} is prepared. </li>
+ * <li><strong>rebuffering during playback:</strong> resumePlaybackMarkMs is used when
+ * {@link MediaPlayer} is playing back content.
+ * <ul>
+ * <li> {@link MediaPlayer} has internal mark, namely pausePlaybackMarkMs, to decide when
+ * to pause playback if cached data amount runs low. This internal mark varies based on
+ * type of data source. </li>
+ * <li> When cached data amount exceeds resumePlaybackMarkMs, {@link MediaPlayer} will
+ * resume playback if it has been paused due to low cached data amount. The internal mark
+ * pausePlaybackMarkMs shall be less than resumePlaybackMarkMs. </li>
+ * <li> {@link MediaPlayer} has internal mark, namely pauseRebufferingMarkMs, to decide
+ * when to pause rebuffering. Apparently, this internal mark shall be no less than
+ * resumePlaybackMarkMs. </li>
+ * <li> {@link MediaPlayer} has internal mark, namely resumeRebufferingMarkMs, to decide
+ * when to resume buffering. This internal mark varies based on type of data source. This
+ * mark shall be larger than pausePlaybackMarkMs, and less than pauseRebufferingMarkMs.
+ * </li>
+ * </ul> </li>
+ * </ul>
+ * <p>Users should use {@link Builder} to change {@link BufferingParams}.
+ * @hide
+ */
+public final class BufferingParams implements Parcelable {
+ private static final int BUFFERING_NO_MARK = -1;
+
+ // params
+ private int mInitialMarkMs = BUFFERING_NO_MARK;
+
+ private int mResumePlaybackMarkMs = BUFFERING_NO_MARK;
+
+ private BufferingParams() {
+ }
+
+ /**
+ * Return initial buffering mark in milliseconds.
+ * @return initial buffering mark in milliseconds
+ */
+ public int getInitialMarkMs() {
+ return mInitialMarkMs;
+ }
+
+ /**
+ * Return the mark in milliseconds for resuming playback.
+ * @return the mark for resuming playback in milliseconds
+ */
+ public int getResumePlaybackMarkMs() {
+ return mResumePlaybackMarkMs;
+ }
+
+ /**
+ * Builder class for {@link BufferingParams} objects.
+ * <p> Here is an example where <code>Builder</code> is used to define the
+ * {@link BufferingParams} to be used by a {@link MediaPlayer} instance:
+ *
+ * <pre class="prettyprint">
+ * BufferingParams myParams = mediaplayer.getDefaultBufferingParams();
+ * myParams = new BufferingParams.Builder(myParams)
+ * .setInitialMarkMs(10000)
+ * .setResumePlaybackMarkMs(15000)
+ * .build();
+ * mediaplayer.setBufferingParams(myParams);
+ * </pre>
+ */
+ public static class Builder {
+ private int mInitialMarkMs = BUFFERING_NO_MARK;
+ private int mResumePlaybackMarkMs = BUFFERING_NO_MARK;
+
+ /**
+ * Constructs a new Builder with the defaults.
+ * By default, all marks are -1.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Constructs a new Builder from a given {@link BufferingParams} instance
+ * @param bp the {@link BufferingParams} object whose data will be reused
+ * in the new Builder.
+ */
+ public Builder(BufferingParams bp) {
+ mInitialMarkMs = bp.mInitialMarkMs;
+ mResumePlaybackMarkMs = bp.mResumePlaybackMarkMs;
+ }
+
+ /**
+ * Combines all of the fields that have been set and return a new
+ * {@link BufferingParams} object. <code>IllegalStateException</code> will be
+ * thrown if there is conflict between fields.
+ * @return a new {@link BufferingParams} object
+ */
+ public BufferingParams build() {
+ BufferingParams bp = new BufferingParams();
+ bp.mInitialMarkMs = mInitialMarkMs;
+ bp.mResumePlaybackMarkMs = mResumePlaybackMarkMs;
+
+ return bp;
+ }
+
+ /**
+ * Sets the time based mark in milliseconds for initial buffering.
+ * @param markMs time based mark in milliseconds
+ * @return the same Builder instance.
+ */
+ public Builder setInitialMarkMs(int markMs) {
+ mInitialMarkMs = markMs;
+ return this;
+ }
+
+ /**
+ * Sets the time based mark in milliseconds for resuming playback.
+ * @param markMs time based mark in milliseconds for resuming playback
+ * @return the same Builder instance.
+ */
+ public Builder setResumePlaybackMarkMs(int markMs) {
+ mResumePlaybackMarkMs = markMs;
+ return this;
+ }
+ }
+
+ private BufferingParams(Parcel in) {
+ mInitialMarkMs = in.readInt();
+ mResumePlaybackMarkMs = in.readInt();
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BufferingParams> CREATOR =
+ new Parcelable.Creator<BufferingParams>() {
+ @Override
+ public BufferingParams createFromParcel(Parcel in) {
+ return new BufferingParams(in);
+ }
+
+ @Override
+ public BufferingParams[] newArray(int size) {
+ return new BufferingParams[size];
+ }
+ };
+
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mInitialMarkMs);
+ dest.writeInt(mResumePlaybackMarkMs);
+ }
+}
diff --git a/apex/media/framework/java/android/media/Controller2Link.java b/apex/media/framework/java/android/media/Controller2Link.java
new file mode 100644
index 000000000000..04185e79b0ad
--- /dev/null
+++ b/apex/media/framework/java/android/media/Controller2Link.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 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.media;
+
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+
+import java.util.Objects;
+
+/**
+ * Handles incoming commands from {@link MediaSession2} to both {@link MediaController2}.
+ * @hide
+ */
+// @SystemApi
+public final class Controller2Link implements Parcelable {
+ private static final String TAG = "Controller2Link";
+ private static final boolean DEBUG = MediaController2.DEBUG;
+
+ public static final @android.annotation.NonNull Parcelable.Creator<Controller2Link> CREATOR =
+ new Parcelable.Creator<Controller2Link>() {
+ @Override
+ public Controller2Link createFromParcel(Parcel in) {
+ return new Controller2Link(in);
+ }
+
+ @Override
+ public Controller2Link[] newArray(int size) {
+ return new Controller2Link[size];
+ }
+ };
+
+
+ private final MediaController2 mController;
+ private final IMediaController2 mIController;
+
+ public Controller2Link(MediaController2 controller) {
+ mController = controller;
+ mIController = new Controller2Stub();
+ }
+
+ Controller2Link(Parcel in) {
+ mController = null;
+ mIController = IMediaController2.Stub.asInterface(in.readStrongBinder());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mIController.asBinder());
+ }
+
+ @Override
+ public int hashCode() {
+ return mIController.asBinder().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Controller2Link)) {
+ return false;
+ }
+ Controller2Link other = (Controller2Link) obj;
+ return Objects.equals(mIController.asBinder(), other.mIController.asBinder());
+ }
+
+ /** Interface method for IMediaController2.notifyConnected */
+ public void notifyConnected(int seq, Bundle connectionResult) {
+ try {
+ mIController.notifyConnected(seq, connectionResult);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Interface method for IMediaController2.notifyDisonnected */
+ public void notifyDisconnected(int seq) {
+ try {
+ mIController.notifyDisconnected(seq);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Interface method for IMediaController2.notifyPlaybackActiveChanged */
+ public void notifyPlaybackActiveChanged(int seq, boolean playbackActive) {
+ try {
+ mIController.notifyPlaybackActiveChanged(seq, playbackActive);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Interface method for IMediaController2.sendSessionCommand */
+ public void sendSessionCommand(int seq, Session2Command command, Bundle args,
+ ResultReceiver resultReceiver) {
+ try {
+ mIController.sendSessionCommand(seq, command, args, resultReceiver);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Interface method for IMediaController2.cancelSessionCommand */
+ public void cancelSessionCommand(int seq) {
+ try {
+ mIController.cancelSessionCommand(seq);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Stub implementation for IMediaController2.notifyConnected */
+ public void onConnected(int seq, Bundle connectionResult) {
+ if (connectionResult == null) {
+ onDisconnected(seq);
+ return;
+ }
+ mController.onConnected(seq, connectionResult);
+ }
+
+ /** Stub implementation for IMediaController2.notifyDisonnected */
+ public void onDisconnected(int seq) {
+ mController.onDisconnected(seq);
+ }
+
+ /** Stub implementation for IMediaController2.notifyPlaybackActiveChanged */
+ public void onPlaybackActiveChanged(int seq, boolean playbackActive) {
+ mController.onPlaybackActiveChanged(seq, playbackActive);
+ }
+
+ /** Stub implementation for IMediaController2.sendSessionCommand */
+ public void onSessionCommand(int seq, Session2Command command, Bundle args,
+ ResultReceiver resultReceiver) {
+ mController.onSessionCommand(seq, command, args, resultReceiver);
+ }
+
+ /** Stub implementation for IMediaController2.cancelSessionCommand */
+ public void onCancelCommand(int seq) {
+ mController.onCancelCommand(seq);
+ }
+
+ private class Controller2Stub extends IMediaController2.Stub {
+ @Override
+ public void notifyConnected(int seq, Bundle connectionResult) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Controller2Link.this.onConnected(seq, connectionResult);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void notifyDisconnected(int seq) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Controller2Link.this.onDisconnected(seq);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void notifyPlaybackActiveChanged(int seq, boolean playbackActive) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Controller2Link.this.onPlaybackActiveChanged(seq, playbackActive);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void sendSessionCommand(int seq, Session2Command command, Bundle args,
+ ResultReceiver resultReceiver) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Controller2Link.this.onSessionCommand(seq, command, args, resultReceiver);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void cancelSessionCommand(int seq) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Controller2Link.this.onCancelCommand(seq);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+}
diff --git a/apex/media/framework/java/android/media/DataSourceCallback.java b/apex/media/framework/java/android/media/DataSourceCallback.java
new file mode 100644
index 000000000000..c297ecda249c
--- /dev/null
+++ b/apex/media/framework/java/android/media/DataSourceCallback.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.NonNull;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * For supplying media data to the framework. Implement this if your app has
+ * special requirements for the way media data is obtained.
+ *
+ * <p class="note">Methods of this interface may be called on multiple different
+ * threads. There will be a thread synchronization point between each call to ensure that
+ * modifications to the state of your DataSourceCallback are visible to future calls. This means
+ * you don't need to do your own synchronization unless you're modifying the
+ * DataSourceCallback from another thread while it's being used by the framework.</p>
+ *
+ * @hide
+ */
+public abstract class DataSourceCallback implements Closeable {
+
+ public static final int END_OF_STREAM = -1;
+
+ /**
+ * Called to request data from the given position.
+ *
+ * Implementations should should write up to {@code size} bytes into
+ * {@code buffer}, and return the number of bytes written.
+ *
+ * Return {@code 0} if size is zero (thus no bytes are read).
+ *
+ * Return {@code -1} to indicate that end of stream is reached.
+ *
+ * @param position the position in the data source to read from.
+ * @param buffer the buffer to read the data into.
+ * @param offset the offset within buffer to read the data into.
+ * @param size the number of bytes to read.
+ * @throws IOException on fatal errors.
+ * @return the number of bytes read, or {@link #END_OF_STREAM} if end of stream is reached.
+ */
+ public abstract int readAt(long position, @NonNull byte[] buffer, int offset, int size)
+ throws IOException;
+
+ /**
+ * Called to get the size of the data source.
+ *
+ * @throws IOException on fatal errors
+ * @return the size of data source in bytes, or -1 if the size is unknown.
+ */
+ public abstract long getSize() throws IOException;
+}
diff --git a/apex/media/framework/java/android/media/MediaConstants.java b/apex/media/framework/java/android/media/MediaConstants.java
new file mode 100644
index 000000000000..ce108894b9a5
--- /dev/null
+++ b/apex/media/framework/java/android/media/MediaConstants.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 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.media;
+
+class MediaConstants {
+ // Bundle key for int
+ static final String KEY_PID = "android.media.key.PID";
+
+ // Bundle key for String
+ static final String KEY_PACKAGE_NAME = "android.media.key.PACKAGE_NAME";
+
+ // Bundle key for Parcelable
+ static final String KEY_SESSION2LINK = "android.media.key.SESSION2LINK";
+ static final String KEY_ALLOWED_COMMANDS = "android.media.key.ALLOWED_COMMANDS";
+ static final String KEY_PLAYBACK_ACTIVE = "android.media.key.PLAYBACK_ACTIVE";
+ static final String KEY_TOKEN_EXTRAS = "android.media.key.TOKEN_EXTRAS";
+ static final String KEY_CONNECTION_HINTS = "android.media.key.CONNECTION_HINTS";
+
+ private MediaConstants() {
+ }
+}
diff --git a/apex/media/framework/java/android/media/MediaController2.java b/apex/media/framework/java/android/media/MediaController2.java
new file mode 100644
index 000000000000..d059c670ccb6
--- /dev/null
+++ b/apex/media/framework/java/android/media/MediaController2.java
@@ -0,0 +1,638 @@
+/*
+ * Copyright 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.media;
+
+import static android.media.MediaConstants.KEY_ALLOWED_COMMANDS;
+import static android.media.MediaConstants.KEY_CONNECTION_HINTS;
+import static android.media.MediaConstants.KEY_PACKAGE_NAME;
+import static android.media.MediaConstants.KEY_PID;
+import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE;
+import static android.media.MediaConstants.KEY_SESSION2LINK;
+import static android.media.MediaConstants.KEY_TOKEN_EXTRAS;
+import static android.media.Session2Command.Result.RESULT_ERROR_UNKNOWN_ERROR;
+import static android.media.Session2Command.Result.RESULT_INFO_SKIPPED;
+import static android.media.Session2Token.TYPE_SESSION;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.util.concurrent.Executor;
+
+/**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ *
+ * Allows an app to interact with an active {@link MediaSession2} or a
+ * {@link MediaSession2Service} which would provide {@link MediaSession2}. Media buttons and other
+ * commands can be sent to the session.
+ */
+public class MediaController2 implements AutoCloseable {
+ static final String TAG = "MediaController2";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final ControllerCallback mCallback;
+
+ private final IBinder.DeathRecipient mDeathRecipient = () -> close();
+ private final Context mContext;
+ private final Session2Token mSessionToken;
+ private final Executor mCallbackExecutor;
+ private final Controller2Link mControllerStub;
+ private final Handler mResultHandler;
+ private final SessionServiceConnection mServiceConnection;
+
+ private final Object mLock = new Object();
+ //@GuardedBy("mLock")
+ private boolean mClosed;
+ //@GuardedBy("mLock")
+ private int mNextSeqNumber;
+ //@GuardedBy("mLock")
+ private Session2Link mSessionBinder;
+ //@GuardedBy("mLock")
+ private Session2CommandGroup mAllowedCommands;
+ //@GuardedBy("mLock")
+ private Session2Token mConnectedToken;
+ //@GuardedBy("mLock")
+ private ArrayMap<ResultReceiver, Integer> mPendingCommands;
+ //@GuardedBy("mLock")
+ private ArraySet<Integer> mRequestedCommandSeqNumbers;
+ //@GuardedBy("mLock")
+ private boolean mPlaybackActive;
+
+ /**
+ * Create a {@link MediaController2} from the {@link Session2Token}.
+ * This connects to the session and may wake up the service if it's not available.
+ *
+ * @param context context
+ * @param token token to connect to
+ * @param connectionHints a session-specific argument to send to the session when connecting.
+ * The contents of this bundle may affect the connection result.
+ * @param executor executor to run callbacks on.
+ * @param callback controller callback to receive changes in.
+ */
+ MediaController2(@NonNull Context context, @NonNull Session2Token token,
+ @NonNull Bundle connectionHints, @NonNull Executor executor,
+ @NonNull ControllerCallback callback) {
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ if (token == null) {
+ throw new IllegalArgumentException("token shouldn't be null");
+ }
+ mContext = context;
+ mSessionToken = token;
+ mCallbackExecutor = (executor == null) ? context.getMainExecutor() : executor;
+ mCallback = (callback == null) ? new ControllerCallback() {} : callback;
+ mControllerStub = new Controller2Link(this);
+ // NOTE: mResultHandler uses main looper, so this MUST NOT be blocked.
+ mResultHandler = new Handler(context.getMainLooper());
+
+ mNextSeqNumber = 0;
+ mPendingCommands = new ArrayMap<>();
+ mRequestedCommandSeqNumbers = new ArraySet<>();
+
+ boolean connectRequested;
+ if (token.getType() == TYPE_SESSION) {
+ mServiceConnection = null;
+ connectRequested = requestConnectToSession(connectionHints);
+ } else {
+ mServiceConnection = new SessionServiceConnection(connectionHints);
+ connectRequested = requestConnectToService();
+ }
+ if (!connectRequested) {
+ close();
+ }
+ }
+
+ @Override
+ public void close() {
+ synchronized (mLock) {
+ if (mClosed) {
+ // Already closed. Ignore rest of clean up code.
+ // Note: unbindService() throws IllegalArgumentException when it's called twice.
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "closing " + this);
+ }
+ mClosed = true;
+ if (mServiceConnection != null) {
+ // Note: This should be called even when the bindService() has returned false.
+ mContext.unbindService(mServiceConnection);
+ }
+ if (mSessionBinder != null) {
+ try {
+ mSessionBinder.disconnect(mControllerStub, getNextSeqNumber());
+ mSessionBinder.unlinkToDeath(mDeathRecipient, 0);
+ } catch (RuntimeException e) {
+ // No-op
+ }
+ }
+ mConnectedToken = null;
+ mPendingCommands.clear();
+ mRequestedCommandSeqNumbers.clear();
+ mCallbackExecutor.execute(() -> {
+ mCallback.onDisconnected(MediaController2.this);
+ });
+ mSessionBinder = null;
+ }
+ }
+
+ /**
+ * Returns {@link Session2Token} of the connected session.
+ * If it is not connected yet, it returns {@code null}.
+ * <p>
+ * This may differ with the {@link Session2Token} from the constructor. For example, if the
+ * controller is created with the token for {@link MediaSession2Service}, this would return
+ * token for the {@link MediaSession2} in the service.
+ *
+ * @return Session2Token of the connected session, or {@code null} if not connected
+ */
+ @Nullable
+ public Session2Token getConnectedToken() {
+ synchronized (mLock) {
+ return mConnectedToken;
+ }
+ }
+
+ /**
+ * Returns whether the session's playback is active.
+ *
+ * @return {@code true} if playback active. {@code false} otherwise.
+ * @see ControllerCallback#onPlaybackActiveChanged(MediaController2, boolean)
+ */
+ public boolean isPlaybackActive() {
+ synchronized (mLock) {
+ return mPlaybackActive;
+ }
+ }
+
+ /**
+ * Sends a session command to the session
+ * <p>
+ * @param command the session command
+ * @param args optional arguments
+ * @return a token which will be sent together in {@link ControllerCallback#onCommandResult}
+ * when its result is received.
+ */
+ @NonNull
+ public Object sendSessionCommand(@NonNull Session2Command command, @Nullable Bundle args) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+
+ ResultReceiver resultReceiver = new ResultReceiver(mResultHandler) {
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ synchronized (mLock) {
+ mPendingCommands.remove(this);
+ }
+ mCallbackExecutor.execute(() -> {
+ mCallback.onCommandResult(MediaController2.this, this,
+ command, new Session2Command.Result(resultCode, resultData));
+ });
+ }
+ };
+
+ synchronized (mLock) {
+ if (mSessionBinder != null) {
+ int seq = getNextSeqNumber();
+ mPendingCommands.put(resultReceiver, seq);
+ try {
+ mSessionBinder.sendSessionCommand(mControllerStub, seq, command, args,
+ resultReceiver);
+ } catch (RuntimeException e) {
+ mPendingCommands.remove(resultReceiver);
+ resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null);
+ }
+ }
+ }
+ return resultReceiver;
+ }
+
+ /**
+ * Cancels the session command previously sent.
+ *
+ * @param token the token which is returned from {@link #sendSessionCommand}.
+ */
+ public void cancelSessionCommand(@NonNull Object token) {
+ if (token == null) {
+ throw new IllegalArgumentException("token shouldn't be null");
+ }
+ synchronized (mLock) {
+ if (mSessionBinder == null) return;
+ Integer seq = mPendingCommands.remove(token);
+ if (seq != null) {
+ mSessionBinder.cancelSessionCommand(mControllerStub, seq);
+ }
+ }
+ }
+
+ // Called by Controller2Link.onConnected
+ void onConnected(int seq, Bundle connectionResult) {
+ Session2Link sessionBinder = connectionResult.getParcelable(KEY_SESSION2LINK);
+ Session2CommandGroup allowedCommands =
+ connectionResult.getParcelable(KEY_ALLOWED_COMMANDS);
+ boolean playbackActive = connectionResult.getBoolean(KEY_PLAYBACK_ACTIVE);
+
+ Bundle tokenExtras = connectionResult.getBundle(KEY_TOKEN_EXTRAS);
+ if (tokenExtras == null) {
+ Log.w(TAG, "extras shouldn't be null.");
+ tokenExtras = Bundle.EMPTY;
+ } else if (MediaSession2.hasCustomParcelable(tokenExtras)) {
+ Log.w(TAG, "extras contain custom parcelable. Ignoring.");
+ tokenExtras = Bundle.EMPTY;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "notifyConnected sessionBinder=" + sessionBinder
+ + ", allowedCommands=" + allowedCommands);
+ }
+ if (sessionBinder == null || allowedCommands == null) {
+ // Connection rejected.
+ close();
+ return;
+ }
+ synchronized (mLock) {
+ mSessionBinder = sessionBinder;
+ mAllowedCommands = allowedCommands;
+ mPlaybackActive = playbackActive;
+
+ // Implementation for the local binder is no-op,
+ // so can be used without worrying about deadlock.
+ sessionBinder.linkToDeath(mDeathRecipient, 0);
+ mConnectedToken = new Session2Token(mSessionToken.getUid(), TYPE_SESSION,
+ mSessionToken.getPackageName(), sessionBinder, tokenExtras);
+ }
+ mCallbackExecutor.execute(() -> {
+ mCallback.onConnected(MediaController2.this, allowedCommands);
+ });
+ }
+
+ // Called by Controller2Link.onDisconnected
+ void onDisconnected(int seq) {
+ // close() will call mCallback.onDisconnected
+ close();
+ }
+
+ // Called by Controller2Link.onPlaybackActiveChanged
+ void onPlaybackActiveChanged(int seq, boolean playbackActive) {
+ synchronized (mLock) {
+ mPlaybackActive = playbackActive;
+ }
+ mCallbackExecutor.execute(() -> {
+ mCallback.onPlaybackActiveChanged(MediaController2.this, playbackActive);
+ });
+ }
+
+ // Called by Controller2Link.onSessionCommand
+ void onSessionCommand(int seq, Session2Command command, Bundle args,
+ @Nullable ResultReceiver resultReceiver) {
+ synchronized (mLock) {
+ mRequestedCommandSeqNumbers.add(seq);
+ }
+ mCallbackExecutor.execute(() -> {
+ boolean isCanceled;
+ synchronized (mLock) {
+ isCanceled = !mRequestedCommandSeqNumbers.remove(seq);
+ }
+ if (isCanceled) {
+ if (resultReceiver != null) {
+ resultReceiver.send(RESULT_INFO_SKIPPED, null);
+ }
+ return;
+ }
+ Session2Command.Result result = mCallback.onSessionCommand(
+ MediaController2.this, command, args);
+ if (resultReceiver != null) {
+ if (result == null) {
+ resultReceiver.send(RESULT_INFO_SKIPPED, null);
+ } else {
+ resultReceiver.send(result.getResultCode(), result.getResultData());
+ }
+ }
+ });
+ }
+
+ // Called by Controller2Link.onSessionCommand
+ void onCancelCommand(int seq) {
+ synchronized (mLock) {
+ mRequestedCommandSeqNumbers.remove(seq);
+ }
+ }
+
+ private int getNextSeqNumber() {
+ synchronized (mLock) {
+ return mNextSeqNumber++;
+ }
+ }
+
+ private Bundle createConnectionRequest(@NonNull Bundle connectionHints) {
+ Bundle connectionRequest = new Bundle();
+ connectionRequest.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
+ connectionRequest.putInt(KEY_PID, Process.myPid());
+ connectionRequest.putBundle(KEY_CONNECTION_HINTS, connectionHints);
+ return connectionRequest;
+ }
+
+ private boolean requestConnectToSession(@NonNull Bundle connectionHints) {
+ Session2Link sessionBinder = mSessionToken.getSessionLink();
+ Bundle connectionRequest = createConnectionRequest(connectionHints);
+ try {
+ sessionBinder.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Failed to call connection request", e);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean requestConnectToService() {
+ // Service. Needs to get fresh binder whenever connection is needed.
+ final Intent intent = new Intent(MediaSession2Service.SERVICE_INTERFACE);
+ intent.setClassName(mSessionToken.getPackageName(), mSessionToken.getServiceName());
+
+ // Use bindService() instead of startForegroundService() to start session service for three
+ // reasons.
+ // 1. Prevent session service owner's stopSelf() from destroying service.
+ // With the startForegroundService(), service's call of stopSelf() will trigger immediate
+ // onDestroy() calls on the main thread even when onConnect() is running in another
+ // thread.
+ // 2. Minimize APIs for developers to take care about.
+ // With bindService(), developers only need to take care about Service.onBind()
+ // but Service.onStartCommand() should be also taken care about with the
+ // startForegroundService().
+ // 3. Future support for UI-less playback
+ // If a service wants to keep running, it should be either foreground service or
+ // bound service. But there had been request for the feature for system apps
+ // and using bindService() will be better fit with it.
+ synchronized (mLock) {
+ boolean result = mContext.bindService(
+ intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ if (!result) {
+ Log.w(TAG, "bind to " + mSessionToken + " failed");
+ return false;
+ } else if (DEBUG) {
+ Log.d(TAG, "bind to " + mSessionToken + " succeeded");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Builder for {@link MediaController2}.
+ * <p>
+ * Any incoming event from the {@link MediaSession2} will be handled on the callback
+ * executor. If it's not set, {@link Context#getMainExecutor()} will be used by default.
+ */
+ public static final class Builder {
+ private Context mContext;
+ private Session2Token mToken;
+ private Bundle mConnectionHints;
+ private Executor mCallbackExecutor;
+ private ControllerCallback mCallback;
+
+ /**
+ * Creates a builder for {@link MediaController2}.
+ *
+ * @param context context
+ * @param token token of the session to connect to
+ */
+ public Builder(@NonNull Context context, @NonNull Session2Token token) {
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ if (token == null) {
+ throw new IllegalArgumentException("token shouldn't be null");
+ }
+ mContext = context;
+ mToken = token;
+ }
+
+ /**
+ * Set the connection hints for the controller.
+ * <p>
+ * {@code connectionHints} is a session-specific argument to send to the session when
+ * connecting. The contents of this bundle may affect the connection result.
+ * <p>
+ * An {@link IllegalArgumentException} will be thrown if the bundle contains any
+ * non-framework Parcelable objects.
+ *
+ * @param connectionHints a bundle which contains the connection hints
+ * @return The Builder to allow chaining
+ */
+ @NonNull
+ public Builder setConnectionHints(@NonNull Bundle connectionHints) {
+ if (connectionHints == null) {
+ throw new IllegalArgumentException("connectionHints shouldn't be null");
+ }
+ if (MediaSession2.hasCustomParcelable(connectionHints)) {
+ throw new IllegalArgumentException("connectionHints shouldn't contain any custom "
+ + "parcelables");
+ }
+ mConnectionHints = new Bundle(connectionHints);
+ return this;
+ }
+
+ /**
+ * Set callback for the controller and its executor.
+ *
+ * @param executor callback executor
+ * @param callback session callback.
+ * @return The Builder to allow chaining
+ */
+ @NonNull
+ public Builder setControllerCallback(@NonNull Executor executor,
+ @NonNull ControllerCallback callback) {
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+ mCallbackExecutor = executor;
+ mCallback = callback;
+ return this;
+ }
+
+ /**
+ * Build {@link MediaController2}.
+ *
+ * @return a new controller
+ */
+ @NonNull
+ public MediaController2 build() {
+ if (mCallbackExecutor == null) {
+ mCallbackExecutor = mContext.getMainExecutor();
+ }
+ if (mCallback == null) {
+ mCallback = new ControllerCallback() {};
+ }
+ if (mConnectionHints == null) {
+ mConnectionHints = Bundle.EMPTY;
+ }
+ return new MediaController2(
+ mContext, mToken, mConnectionHints, mCallbackExecutor, mCallback);
+ }
+ }
+
+ /**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Interface for listening to change in activeness of the {@link MediaSession2}.
+ */
+ public abstract static class ControllerCallback {
+ /**
+ * Called when the controller is successfully connected to the session. The controller
+ * becomes available afterwards.
+ *
+ * @param controller the controller for this event
+ * @param allowedCommands commands that's allowed by the session.
+ */
+ public void onConnected(@NonNull MediaController2 controller,
+ @NonNull Session2CommandGroup allowedCommands) {}
+
+ /**
+ * Called when the session refuses the controller or the controller is disconnected from
+ * the session. The controller becomes unavailable afterwards and the callback wouldn't
+ * be called.
+ * <p>
+ * It will be also called after the {@link #close()}, so you can put clean up code here.
+ * You don't need to call {@link #close()} after this.
+ *
+ * @param controller the controller for this event
+ */
+ public void onDisconnected(@NonNull MediaController2 controller) {}
+
+ /**
+ * Called when the session's playback activeness is changed.
+ *
+ * @param controller the controller for this event
+ * @param playbackActive {@code true} if the session's playback is active.
+ * {@code false} otherwise.
+ * @see MediaController2#isPlaybackActive()
+ */
+ public void onPlaybackActiveChanged(@NonNull MediaController2 controller,
+ boolean playbackActive) {}
+
+ /**
+ * Called when the connected session sent a session command.
+ *
+ * @param controller the controller for this event
+ * @param command the session command
+ * @param args optional arguments
+ * @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED
+ * will be sent to the session.
+ */
+ @Nullable
+ public Session2Command.Result onSessionCommand(@NonNull MediaController2 controller,
+ @NonNull Session2Command command, @Nullable Bundle args) {
+ return null;
+ }
+
+ /**
+ * Called when the command sent to the connected session is finished.
+ *
+ * @param controller the controller for this event
+ * @param token the token got from {@link MediaController2#sendSessionCommand}
+ * @param command the session command
+ * @param result the result of the session command
+ */
+ public void onCommandResult(@NonNull MediaController2 controller, @NonNull Object token,
+ @NonNull Session2Command command, @NonNull Session2Command.Result result) {}
+ }
+
+ // This will be called on the main thread.
+ private class SessionServiceConnection implements ServiceConnection {
+ private final Bundle mConnectionHints;
+
+ SessionServiceConnection(@Nullable Bundle connectionHints) {
+ mConnectionHints = connectionHints;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ // Note that it's always main-thread.
+ boolean connectRequested = false;
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "onServiceConnected " + name + " " + this);
+ }
+ // Sanity check
+ if (!mSessionToken.getPackageName().equals(name.getPackageName())) {
+ Log.wtf(TAG, "Expected connection to " + mSessionToken.getPackageName()
+ + " but is connected to " + name);
+ return;
+ }
+ IMediaSession2Service iService = IMediaSession2Service.Stub.asInterface(service);
+ if (iService == null) {
+ Log.wtf(TAG, "Service interface is missing.");
+ return;
+ }
+ Bundle connectionRequest = createConnectionRequest(mConnectionHints);
+ iService.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
+ connectRequested = true;
+ } catch (RemoteException e) {
+ Log.w(TAG, "Service " + name + " has died prematurely", e);
+ } finally {
+ if (!connectRequested) {
+ close();
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ // Temporal lose of the binding because of the service crash. System will automatically
+ // rebind, so just no-op.
+ if (DEBUG) {
+ Log.w(TAG, "Session service " + name + " is disconnected.");
+ }
+ close();
+ }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ // Permanent lose of the binding because of the service package update or removed.
+ // This SessionServiceRecord will be removed accordingly, but forget session binder here
+ // for sure.
+ close();
+ }
+ }
+}
diff --git a/apex/media/framework/java/android/media/MediaParser.java b/apex/media/framework/java/android/media/MediaParser.java
new file mode 100644
index 000000000000..e4b5d19e67c9
--- /dev/null
+++ b/apex/media/framework/java/android/media/MediaParser.java
@@ -0,0 +1,2130 @@
+/*
+ * 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.media;
+
+import android.annotation.CheckResult;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.media.MediaCodec.CryptoInfo;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.extractor.ChunkIndex;
+import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
+import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
+import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
+import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
+import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
+import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
+import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
+import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
+import com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
+import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
+import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
+import com.google.android.exoplayer2.extractor.ts.PsExtractor;
+import com.google.android.exoplayer2.extractor.ts.TsExtractor;
+import com.google.android.exoplayer2.extractor.wav.WavExtractor;
+import com.google.android.exoplayer2.upstream.DataReader;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.ColorInfo;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Parses media container formats and extracts contained media samples and metadata.
+ *
+ * <p>This class provides access to a battery of low-level media container parsers. Each instance of
+ * this class is associated to a specific media parser implementation which is suitable for
+ * extraction from a specific media container format. The media parser implementation assignment
+ * depends on the factory method (see {@link #create} and {@link #createByName}) used to create the
+ * instance.
+ *
+ * <p>Users must implement the following to use this class.
+ *
+ * <ul>
+ * <li>{@link InputReader}: Provides the media container's bytes to parse.
+ * <li>{@link OutputConsumer}: Provides a sink for all extracted data and metadata.
+ * </ul>
+ *
+ * <p>The following code snippet includes a usage example:
+ *
+ * <pre>
+ * MyOutputConsumer myOutputConsumer = new MyOutputConsumer();
+ * MyInputReader myInputReader = new MyInputReader("www.example.com");
+ * MediaParser mediaParser = MediaParser.create(myOutputConsumer);
+ *
+ * while (mediaParser.advance(myInputReader)) {}
+ *
+ * mediaParser.release();
+ * mediaParser = null;
+ * </pre>
+ *
+ * <p>The following code snippet provides a rudimentary {@link OutputConsumer} sample implementation
+ * which extracts and publishes all video samples:
+ *
+ * <pre>
+ * class VideoOutputConsumer implements MediaParser.OutputConsumer {
+ *
+ * private byte[] sampleDataBuffer = new byte[4096];
+ * private byte[] discardedDataBuffer = new byte[4096];
+ * private int videoTrackIndex = -1;
+ * private int bytesWrittenCount = 0;
+ *
+ * &#64;Override
+ * public void onSeekMapFound(int i, &#64;NonNull MediaFormat mediaFormat) {
+ * // Do nothing.
+ * }
+ *
+ * &#64;Override
+ * public void onTrackDataFound(int i, &#64;NonNull TrackData trackData) {
+ * MediaFormat mediaFormat = trackData.mediaFormat;
+ * if (videoTrackIndex == -1 &amp;&amp;
+ * mediaFormat
+ * .getString(MediaFormat.KEY_MIME, &#47;* defaultValue= *&#47; "")
+ * .startsWith("video/")) {
+ * videoTrackIndex = i;
+ * }
+ * }
+ *
+ * &#64;Override
+ * public void onSampleDataFound(int trackIndex, &#64;NonNull InputReader inputReader)
+ * throws IOException {
+ * int numberOfBytesToRead = (int) inputReader.getLength();
+ * if (videoTrackIndex != trackIndex) {
+ * // Discard contents.
+ * inputReader.read(
+ * discardedDataBuffer,
+ * &#47;* offset= *&#47; 0,
+ * Math.min(discardDataBuffer.length, numberOfBytesToRead));
+ * } else {
+ * ensureSpaceInBuffer(numberOfBytesToRead);
+ * int bytesRead = inputReader.read(
+ * sampleDataBuffer, bytesWrittenCount, numberOfBytesToRead);
+ * bytesWrittenCount += bytesRead;
+ * }
+ * }
+ *
+ * &#64;Override
+ * public void onSampleCompleted(
+ * int trackIndex,
+ * long timeMicros,
+ * int flags,
+ * int size,
+ * int offset,
+ * &#64;Nullable CryptoInfo cryptoData) {
+ * if (videoTrackIndex != trackIndex) {
+ * return; // It's not the video track. Ignore.
+ * }
+ * byte[] sampleData = new byte[size];
+ * int sampleStartOffset = bytesWrittenCount - size - offset;
+ * System.arraycopy(
+ * sampleDataBuffer,
+ * sampleStartOffset,
+ * sampleData,
+ * &#47;* destPos= *&#47; 0,
+ * size);
+ * // Place trailing bytes at the start of the buffer.
+ * System.arraycopy(
+ * sampleDataBuffer,
+ * bytesWrittenCount - offset,
+ * sampleDataBuffer,
+ * &#47;* destPos= *&#47; 0,
+ * &#47;* size= *&#47; offset);
+ * bytesWrittenCount = bytesWrittenCount - offset;
+ * publishSample(sampleData, timeMicros, flags);
+ * }
+ *
+ * private void ensureSpaceInBuffer(int numberOfBytesToRead) {
+ * int requiredLength = bytesWrittenCount + numberOfBytesToRead;
+ * if (requiredLength &gt; sampleDataBuffer.length) {
+ * sampleDataBuffer = Arrays.copyOf(sampleDataBuffer, requiredLength);
+ * }
+ * }
+ *
+ * }
+ *
+ * </pre>
+ */
+public final class MediaParser {
+
+ /**
+ * Maps seek positions to {@link SeekPoint SeekPoints} in the stream.
+ *
+ * <p>A {@link SeekPoint} is a position in the stream from which a player may successfully start
+ * playing media samples.
+ */
+ public static final class SeekMap {
+
+ /** Returned by {@link #getDurationMicros()} when the duration is unknown. */
+ public static final int UNKNOWN_DURATION = Integer.MIN_VALUE;
+
+ /**
+ * For each {@link #getSeekPoints} call, returns a single {@link SeekPoint} whose {@link
+ * SeekPoint#timeMicros} matches the requested timestamp, and whose {@link
+ * SeekPoint#position} is 0.
+ *
+ * @hide
+ */
+ public static final SeekMap DUMMY = new SeekMap(new DummyExoPlayerSeekMap());
+
+ private final com.google.android.exoplayer2.extractor.SeekMap mExoPlayerSeekMap;
+
+ private SeekMap(com.google.android.exoplayer2.extractor.SeekMap exoplayerSeekMap) {
+ mExoPlayerSeekMap = exoplayerSeekMap;
+ }
+
+ /** Returns whether seeking is supported. */
+ public boolean isSeekable() {
+ return mExoPlayerSeekMap.isSeekable();
+ }
+
+ /**
+ * Returns the duration of the stream in microseconds or {@link #UNKNOWN_DURATION} if the
+ * duration is unknown.
+ */
+ public long getDurationMicros() {
+ long durationUs = mExoPlayerSeekMap.getDurationUs();
+ return durationUs != C.TIME_UNSET ? durationUs : UNKNOWN_DURATION;
+ }
+
+ /**
+ * Obtains {@link SeekPoint SeekPoints} for the specified seek time in microseconds.
+ *
+ * <p>{@code getSeekPoints(timeMicros).first} contains the latest seek point for samples
+ * with timestamp equal to or smaller than {@code timeMicros}.
+ *
+ * <p>{@code getSeekPoints(timeMicros).second} contains the earliest seek point for samples
+ * with timestamp equal to or greater than {@code timeMicros}. If a seek point exists for
+ * {@code timeMicros}, the returned pair will contain the same {@link SeekPoint} twice.
+ *
+ * @param timeMicros A seek time in microseconds.
+ * @return The corresponding {@link SeekPoint SeekPoints}.
+ */
+ @NonNull
+ public Pair<SeekPoint, SeekPoint> getSeekPoints(long timeMicros) {
+ SeekPoints seekPoints = mExoPlayerSeekMap.getSeekPoints(timeMicros);
+ return new Pair<>(toSeekPoint(seekPoints.first), toSeekPoint(seekPoints.second));
+ }
+ }
+
+ /** Holds information associated with a track. */
+ public static final class TrackData {
+
+ /** Holds {@link MediaFormat} information for the track. */
+ @NonNull public final MediaFormat mediaFormat;
+
+ /**
+ * Holds {@link DrmInitData} necessary to acquire keys associated with the track, or null if
+ * the track has no encryption data.
+ */
+ @Nullable public final DrmInitData drmInitData;
+
+ private TrackData(MediaFormat mediaFormat, DrmInitData drmInitData) {
+ this.mediaFormat = mediaFormat;
+ this.drmInitData = drmInitData;
+ }
+ }
+
+ /** Defines a seek point in a media stream. */
+ public static final class SeekPoint {
+
+ /** A {@link SeekPoint} whose time and byte offset are both set to 0. */
+ @NonNull public static final SeekPoint START = new SeekPoint(0, 0);
+
+ /** The time of the seek point, in microseconds. */
+ public final long timeMicros;
+
+ /** The byte offset of the seek point. */
+ public final long position;
+
+ /**
+ * @param timeMicros The time of the seek point, in microseconds.
+ * @param position The byte offset of the seek point.
+ */
+ private SeekPoint(long timeMicros, long position) {
+ this.timeMicros = timeMicros;
+ this.position = position;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "[timeMicros=" + timeMicros + ", position=" + position + "]";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ SeekPoint other = (SeekPoint) obj;
+ return timeMicros == other.timeMicros && position == other.position;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) timeMicros;
+ result = 31 * result + (int) position;
+ return result;
+ }
+ }
+
+ /** Provides input data to {@link MediaParser}. */
+ public interface InputReader {
+
+ /**
+ * Reads up to {@code readLength} bytes of data and stores them into {@code buffer},
+ * starting at index {@code offset}.
+ *
+ * <p>This method blocks until at least one byte is read, the end of input is detected, or
+ * an exception is thrown. The read position advances to the first unread byte.
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param offset The start offset into {@code buffer} at which data should be written.
+ * @param readLength The maximum number of bytes to read.
+ * @return The non-zero number of bytes read, or -1 if no data is available because the end
+ * of the input has been reached.
+ * @throws java.io.IOException If an error occurs reading from the source.
+ */
+ int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException;
+
+ /** Returns the current read position (byte offset) in the stream. */
+ long getPosition();
+
+ /** Returns the length of the input in bytes, or -1 if the length is unknown. */
+ long getLength();
+ }
+
+ /** {@link InputReader} that allows setting the read position. */
+ public interface SeekableInputReader extends InputReader {
+
+ /**
+ * Sets the read position at the given {@code position}.
+ *
+ * <p>{@link #advance} will immediately return after calling this method.
+ *
+ * @param position The position to seek to, in bytes.
+ */
+ void seekToPosition(long position);
+ }
+
+ /** Receives extracted media sample data and metadata from {@link MediaParser}. */
+ public interface OutputConsumer {
+
+ /**
+ * Called when a {@link SeekMap} has been extracted from the stream.
+ *
+ * <p>This method is called at least once before any samples are {@link #onSampleCompleted
+ * complete}. May be called multiple times after that in order to add {@link SeekPoint
+ * SeekPoints}.
+ *
+ * @param seekMap The extracted {@link SeekMap}.
+ */
+ void onSeekMapFound(@NonNull SeekMap seekMap);
+
+ /**
+ * Called when the number of tracks is found.
+ *
+ * @param numberOfTracks The number of tracks in the stream.
+ */
+ void onTrackCountFound(int numberOfTracks);
+
+ /**
+ * Called when new {@link TrackData} is found in the stream.
+ *
+ * @param trackIndex The index of the track for which the {@link TrackData} was extracted.
+ * @param trackData The extracted {@link TrackData}.
+ */
+ void onTrackDataFound(int trackIndex, @NonNull TrackData trackData);
+
+ /**
+ * Called when sample data is found in the stream.
+ *
+ * <p>If the invocation of this method returns before the entire {@code inputReader} {@link
+ * InputReader#getLength() length} is consumed, the method will be called again for the
+ * implementer to read the remaining data. Implementers should surface any thrown {@link
+ * IOException} caused by reading from {@code input}.
+ *
+ * @param trackIndex The index of the track to which the sample data corresponds.
+ * @param inputReader The {@link InputReader} from which to read the data.
+ * @throws IOException If an exception occurs while reading from {@code inputReader}.
+ */
+ void onSampleDataFound(int trackIndex, @NonNull InputReader inputReader) throws IOException;
+
+ /**
+ * Called once all the data of a sample has been passed to {@link #onSampleDataFound}.
+ *
+ * <p>Includes sample metadata, like presentation timestamp and flags.
+ *
+ * @param trackIndex The index of the track to which the sample corresponds.
+ * @param timeMicros The media timestamp associated with the sample, in microseconds.
+ * @param flags Flags associated with the sample. See the {@code SAMPLE_FLAG_*} constants.
+ * @param size The size of the sample data, in bytes.
+ * @param offset The number of bytes that have been consumed by {@code
+ * onSampleDataFound(int, MediaParser.InputReader)} for the specified track, since the
+ * last byte belonging to the sample whose metadata is being passed.
+ * @param cryptoInfo Encryption data required to decrypt the sample. May be null for
+ * unencrypted samples. Implementors should treat any output {@link CryptoInfo}
+ * instances as immutable. MediaParser will not modify any output {@code cryptoInfos}
+ * and implementors should not modify them either.
+ */
+ void onSampleCompleted(
+ int trackIndex,
+ long timeMicros,
+ @SampleFlags int flags,
+ int size,
+ int offset,
+ @Nullable CryptoInfo cryptoInfo);
+ }
+
+ /**
+ * Thrown if all parser implementations provided to {@link #create} failed to sniff the input
+ * content.
+ */
+ public static final class UnrecognizedInputFormatException extends IOException {
+
+ /**
+ * Creates a new instance which signals that the parsers with the given names failed to
+ * parse the input.
+ */
+ @NonNull
+ @CheckResult
+ private static UnrecognizedInputFormatException createForExtractors(
+ @NonNull String... extractorNames) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("None of the available parsers ( ");
+ builder.append(extractorNames[0]);
+ for (int i = 1; i < extractorNames.length; i++) {
+ builder.append(", ");
+ builder.append(extractorNames[i]);
+ }
+ builder.append(") could read the stream.");
+ return new UnrecognizedInputFormatException(builder.toString());
+ }
+
+ private UnrecognizedInputFormatException(String extractorNames) {
+ super(extractorNames);
+ }
+ }
+
+ /** Thrown when an error occurs while parsing a media stream. */
+ public static final class ParsingException extends IOException {
+
+ private ParsingException(ParserException cause) {
+ super(cause);
+ }
+ }
+
+ // Sample flags.
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ SAMPLE_FLAG_KEY_FRAME,
+ SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA,
+ SAMPLE_FLAG_LAST_SAMPLE,
+ SAMPLE_FLAG_ENCRYPTED,
+ SAMPLE_FLAG_DECODE_ONLY
+ })
+ public @interface SampleFlags {}
+ /** Indicates that the sample holds a synchronization sample. */
+ public static final int SAMPLE_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME;
+ /**
+ * Indicates that the sample has supplemental data.
+ *
+ * <p>Samples will not have this flag set unless the {@code
+ * "android.media.mediaparser.includeSupplementalData"} parameter is set to {@code true} via
+ * {@link #setParameter}.
+ *
+ * <p>Samples with supplemental data have the following sample data format:
+ *
+ * <ul>
+ * <li>If the {@code "android.media.mediaparser.inBandCryptoInfo"} parameter is set, all
+ * encryption information.
+ * <li>(4 bytes) {@code sample_data_size}: The size of the actual sample data, not including
+ * supplemental data or encryption information.
+ * <li>({@code sample_data_size} bytes): The media sample data.
+ * <li>(remaining bytes) The supplemental data.
+ * </ul>
+ */
+ public static final int SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28;
+ /** Indicates that the sample is known to contain the last media sample of the stream. */
+ public static final int SAMPLE_FLAG_LAST_SAMPLE = 1 << 29;
+ /** Indicates that the sample is (at least partially) encrypted. */
+ public static final int SAMPLE_FLAG_ENCRYPTED = 1 << 30;
+ /** Indicates that the sample should be decoded but not rendered. */
+ public static final int SAMPLE_FLAG_DECODE_ONLY = 1 << 31;
+
+ // Parser implementation names.
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(
+ prefix = {"PARSER_NAME_"},
+ value = {
+ PARSER_NAME_UNKNOWN,
+ PARSER_NAME_MATROSKA,
+ PARSER_NAME_FMP4,
+ PARSER_NAME_MP4,
+ PARSER_NAME_MP3,
+ PARSER_NAME_ADTS,
+ PARSER_NAME_AC3,
+ PARSER_NAME_TS,
+ PARSER_NAME_FLV,
+ PARSER_NAME_OGG,
+ PARSER_NAME_PS,
+ PARSER_NAME_WAV,
+ PARSER_NAME_AMR,
+ PARSER_NAME_AC4,
+ PARSER_NAME_FLAC
+ })
+ public @interface ParserName {}
+
+ /** Parser name returned by {@link #getParserName()} when no parser has been selected yet. */
+ public static final String PARSER_NAME_UNKNOWN = "android.media.mediaparser.UNKNOWN";
+ /**
+ * Parser for the Matroska container format, as defined in the <a
+ * href="https://matroska.org/technical/specs/">spec</a>.
+ */
+ public static final String PARSER_NAME_MATROSKA = "android.media.mediaparser.MatroskaParser";
+ /**
+ * Parser for fragmented files using the MP4 container format, as defined in ISO/IEC 14496-12.
+ */
+ public static final String PARSER_NAME_FMP4 = "android.media.mediaparser.FragmentedMp4Parser";
+ /**
+ * Parser for non-fragmented files using the MP4 container format, as defined in ISO/IEC
+ * 14496-12.
+ */
+ public static final String PARSER_NAME_MP4 = "android.media.mediaparser.Mp4Parser";
+ /** Parser for the MP3 container format, as defined in ISO/IEC 11172-3. */
+ public static final String PARSER_NAME_MP3 = "android.media.mediaparser.Mp3Parser";
+ /** Parser for the ADTS container format, as defined in ISO/IEC 13818-7. */
+ public static final String PARSER_NAME_ADTS = "android.media.mediaparser.AdtsParser";
+ /**
+ * Parser for the AC-3 container format, as defined in Digital Audio Compression Standard
+ * (AC-3).
+ */
+ public static final String PARSER_NAME_AC3 = "android.media.mediaparser.Ac3Parser";
+ /** Parser for the TS container format, as defined in ISO/IEC 13818-1. */
+ public static final String PARSER_NAME_TS = "android.media.mediaparser.TsParser";
+ /**
+ * Parser for the FLV container format, as defined in Adobe Flash Video File Format
+ * Specification.
+ */
+ public static final String PARSER_NAME_FLV = "android.media.mediaparser.FlvParser";
+ /** Parser for the OGG container format, as defined in RFC 3533. */
+ public static final String PARSER_NAME_OGG = "android.media.mediaparser.OggParser";
+ /** Parser for the PS container format, as defined in ISO/IEC 11172-1. */
+ public static final String PARSER_NAME_PS = "android.media.mediaparser.PsParser";
+ /**
+ * Parser for the WAV container format, as defined in Multimedia Programming Interface and Data
+ * Specifications.
+ */
+ public static final String PARSER_NAME_WAV = "android.media.mediaparser.WavParser";
+ /** Parser for the AMR container format, as defined in RFC 4867. */
+ public static final String PARSER_NAME_AMR = "android.media.mediaparser.AmrParser";
+ /**
+ * Parser for the AC-4 container format, as defined by Dolby AC-4: Audio delivery for
+ * Next-Generation Entertainment Services.
+ */
+ public static final String PARSER_NAME_AC4 = "android.media.mediaparser.Ac4Parser";
+ /**
+ * Parser for the FLAC container format, as defined in the <a
+ * href="https://xiph.org/flac/">spec</a>.
+ */
+ public static final String PARSER_NAME_FLAC = "android.media.mediaparser.FlacParser";
+
+ // MediaParser parameters.
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(
+ prefix = {"PARAMETER_"},
+ value = {
+ PARAMETER_ADTS_ENABLE_CBR_SEEKING,
+ PARAMETER_AMR_ENABLE_CBR_SEEKING,
+ PARAMETER_FLAC_DISABLE_ID3,
+ PARAMETER_MP4_IGNORE_EDIT_LISTS,
+ PARAMETER_MP4_IGNORE_TFDT_BOX,
+ PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES,
+ PARAMETER_MATROSKA_DISABLE_CUES_SEEKING,
+ PARAMETER_MP3_DISABLE_ID3,
+ PARAMETER_MP3_ENABLE_CBR_SEEKING,
+ PARAMETER_MP3_ENABLE_INDEX_SEEKING,
+ PARAMETER_TS_MODE,
+ PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES,
+ PARAMETER_TS_IGNORE_AAC_STREAM,
+ PARAMETER_TS_IGNORE_AVC_STREAM,
+ PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM,
+ PARAMETER_TS_DETECT_ACCESS_UNITS,
+ PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS,
+ PARAMETER_IN_BAND_CRYPTO_INFO,
+ PARAMETER_INCLUDE_SUPPLEMENTAL_DATA
+ })
+ public @interface ParameterName {}
+
+ /**
+ * Sets whether constant bitrate seeking should be enabled for ADTS parsing. {@code boolean}
+ * expected. Default value is {@code false}.
+ */
+ public static final String PARAMETER_ADTS_ENABLE_CBR_SEEKING =
+ "android.media.mediaparser.adts.enableCbrSeeking";
+ /**
+ * Sets whether constant bitrate seeking should be enabled for AMR. {@code boolean} expected.
+ * Default value is {@code false}.
+ */
+ public static final String PARAMETER_AMR_ENABLE_CBR_SEEKING =
+ "android.media.mediaparser.amr.enableCbrSeeking";
+ /**
+ * Sets whether the ID3 track should be disabled for FLAC. {@code boolean} expected. Default
+ * value is {@code false}.
+ */
+ public static final String PARAMETER_FLAC_DISABLE_ID3 =
+ "android.media.mediaparser.flac.disableId3";
+ /**
+ * Sets whether MP4 parsing should ignore edit lists. {@code boolean} expected. Default value is
+ * {@code false}.
+ */
+ public static final String PARAMETER_MP4_IGNORE_EDIT_LISTS =
+ "android.media.mediaparser.mp4.ignoreEditLists";
+ /**
+ * Sets whether MP4 parsing should ignore the tfdt box. {@code boolean} expected. Default value
+ * is {@code false}.
+ */
+ public static final String PARAMETER_MP4_IGNORE_TFDT_BOX =
+ "android.media.mediaparser.mp4.ignoreTfdtBox";
+ /**
+ * Sets whether MP4 parsing should treat all video frames as key frames. {@code boolean}
+ * expected. Default value is {@code false}.
+ */
+ public static final String PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES =
+ "android.media.mediaparser.mp4.treatVideoFramesAsKeyframes";
+ /**
+ * Sets whether Matroska parsing should avoid seeking to the cues element. {@code boolean}
+ * expected. Default value is {@code false}.
+ *
+ * <p>If this flag is enabled and the cues element occurs after the first cluster, then the
+ * media is treated as unseekable.
+ */
+ public static final String PARAMETER_MATROSKA_DISABLE_CUES_SEEKING =
+ "android.media.mediaparser.matroska.disableCuesSeeking";
+ /**
+ * Sets whether the ID3 track should be disabled for MP3. {@code boolean} expected. Default
+ * value is {@code false}.
+ */
+ public static final String PARAMETER_MP3_DISABLE_ID3 =
+ "android.media.mediaparser.mp3.disableId3";
+ /**
+ * Sets whether constant bitrate seeking should be enabled for MP3. {@code boolean} expected.
+ * Default value is {@code false}.
+ */
+ public static final String PARAMETER_MP3_ENABLE_CBR_SEEKING =
+ "android.media.mediaparser.mp3.enableCbrSeeking";
+ /**
+ * Sets whether MP3 parsing should generate a time-to-byte mapping. {@code boolean} expected.
+ * Default value is {@code false}.
+ *
+ * <p>Enabling this flag may require to scan a significant portion of the file to compute a seek
+ * point. Therefore, it should only be used if:
+ *
+ * <ul>
+ * <li>the file is small, or
+ * <li>the bitrate is variable (or the type of bitrate is unknown) and the seeking metadata
+ * provided in the file is not precise enough (or is not present).
+ * </ul>
+ */
+ public static final String PARAMETER_MP3_ENABLE_INDEX_SEEKING =
+ "android.media.mediaparser.mp3.enableIndexSeeking";
+ /**
+ * Sets the operation mode for TS parsing. {@code String} expected. Valid values are {@code
+ * "single_pmt"}, {@code "multi_pmt"}, and {@code "hls"}. Default value is {@code "single_pmt"}.
+ *
+ * <p>The operation modes alter the way TS behaves so that it can handle certain kinds of
+ * commonly-occurring malformed media.
+ *
+ * <ul>
+ * <li>{@code "single_pmt"}: Only the first found PMT is parsed. Others are ignored, even if
+ * more PMTs are declared in the PAT.
+ * <li>{@code "multi_pmt"}: Behave as described in ISO/IEC 13818-1.
+ * <li>{@code "hls"}: Enable {@code "single_pmt"} mode, and ignore continuity counters.
+ * </ul>
+ */
+ public static final String PARAMETER_TS_MODE = "android.media.mediaparser.ts.mode";
+ /**
+ * Sets whether TS should treat samples consisting of non-IDR I slices as synchronization
+ * samples (key-frames). {@code boolean} expected. Default value is {@code false}.
+ */
+ public static final String PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES =
+ "android.media.mediaparser.ts.allowNonIdrAvcKeyframes";
+ /**
+ * Sets whether TS parsing should ignore AAC elementary streams. {@code boolean} expected.
+ * Default value is {@code false}.
+ */
+ public static final String PARAMETER_TS_IGNORE_AAC_STREAM =
+ "android.media.mediaparser.ts.ignoreAacStream";
+ /**
+ * Sets whether TS parsing should ignore AVC elementary streams. {@code boolean} expected.
+ * Default value is {@code false}.
+ */
+ public static final String PARAMETER_TS_IGNORE_AVC_STREAM =
+ "android.media.mediaparser.ts.ignoreAvcStream";
+ /**
+ * Sets whether TS parsing should ignore splice information streams. {@code boolean} expected.
+ * Default value is {@code false}.
+ */
+ public static final String PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM =
+ "android.media.mediaparser.ts.ignoreSpliceInfoStream";
+ /**
+ * Sets whether TS parsing should split AVC stream into access units based on slice headers.
+ * {@code boolean} expected. Default value is {@code false}.
+ *
+ * <p>This flag should be left disabled if the stream contains access units delimiters in order
+ * to avoid unnecessary computational costs.
+ */
+ public static final String PARAMETER_TS_DETECT_ACCESS_UNITS =
+ "android.media.mediaparser.ts.ignoreDetectAccessUnits";
+ /**
+ * Sets whether TS parsing should handle HDMV DTS audio streams. {@code boolean} expected.
+ * Default value is {@code false}.
+ *
+ * <p>Enabling this flag will disable the detection of SCTE subtitles.
+ */
+ public static final String PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS =
+ "android.media.mediaparser.ts.enableHdmvDtsAudioStreams";
+ /**
+ * Sets whether encryption data should be sent in-band with the sample data, as per {@link
+ * OutputConsumer#onSampleDataFound}. {@code boolean} expected. Default value is {@code false}.
+ *
+ * <p>If this parameter is set, encrypted samples' data will be prefixed with the encryption
+ * information bytes. The format for in-band encryption information is:
+ *
+ * <ul>
+ * <li>(1 byte) {@code encryption_signal_byte}: Most significant bit signals whether the
+ * encryption data contains subsample encryption data. The remaining bits contain {@code
+ * initialization_vector_size}.
+ * <li>({@code initialization_vector_size} bytes) Initialization vector.
+ * <li>If subsample encryption data is present, as per {@code encryption_signal_byte}, the
+ * encryption data also contains:
+ * <ul>
+ * <li>(2 bytes) {@code subsample_encryption_data_length}.
+ * <li>({@code subsample_encryption_data_length * 6} bytes) Subsample encryption data
+ * (repeated {@code subsample_encryption_data_length} times):
+ * <ul>
+ * <li>(2 bytes) Size of a clear section in sample.
+ * <li>(4 bytes) Size of an encryption section in sample.
+ * </ul>
+ * </ul>
+ * </ul>
+ *
+ * @hide
+ */
+ public static final String PARAMETER_IN_BAND_CRYPTO_INFO =
+ "android.media.mediaparser.inBandCryptoInfo";
+ /**
+ * Sets whether supplemental data should be included as part of the sample data. {@code boolean}
+ * expected. Default value is {@code false}. See {@link #SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA} for
+ * information about the sample data format.
+ *
+ * @hide
+ */
+ public static final String PARAMETER_INCLUDE_SUPPLEMENTAL_DATA =
+ "android.media.mediaparser.includeSupplementalData";
+ /**
+ * Sets whether sample timestamps may start from non-zero offsets. {@code boolean} expected.
+ * Default value is {@code false}.
+ *
+ * <p>When set to true, sample timestamps will not be offset to start from zero, and the media
+ * provided timestamps will be used instead. For example, transport stream sample timestamps
+ * will not be converted to a zero-based timebase.
+ *
+ * @hide
+ */
+ public static final String PARAMETER_IGNORE_TIMESTAMP_OFFSET =
+ "android.media.mediaparser.ignoreTimestampOffset";
+ /**
+ * Sets whether each track type should be eagerly exposed. {@code boolean} expected. Default
+ * value is {@code false}.
+ *
+ * <p>When set to true, each track type will be eagerly exposed through a call to {@link
+ * OutputConsumer#onTrackDataFound} containing a single-value {@link MediaFormat}. The key for
+ * the track type is {@code "track-type-string"}, and the possible values are {@code "video"},
+ * {@code "audio"}, {@code "text"}, {@code "metadata"}, and {@code "unknown"}.
+ *
+ * @hide
+ */
+ public static final String PARAMETER_EAGERLY_EXPOSE_TRACKTYPE =
+ "android.media.mediaparser.eagerlyExposeTrackType";
+ /**
+ * Sets whether a dummy {@link SeekMap} should be exposed before starting extraction. {@code
+ * boolean} expected. Default value is {@code false}.
+ *
+ * <p>For each {@link SeekMap#getSeekPoints} call, the dummy {@link SeekMap} returns a single
+ * {@link SeekPoint} whose {@link SeekPoint#timeMicros} matches the requested timestamp, and
+ * whose {@link SeekPoint#position} is 0.
+ *
+ * @hide
+ */
+ public static final String PARAMETER_EXPOSE_DUMMY_SEEKMAP =
+ "android.media.mediaparser.exposeDummySeekMap";
+
+ /**
+ * Sets whether chunk indices available in the extracted media should be exposed as {@link
+ * MediaFormat MediaFormats}. {@code boolean} expected. Default value is {@link false}.
+ *
+ * <p>When set to true, any information about media segmentation will be exposed as a {@link
+ * MediaFormat} (with track index 0) containing four {@link ByteBuffer} elements under the
+ * following keys:
+ *
+ * <ul>
+ * <li>"chunk-index-int-sizes": Contains {@code ints} representing the sizes in bytes of each
+ * of the media segments.
+ * <li>"chunk-index-long-offsets": Contains {@code longs} representing the byte offsets of
+ * each segment in the stream.
+ * <li>"chunk-index-long-us-durations": Contains {@code longs} representing the media duration
+ * of each segment, in microseconds.
+ * <li>"chunk-index-long-us-times": Contains {@code longs} representing the start time of each
+ * segment, in microseconds.
+ * </ul>
+ *
+ * @hide
+ */
+ public static final String PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT =
+ "android.media.mediaParser.exposeChunkIndexAsMediaFormat";
+ /**
+ * Sets a list of closed-caption {@link MediaFormat MediaFormats} that should be exposed as part
+ * of the extracted media. {@code List<MediaFormat>} expected. Default value is an empty list.
+ *
+ * <p>Expected keys in the {@link MediaFormat} are:
+ *
+ * <ul>
+ * <p>{@link MediaFormat#KEY_MIME}: Determine the type of captions (for example,
+ * application/cea-608). Mandatory.
+ * <p>{@link MediaFormat#KEY_CAPTION_SERVICE_NUMBER}: Determine the channel on which the
+ * captions are transmitted. Optional.
+ * </ul>
+ *
+ * @hide
+ */
+ public static final String PARAMETER_EXPOSE_CAPTION_FORMATS =
+ "android.media.mediaParser.exposeCaptionFormats";
+ /**
+ * Sets whether the value associated with {@link #PARAMETER_EXPOSE_CAPTION_FORMATS} should
+ * override any in-band caption service declarations. {@code boolean} expected. Default value is
+ * {@link false}.
+ *
+ * <p>When {@code false}, any present in-band caption services information will override the
+ * values associated with {@link #PARAMETER_EXPOSE_CAPTION_FORMATS}.
+ *
+ * @hide
+ */
+ public static final String PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS =
+ "android.media.mediaParser.overrideInBandCaptionDeclarations";
+ /**
+ * Sets whether a track for EMSG events should be exposed in case of parsing a container that
+ * supports them. {@code boolean} expected. Default value is {@link false}.
+ *
+ * @hide
+ */
+ public static final String PARAMETER_EXPOSE_EMSG_TRACK =
+ "android.media.mediaParser.exposeEmsgTrack";
+
+ // Private constants.
+
+ private static final String TAG = "MediaParser";
+ private static final Map<String, ExtractorFactory> EXTRACTOR_FACTORIES_BY_NAME;
+ private static final Map<String, Class> EXPECTED_TYPE_BY_PARAMETER_NAME;
+ private static final String TS_MODE_SINGLE_PMT = "single_pmt";
+ private static final String TS_MODE_MULTI_PMT = "multi_pmt";
+ private static final String TS_MODE_HLS = "hls";
+ private static final int BYTES_PER_SUBSAMPLE_ENCRYPTION_ENTRY = 6;
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ @IntDef(
+ value = {
+ STATE_READING_SIGNAL_BYTE,
+ STATE_READING_INIT_VECTOR,
+ STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE,
+ STATE_READING_SUBSAMPLE_ENCRYPTION_DATA
+ })
+ private @interface EncryptionDataReadState {}
+
+ private static final int STATE_READING_SIGNAL_BYTE = 0;
+ private static final int STATE_READING_INIT_VECTOR = 1;
+ private static final int STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE = 2;
+ private static final int STATE_READING_SUBSAMPLE_ENCRYPTION_DATA = 3;
+
+ // Instance creation methods.
+
+ /**
+ * Creates an instance backed by the parser with the given {@code name}. The returned instance
+ * will attempt parsing without sniffing the content.
+ *
+ * @param name The name of the parser that will be associated with the created instance.
+ * @param outputConsumer The {@link OutputConsumer} to which track data and samples are pushed.
+ * @return A new instance.
+ * @throws IllegalArgumentException If an invalid name is provided.
+ */
+ @NonNull
+ public static MediaParser createByName(
+ @NonNull @ParserName String name, @NonNull OutputConsumer outputConsumer) {
+ String[] nameAsArray = new String[] {name};
+ assertValidNames(nameAsArray);
+ return new MediaParser(outputConsumer, /* sniff= */ false, name);
+ }
+
+ /**
+ * Creates an instance whose backing parser will be selected by sniffing the content during the
+ * first {@link #advance} call. Parser implementations will sniff the content in order of
+ * appearance in {@code parserNames}.
+ *
+ * @param outputConsumer The {@link OutputConsumer} to which extracted data is output.
+ * @param parserNames The names of the parsers to sniff the content with. If empty, a default
+ * array of names is used.
+ * @return A new instance.
+ */
+ @NonNull
+ public static MediaParser create(
+ @NonNull OutputConsumer outputConsumer, @NonNull @ParserName String... parserNames) {
+ assertValidNames(parserNames);
+ if (parserNames.length == 0) {
+ parserNames = EXTRACTOR_FACTORIES_BY_NAME.keySet().toArray(new String[0]);
+ }
+ return new MediaParser(outputConsumer, /* sniff= */ true, parserNames);
+ }
+
+ // Misc static methods.
+
+ /**
+ * Returns an immutable list with the names of the parsers that are suitable for container
+ * formats with the given {@link MediaFormat}.
+ *
+ * <p>A parser supports a {@link MediaFormat} if the mime type associated with {@link
+ * MediaFormat#KEY_MIME} corresponds to the supported container format.
+ *
+ * @param mediaFormat The {@link MediaFormat} to check support for.
+ * @return The parser names that support the given {@code mediaFormat}, or the list of all
+ * parsers available if no container specific format information is provided.
+ */
+ @NonNull
+ @ParserName
+ public static List<String> getParserNames(@NonNull MediaFormat mediaFormat) {
+ String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
+ mimeType = mimeType == null ? null : Util.toLowerInvariant(mimeType.trim());
+ if (TextUtils.isEmpty(mimeType)) {
+ // No MIME type provided. Return all.
+ return Collections.unmodifiableList(
+ new ArrayList<>(EXTRACTOR_FACTORIES_BY_NAME.keySet()));
+ }
+ ArrayList<String> result = new ArrayList<>();
+ switch (mimeType) {
+ case "video/x-matroska":
+ case "audio/x-matroska":
+ case "video/x-webm":
+ case "audio/x-webm":
+ result.add(PARSER_NAME_MATROSKA);
+ break;
+ case "video/mp4":
+ case "audio/mp4":
+ case "application/mp4":
+ result.add(PARSER_NAME_MP4);
+ result.add(PARSER_NAME_FMP4);
+ break;
+ case "audio/mpeg":
+ result.add(PARSER_NAME_MP3);
+ break;
+ case "audio/aac":
+ result.add(PARSER_NAME_ADTS);
+ break;
+ case "audio/ac3":
+ result.add(PARSER_NAME_AC3);
+ break;
+ case "video/mp2t":
+ case "audio/mp2t":
+ result.add(PARSER_NAME_TS);
+ break;
+ case "video/x-flv":
+ result.add(PARSER_NAME_FLV);
+ break;
+ case "video/ogg":
+ case "audio/ogg":
+ case "application/ogg":
+ result.add(PARSER_NAME_OGG);
+ break;
+ case "video/mp2p":
+ case "video/mp1s":
+ result.add(PARSER_NAME_PS);
+ break;
+ case "audio/vnd.wave":
+ case "audio/wav":
+ case "audio/wave":
+ case "audio/x-wav":
+ result.add(PARSER_NAME_WAV);
+ break;
+ case "audio/amr":
+ result.add(PARSER_NAME_AMR);
+ break;
+ case "audio/ac4":
+ result.add(PARSER_NAME_AC4);
+ break;
+ case "audio/flac":
+ case "audio/x-flac":
+ result.add(PARSER_NAME_FLAC);
+ break;
+ default:
+ // No parsers support the given mime type. Do nothing.
+ break;
+ }
+ return Collections.unmodifiableList(result);
+ }
+
+ // Private fields.
+
+ private final Map<String, Object> mParserParameters;
+ private final OutputConsumer mOutputConsumer;
+ private final String[] mParserNamesPool;
+ private final PositionHolder mPositionHolder;
+ private final InputReadingDataReader mExoDataReader;
+ private final DataReaderAdapter mScratchDataReaderAdapter;
+ private final ParsableByteArrayAdapter mScratchParsableByteArrayAdapter;
+ @Nullable private final Constructor<DrmInitData.SchemeInitData> mSchemeInitDataConstructor;
+ private final ArrayList<Format> mMuxedCaptionFormats;
+ private boolean mInBandCryptoInfo;
+ private boolean mIncludeSupplementalData;
+ private boolean mIgnoreTimestampOffset;
+ private boolean mEagerlyExposeTrackType;
+ private boolean mExposeDummySeekMap;
+ private boolean mExposeChunkIndexAsMediaFormat;
+ private String mParserName;
+ private Extractor mExtractor;
+ private ExtractorInput mExtractorInput;
+ private boolean mPendingExtractorInit;
+ private long mPendingSeekPosition;
+ private long mPendingSeekTimeMicros;
+ private boolean mLoggedSchemeInitDataCreationException;
+
+ // Public methods.
+
+ /**
+ * Sets parser-specific parameters which allow customizing behavior.
+ *
+ * <p>Must be called before the first call to {@link #advance}.
+ *
+ * @param parameterName The name of the parameter to set. See {@code PARAMETER_*} constants for
+ * documentation on possible values.
+ * @param value The value to set for the given {@code parameterName}. See {@code PARAMETER_*}
+ * constants for documentation on the expected types.
+ * @return This instance, for convenience.
+ * @throws IllegalStateException If called after calling {@link #advance} on the same instance.
+ */
+ @NonNull
+ public MediaParser setParameter(
+ @NonNull @ParameterName String parameterName, @NonNull Object value) {
+ if (mExtractor != null) {
+ throw new IllegalStateException(
+ "setParameters() must be called before the first advance() call.");
+ }
+ Class expectedType = EXPECTED_TYPE_BY_PARAMETER_NAME.get(parameterName);
+ // Ignore parameter names that are not contained in the map, in case the client is passing
+ // a parameter that is being added in a future version of this library.
+ if (expectedType != null && !expectedType.isInstance(value)) {
+ throw new IllegalArgumentException(
+ parameterName
+ + " expects a "
+ + expectedType.getSimpleName()
+ + " but a "
+ + value.getClass().getSimpleName()
+ + " was passed.");
+ }
+ if (PARAMETER_TS_MODE.equals(parameterName)
+ && !TS_MODE_SINGLE_PMT.equals(value)
+ && !TS_MODE_HLS.equals(value)
+ && !TS_MODE_MULTI_PMT.equals(value)) {
+ throw new IllegalArgumentException(PARAMETER_TS_MODE + " does not accept: " + value);
+ }
+ if (PARAMETER_IN_BAND_CRYPTO_INFO.equals(parameterName)) {
+ mInBandCryptoInfo = (boolean) value;
+ }
+ if (PARAMETER_INCLUDE_SUPPLEMENTAL_DATA.equals(parameterName)) {
+ mIncludeSupplementalData = (boolean) value;
+ }
+ if (PARAMETER_IGNORE_TIMESTAMP_OFFSET.equals(parameterName)) {
+ mIgnoreTimestampOffset = (boolean) value;
+ }
+ if (PARAMETER_EAGERLY_EXPOSE_TRACKTYPE.equals(parameterName)) {
+ mEagerlyExposeTrackType = (boolean) value;
+ }
+ if (PARAMETER_EXPOSE_DUMMY_SEEKMAP.equals(parameterName)) {
+ mExposeDummySeekMap = (boolean) value;
+ }
+ if (PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT.equals(parameterName)) {
+ mExposeChunkIndexAsMediaFormat = (boolean) value;
+ }
+ if (PARAMETER_EXPOSE_CAPTION_FORMATS.equals(parameterName)) {
+ setMuxedCaptionFormats((List<MediaFormat>) value);
+ }
+ mParserParameters.put(parameterName, value);
+ return this;
+ }
+
+ /**
+ * Returns whether the given {@code parameterName} is supported by this parser.
+ *
+ * @param parameterName The parameter name to check support for. One of the {@code PARAMETER_*}
+ * constants.
+ * @return Whether the given {@code parameterName} is supported.
+ */
+ public boolean supportsParameter(@NonNull @ParameterName String parameterName) {
+ return EXPECTED_TYPE_BY_PARAMETER_NAME.containsKey(parameterName);
+ }
+
+ /**
+ * Returns the name of the backing parser implementation.
+ *
+ * <p>If this instance was creating using {@link #createByName}, the provided name is returned.
+ * If this instance was created using {@link #create}, this method will return {@link
+ * #PARSER_NAME_UNKNOWN} until the first call to {@link #advance}, after which the name of the
+ * backing parser implementation is returned.
+ *
+ * @return The name of the backing parser implementation, or null if the backing parser
+ * implementation has not yet been selected.
+ */
+ @NonNull
+ @ParserName
+ public String getParserName() {
+ return mParserName;
+ }
+
+ /**
+ * Makes progress in the extraction of the input media stream, unless the end of the input has
+ * been reached.
+ *
+ * <p>This method will block until some progress has been made.
+ *
+ * <p>If this instance was created using {@link #create}, the first call to this method will
+ * sniff the content using the selected parser implementations.
+ *
+ * @param seekableInputReader The {@link SeekableInputReader} from which to obtain the media
+ * container data.
+ * @return Whether there is any data left to extract. Returns false if the end of input has been
+ * reached.
+ * @throws IOException If an error occurs while reading from the {@link SeekableInputReader}.
+ * @throws UnrecognizedInputFormatException If the format cannot be recognized by any of the
+ * underlying parser implementations.
+ */
+ public boolean advance(@NonNull SeekableInputReader seekableInputReader) throws IOException {
+ if (mExtractorInput == null) {
+ // TODO: For efficiency, the same implementation should be used, by providing a
+ // clearBuffers() method, or similar.
+ mExtractorInput =
+ new DefaultExtractorInput(
+ mExoDataReader,
+ seekableInputReader.getPosition(),
+ seekableInputReader.getLength());
+ }
+ mExoDataReader.mInputReader = seekableInputReader;
+
+ if (mExtractor == null) {
+ mPendingExtractorInit = true;
+ if (!mParserName.equals(PARSER_NAME_UNKNOWN)) {
+ mExtractor = createExtractor(mParserName);
+ } else {
+ for (String parserName : mParserNamesPool) {
+ Extractor extractor = createExtractor(parserName);
+ try {
+ if (extractor.sniff(mExtractorInput)) {
+ mParserName = parserName;
+ mExtractor = extractor;
+ mPendingExtractorInit = true;
+ break;
+ }
+ } catch (EOFException e) {
+ // Do nothing.
+ } finally {
+ mExtractorInput.resetPeekPosition();
+ }
+ }
+ if (mExtractor == null) {
+ throw UnrecognizedInputFormatException.createForExtractors(mParserNamesPool);
+ }
+ return true;
+ }
+ }
+
+ if (mPendingExtractorInit) {
+ if (mExposeDummySeekMap) {
+ // We propagate the dummy seek map before initializing the extractor, in case the
+ // extractor initialization outputs a seek map.
+ mOutputConsumer.onSeekMapFound(SeekMap.DUMMY);
+ }
+ mExtractor.init(new ExtractorOutputAdapter());
+ mPendingExtractorInit = false;
+ // We return after initialization to allow clients use any output information before
+ // starting actual extraction.
+ return true;
+ }
+
+ if (isPendingSeek()) {
+ mExtractor.seek(mPendingSeekPosition, mPendingSeekTimeMicros);
+ removePendingSeek();
+ }
+
+ mPositionHolder.position = seekableInputReader.getPosition();
+ int result;
+ try {
+ result = mExtractor.read(mExtractorInput, mPositionHolder);
+ } catch (ParserException e) {
+ throw new ParsingException(e);
+ }
+ if (result == Extractor.RESULT_END_OF_INPUT) {
+ mExtractorInput = null;
+ return false;
+ }
+ if (result == Extractor.RESULT_SEEK) {
+ mExtractorInput = null;
+ seekableInputReader.seekToPosition(mPositionHolder.position);
+ }
+ return true;
+ }
+
+ /**
+ * Seeks within the media container being extracted.
+ *
+ * <p>{@link SeekPoint SeekPoints} can be obtained from the {@link SeekMap} passed to {@link
+ * OutputConsumer#onSeekMapFound(SeekMap)}.
+ *
+ * <p>Following a call to this method, the {@link InputReader} passed to the next invocation of
+ * {@link #advance} must provide data starting from {@link SeekPoint#position} in the stream.
+ *
+ * @param seekPoint The {@link SeekPoint} to seek to.
+ */
+ public void seek(@NonNull SeekPoint seekPoint) {
+ if (mExtractor == null) {
+ mPendingSeekPosition = seekPoint.position;
+ mPendingSeekTimeMicros = seekPoint.timeMicros;
+ } else {
+ mExtractor.seek(seekPoint.position, seekPoint.timeMicros);
+ }
+ }
+
+ /**
+ * Releases any acquired resources.
+ *
+ * <p>After calling this method, this instance becomes unusable and no other methods should be
+ * invoked.
+ */
+ public void release() {
+ // TODO: Dump media metrics here.
+ mExtractorInput = null;
+ mExtractor = null;
+ }
+
+ // Private methods.
+
+ private MediaParser(OutputConsumer outputConsumer, boolean sniff, String... parserNamesPool) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ throw new UnsupportedOperationException("Android version must be R or greater.");
+ }
+ mParserParameters = new HashMap<>();
+ mOutputConsumer = outputConsumer;
+ mParserNamesPool = parserNamesPool;
+ mParserName = sniff ? PARSER_NAME_UNKNOWN : parserNamesPool[0];
+ mPositionHolder = new PositionHolder();
+ mExoDataReader = new InputReadingDataReader();
+ removePendingSeek();
+ mScratchDataReaderAdapter = new DataReaderAdapter();
+ mScratchParsableByteArrayAdapter = new ParsableByteArrayAdapter();
+ mSchemeInitDataConstructor = getSchemeInitDataConstructor();
+ mMuxedCaptionFormats = new ArrayList<>();
+ }
+
+ private void setMuxedCaptionFormats(List<MediaFormat> mediaFormats) {
+ mMuxedCaptionFormats.clear();
+ for (MediaFormat mediaFormat : mediaFormats) {
+ mMuxedCaptionFormats.add(toExoPlayerCaptionFormat(mediaFormat));
+ }
+ }
+
+ private boolean isPendingSeek() {
+ return mPendingSeekPosition >= 0;
+ }
+
+ private void removePendingSeek() {
+ mPendingSeekPosition = -1;
+ mPendingSeekTimeMicros = -1;
+ }
+
+ private Extractor createExtractor(String parserName) {
+ int flags = 0;
+ TimestampAdjuster timestampAdjuster = null;
+ if (mIgnoreTimestampOffset) {
+ timestampAdjuster = new TimestampAdjuster(TimestampAdjuster.DO_NOT_OFFSET);
+ }
+ switch (parserName) {
+ case PARSER_NAME_MATROSKA:
+ flags =
+ getBooleanParameter(PARAMETER_MATROSKA_DISABLE_CUES_SEEKING)
+ ? MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES
+ : 0;
+ return new MatroskaExtractor(flags);
+ case PARSER_NAME_FMP4:
+ flags |=
+ getBooleanParameter(PARAMETER_EXPOSE_EMSG_TRACK)
+ ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK
+ : 0;
+ flags |=
+ getBooleanParameter(PARAMETER_MP4_IGNORE_EDIT_LISTS)
+ ? FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTS
+ : 0;
+ flags |=
+ getBooleanParameter(PARAMETER_MP4_IGNORE_TFDT_BOX)
+ ? FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX
+ : 0;
+ flags |=
+ getBooleanParameter(PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES)
+ ? FragmentedMp4Extractor
+ .FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
+ : 0;
+ return new FragmentedMp4Extractor(
+ flags,
+ timestampAdjuster,
+ /* sideloadedTrack= */ null,
+ mMuxedCaptionFormats);
+ case PARSER_NAME_MP4:
+ flags |=
+ getBooleanParameter(PARAMETER_MP4_IGNORE_EDIT_LISTS)
+ ? Mp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTS
+ : 0;
+ return new Mp4Extractor(flags);
+ case PARSER_NAME_MP3:
+ flags |=
+ getBooleanParameter(PARAMETER_MP3_DISABLE_ID3)
+ ? Mp3Extractor.FLAG_DISABLE_ID3_METADATA
+ : 0;
+ flags |=
+ getBooleanParameter(PARAMETER_MP3_ENABLE_CBR_SEEKING)
+ ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
+ : 0;
+ // TODO: Add index seeking once we update the ExoPlayer version.
+ return new Mp3Extractor(flags);
+ case PARSER_NAME_ADTS:
+ flags |=
+ getBooleanParameter(PARAMETER_ADTS_ENABLE_CBR_SEEKING)
+ ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
+ : 0;
+ return new AdtsExtractor(flags);
+ case PARSER_NAME_AC3:
+ return new Ac3Extractor();
+ case PARSER_NAME_TS:
+ flags |=
+ getBooleanParameter(PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES)
+ ? DefaultTsPayloadReaderFactory.FLAG_ALLOW_NON_IDR_KEYFRAMES
+ : 0;
+ flags |=
+ getBooleanParameter(PARAMETER_TS_DETECT_ACCESS_UNITS)
+ ? DefaultTsPayloadReaderFactory.FLAG_DETECT_ACCESS_UNITS
+ : 0;
+ flags |=
+ getBooleanParameter(PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS)
+ ? DefaultTsPayloadReaderFactory.FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS
+ : 0;
+ flags |=
+ getBooleanParameter(PARAMETER_TS_IGNORE_AAC_STREAM)
+ ? DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM
+ : 0;
+ flags |=
+ getBooleanParameter(PARAMETER_TS_IGNORE_AVC_STREAM)
+ ? DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM
+ : 0;
+ flags |=
+ getBooleanParameter(PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM)
+ ? DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM
+ : 0;
+ flags |=
+ getBooleanParameter(PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS)
+ ? DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS
+ : 0;
+ String tsMode = getStringParameter(PARAMETER_TS_MODE, TS_MODE_SINGLE_PMT);
+ int hlsMode =
+ TS_MODE_SINGLE_PMT.equals(tsMode)
+ ? TsExtractor.MODE_SINGLE_PMT
+ : TS_MODE_HLS.equals(tsMode)
+ ? TsExtractor.MODE_HLS
+ : TsExtractor.MODE_MULTI_PMT;
+ return new TsExtractor(
+ hlsMode,
+ timestampAdjuster != null
+ ? timestampAdjuster
+ : new TimestampAdjuster(/* firstSampleTimestampUs= */ 0),
+ new DefaultTsPayloadReaderFactory(flags, mMuxedCaptionFormats));
+ case PARSER_NAME_FLV:
+ return new FlvExtractor();
+ case PARSER_NAME_OGG:
+ return new OggExtractor();
+ case PARSER_NAME_PS:
+ return new PsExtractor();
+ case PARSER_NAME_WAV:
+ return new WavExtractor();
+ case PARSER_NAME_AMR:
+ flags |=
+ getBooleanParameter(PARAMETER_AMR_ENABLE_CBR_SEEKING)
+ ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
+ : 0;
+ return new AmrExtractor(flags);
+ case PARSER_NAME_AC4:
+ return new Ac4Extractor();
+ case PARSER_NAME_FLAC:
+ flags |=
+ getBooleanParameter(PARAMETER_FLAC_DISABLE_ID3)
+ ? FlacExtractor.FLAG_DISABLE_ID3_METADATA
+ : 0;
+ return new FlacExtractor(flags);
+ default:
+ // Should never happen.
+ throw new IllegalStateException("Unexpected attempt to create: " + parserName);
+ }
+ }
+
+ private boolean getBooleanParameter(String name) {
+ return (boolean) mParserParameters.getOrDefault(name, false);
+ }
+
+ private String getStringParameter(String name, String defaultValue) {
+ return (String) mParserParameters.getOrDefault(name, defaultValue);
+ }
+
+ // Private classes.
+
+ private static final class InputReadingDataReader implements DataReader {
+
+ public InputReader mInputReader;
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ return mInputReader.read(buffer, offset, readLength);
+ }
+ }
+
+ private final class MediaParserDrmInitData extends DrmInitData {
+
+ private final SchemeInitData[] mSchemeDatas;
+
+ private MediaParserDrmInitData(com.google.android.exoplayer2.drm.DrmInitData exoDrmInitData)
+ throws IllegalAccessException, InstantiationException, InvocationTargetException {
+ mSchemeDatas = new SchemeInitData[exoDrmInitData.schemeDataCount];
+ for (int i = 0; i < mSchemeDatas.length; i++) {
+ mSchemeDatas[i] = toFrameworkSchemeInitData(exoDrmInitData.get(i));
+ }
+ }
+
+ @Override
+ @Nullable
+ public SchemeInitData get(UUID schemeUuid) {
+ for (SchemeInitData schemeInitData : mSchemeDatas) {
+ if (schemeInitData.uuid.equals(schemeUuid)) {
+ return schemeInitData;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public SchemeInitData getSchemeInitDataAt(int index) {
+ return mSchemeDatas[index];
+ }
+
+ @Override
+ public int getSchemeInitDataCount() {
+ return mSchemeDatas.length;
+ }
+
+ private DrmInitData.SchemeInitData toFrameworkSchemeInitData(SchemeData exoSchemeData)
+ throws IllegalAccessException, InvocationTargetException, InstantiationException {
+ return mSchemeInitDataConstructor.newInstance(
+ exoSchemeData.uuid, exoSchemeData.mimeType, exoSchemeData.data);
+ }
+ }
+
+ private final class ExtractorOutputAdapter implements ExtractorOutput {
+
+ private final SparseArray<TrackOutput> mTrackOutputAdapters;
+ private boolean mTracksEnded;
+
+ private ExtractorOutputAdapter() {
+ mTrackOutputAdapters = new SparseArray<>();
+ }
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ TrackOutput trackOutput = mTrackOutputAdapters.get(id);
+ if (trackOutput == null) {
+ int trackIndex = mTrackOutputAdapters.size();
+ trackOutput = new TrackOutputAdapter(trackIndex);
+ mTrackOutputAdapters.put(id, trackOutput);
+ if (mEagerlyExposeTrackType) {
+ MediaFormat mediaFormat = new MediaFormat();
+ mediaFormat.setString("track-type-string", toTypeString(type));
+ mOutputConsumer.onTrackDataFound(
+ trackIndex, new TrackData(mediaFormat, /* drmInitData= */ null));
+ }
+ }
+ return trackOutput;
+ }
+
+ @Override
+ public void endTracks() {
+ mOutputConsumer.onTrackCountFound(mTrackOutputAdapters.size());
+ }
+
+ @Override
+ public void seekMap(com.google.android.exoplayer2.extractor.SeekMap exoplayerSeekMap) {
+ if (mExposeChunkIndexAsMediaFormat && exoplayerSeekMap instanceof ChunkIndex) {
+ ChunkIndex chunkIndex = (ChunkIndex) exoplayerSeekMap;
+ MediaFormat mediaFormat = new MediaFormat();
+ mediaFormat.setByteBuffer("chunk-index-int-sizes", toByteBuffer(chunkIndex.sizes));
+ mediaFormat.setByteBuffer(
+ "chunk-index-long-offsets", toByteBuffer(chunkIndex.offsets));
+ mediaFormat.setByteBuffer(
+ "chunk-index-long-us-durations", toByteBuffer(chunkIndex.durationsUs));
+ mediaFormat.setByteBuffer(
+ "chunk-index-long-us-times", toByteBuffer(chunkIndex.timesUs));
+ mOutputConsumer.onTrackDataFound(
+ /* trackIndex= */ 0, new TrackData(mediaFormat, /* drmInitData= */ null));
+ }
+ mOutputConsumer.onSeekMapFound(new SeekMap(exoplayerSeekMap));
+ }
+ }
+
+ private class TrackOutputAdapter implements TrackOutput {
+
+ private final int mTrackIndex;
+
+ private CryptoInfo mLastOutputCryptoInfo;
+ private CryptoInfo.Pattern mLastOutputEncryptionPattern;
+ private CryptoData mLastReceivedCryptoData;
+
+ @EncryptionDataReadState private int mEncryptionDataReadState;
+ private int mEncryptionDataSizeToSubtractFromSampleDataSize;
+ private int mEncryptionVectorSize;
+ private byte[] mScratchIvSpace;
+ private int mSubsampleEncryptionDataSize;
+ private int[] mScratchSubsampleEncryptedBytesCount;
+ private int[] mScratchSubsampleClearBytesCount;
+ private boolean mHasSubsampleEncryptionData;
+ private int mSkippedSupplementalDataBytes;
+
+ private TrackOutputAdapter(int trackIndex) {
+ mTrackIndex = trackIndex;
+ mScratchIvSpace = new byte[16]; // Size documented in CryptoInfo.
+ mScratchSubsampleEncryptedBytesCount = new int[32];
+ mScratchSubsampleClearBytesCount = new int[32];
+ mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE;
+ mLastOutputEncryptionPattern =
+ new CryptoInfo.Pattern(/* blocksToEncrypt= */ 0, /* blocksToSkip= */ 0);
+ }
+
+ @Override
+ public void format(Format format) {
+ mOutputConsumer.onTrackDataFound(
+ mTrackIndex,
+ new TrackData(
+ toMediaFormat(format), toFrameworkDrmInitData(format.drmInitData)));
+ }
+
+ @Override
+ public int sampleData(
+ DataReader input,
+ int length,
+ boolean allowEndOfInput,
+ @SampleDataPart int sampleDataPart)
+ throws IOException {
+ mScratchDataReaderAdapter.setDataReader(input, length);
+ long positionBeforeReading = mScratchDataReaderAdapter.getPosition();
+ mOutputConsumer.onSampleDataFound(mTrackIndex, mScratchDataReaderAdapter);
+ return (int) (mScratchDataReaderAdapter.getPosition() - positionBeforeReading);
+ }
+
+ @Override
+ public void sampleData(
+ ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) {
+ if (sampleDataPart == SAMPLE_DATA_PART_ENCRYPTION && !mInBandCryptoInfo) {
+ while (length > 0) {
+ switch (mEncryptionDataReadState) {
+ case STATE_READING_SIGNAL_BYTE:
+ int encryptionSignalByte = data.readUnsignedByte();
+ length--;
+ mHasSubsampleEncryptionData = ((encryptionSignalByte >> 7) & 1) != 0;
+ mEncryptionVectorSize = encryptionSignalByte & 0x7F;
+ mEncryptionDataSizeToSubtractFromSampleDataSize =
+ mEncryptionVectorSize + 1; // Signal byte.
+ mEncryptionDataReadState = STATE_READING_INIT_VECTOR;
+ break;
+ case STATE_READING_INIT_VECTOR:
+ Arrays.fill(mScratchIvSpace, (byte) 0); // Ensure 0-padding.
+ data.readBytes(mScratchIvSpace, /* offset= */ 0, mEncryptionVectorSize);
+ length -= mEncryptionVectorSize;
+ if (mHasSubsampleEncryptionData) {
+ mEncryptionDataReadState = STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE;
+ } else {
+ mSubsampleEncryptionDataSize = 0;
+ mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE;
+ }
+ break;
+ case STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE:
+ mSubsampleEncryptionDataSize = data.readUnsignedShort();
+ if (mScratchSubsampleClearBytesCount.length
+ < mSubsampleEncryptionDataSize) {
+ mScratchSubsampleClearBytesCount =
+ new int[mSubsampleEncryptionDataSize];
+ mScratchSubsampleEncryptedBytesCount =
+ new int[mSubsampleEncryptionDataSize];
+ }
+ length -= 2;
+ mEncryptionDataSizeToSubtractFromSampleDataSize +=
+ 2
+ + mSubsampleEncryptionDataSize
+ * BYTES_PER_SUBSAMPLE_ENCRYPTION_ENTRY;
+ mEncryptionDataReadState = STATE_READING_SUBSAMPLE_ENCRYPTION_DATA;
+ break;
+ case STATE_READING_SUBSAMPLE_ENCRYPTION_DATA:
+ for (int i = 0; i < mSubsampleEncryptionDataSize; i++) {
+ mScratchSubsampleClearBytesCount[i] = data.readUnsignedShort();
+ mScratchSubsampleEncryptedBytesCount[i] = data.readInt();
+ }
+ length -=
+ mSubsampleEncryptionDataSize
+ * BYTES_PER_SUBSAMPLE_ENCRYPTION_ENTRY;
+ mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE;
+ if (length != 0) {
+ throw new IllegalStateException();
+ }
+ break;
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ }
+ } else if (sampleDataPart == SAMPLE_DATA_PART_SUPPLEMENTAL
+ && !mIncludeSupplementalData) {
+ mSkippedSupplementalDataBytes += length;
+ data.skipBytes(length);
+ } else {
+ outputSampleData(data, length);
+ }
+ }
+
+ @Override
+ public void sampleMetadata(
+ long timeUs, int flags, int size, int offset, @Nullable CryptoData cryptoData) {
+ size -= mSkippedSupplementalDataBytes;
+ mSkippedSupplementalDataBytes = 0;
+ mOutputConsumer.onSampleCompleted(
+ mTrackIndex,
+ timeUs,
+ getMediaParserFlags(flags),
+ size - mEncryptionDataSizeToSubtractFromSampleDataSize,
+ offset,
+ getPopulatedCryptoInfo(cryptoData));
+ mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE;
+ mEncryptionDataSizeToSubtractFromSampleDataSize = 0;
+ }
+
+ @Nullable
+ private CryptoInfo getPopulatedCryptoInfo(@Nullable CryptoData cryptoData) {
+ if (cryptoData == null) {
+ // The sample is not encrypted.
+ return null;
+ } else if (mInBandCryptoInfo) {
+ if (cryptoData != mLastReceivedCryptoData) {
+ mLastOutputCryptoInfo =
+ createNewCryptoInfoAndPopulateWithCryptoData(cryptoData);
+ // We are using in-band crypto info, so the IV will be ignored. But we prevent
+ // it from being null because toString assumes it non-null.
+ mLastOutputCryptoInfo.iv = EMPTY_BYTE_ARRAY;
+ }
+ } else /* We must populate the full CryptoInfo. */ {
+ // CryptoInfo.pattern is not accessible to the user, so the user needs to feed
+ // this CryptoInfo directly to MediaCodec. We need to create a new CryptoInfo per
+ // sample because of per-sample initialization vector changes.
+ CryptoInfo newCryptoInfo = createNewCryptoInfoAndPopulateWithCryptoData(cryptoData);
+ newCryptoInfo.iv = Arrays.copyOf(mScratchIvSpace, mScratchIvSpace.length);
+ boolean canReuseSubsampleInfo =
+ mLastOutputCryptoInfo != null
+ && mLastOutputCryptoInfo.numSubSamples
+ == mSubsampleEncryptionDataSize;
+ for (int i = 0; i < mSubsampleEncryptionDataSize && canReuseSubsampleInfo; i++) {
+ canReuseSubsampleInfo =
+ mLastOutputCryptoInfo.numBytesOfClearData[i]
+ == mScratchSubsampleClearBytesCount[i]
+ && mLastOutputCryptoInfo.numBytesOfEncryptedData[i]
+ == mScratchSubsampleEncryptedBytesCount[i];
+ }
+ newCryptoInfo.numSubSamples = mSubsampleEncryptionDataSize;
+ if (canReuseSubsampleInfo) {
+ newCryptoInfo.numBytesOfClearData = mLastOutputCryptoInfo.numBytesOfClearData;
+ newCryptoInfo.numBytesOfEncryptedData =
+ mLastOutputCryptoInfo.numBytesOfEncryptedData;
+ } else {
+ newCryptoInfo.numBytesOfClearData =
+ Arrays.copyOf(
+ mScratchSubsampleClearBytesCount, mSubsampleEncryptionDataSize);
+ newCryptoInfo.numBytesOfEncryptedData =
+ Arrays.copyOf(
+ mScratchSubsampleEncryptedBytesCount,
+ mSubsampleEncryptionDataSize);
+ }
+ mLastOutputCryptoInfo = newCryptoInfo;
+ }
+ mLastReceivedCryptoData = cryptoData;
+ return mLastOutputCryptoInfo;
+ }
+
+ private CryptoInfo createNewCryptoInfoAndPopulateWithCryptoData(CryptoData cryptoData) {
+ CryptoInfo cryptoInfo = new CryptoInfo();
+ cryptoInfo.key = cryptoData.encryptionKey;
+ cryptoInfo.mode = cryptoData.cryptoMode;
+ if (cryptoData.clearBlocks != mLastOutputEncryptionPattern.getSkipBlocks()
+ || cryptoData.encryptedBlocks
+ != mLastOutputEncryptionPattern.getEncryptBlocks()) {
+ mLastOutputEncryptionPattern =
+ new CryptoInfo.Pattern(cryptoData.encryptedBlocks, cryptoData.clearBlocks);
+ }
+ cryptoInfo.setPattern(mLastOutputEncryptionPattern);
+ return cryptoInfo;
+ }
+
+ private void outputSampleData(ParsableByteArray data, int length) {
+ mScratchParsableByteArrayAdapter.resetWithByteArray(data, length);
+ try {
+ // Read all bytes from data. ExoPlayer extractors expect all sample data to be
+ // consumed by TrackOutput implementations when passing a ParsableByteArray.
+ while (mScratchParsableByteArrayAdapter.getLength() > 0) {
+ mOutputConsumer.onSampleDataFound(
+ mTrackIndex, mScratchParsableByteArrayAdapter);
+ }
+ } catch (IOException e) {
+ // Unexpected.
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private static final class DataReaderAdapter implements InputReader {
+
+ private DataReader mDataReader;
+ private int mCurrentPosition;
+ private long mLength;
+
+ public void setDataReader(DataReader dataReader, long length) {
+ mDataReader = dataReader;
+ mCurrentPosition = 0;
+ mLength = length;
+ }
+
+ // Input implementation.
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ int readBytes = 0;
+ readBytes = mDataReader.read(buffer, offset, readLength);
+ mCurrentPosition += readBytes;
+ return readBytes;
+ }
+
+ @Override
+ public long getPosition() {
+ return mCurrentPosition;
+ }
+
+ @Override
+ public long getLength() {
+ return mLength - mCurrentPosition;
+ }
+ }
+
+ private static final class ParsableByteArrayAdapter implements InputReader {
+
+ private ParsableByteArray mByteArray;
+ private long mLength;
+ private int mCurrentPosition;
+
+ public void resetWithByteArray(ParsableByteArray byteArray, long length) {
+ mByteArray = byteArray;
+ mCurrentPosition = 0;
+ mLength = length;
+ }
+
+ // Input implementation.
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) {
+ mByteArray.readBytes(buffer, offset, readLength);
+ mCurrentPosition += readLength;
+ return readLength;
+ }
+
+ @Override
+ public long getPosition() {
+ return mCurrentPosition;
+ }
+
+ @Override
+ public long getLength() {
+ return mLength - mCurrentPosition;
+ }
+ }
+
+ private static final class DummyExoPlayerSeekMap
+ implements com.google.android.exoplayer2.extractor.SeekMap {
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return C.TIME_UNSET;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ com.google.android.exoplayer2.extractor.SeekPoint seekPoint =
+ new com.google.android.exoplayer2.extractor.SeekPoint(
+ timeUs, /* position= */ 0);
+ return new SeekPoints(seekPoint, seekPoint);
+ }
+ }
+
+ /** Creates extractor instances. */
+ private interface ExtractorFactory {
+
+ /** Returns a new extractor instance. */
+ Extractor createInstance();
+ }
+
+ // Private static methods.
+
+ private static Format toExoPlayerCaptionFormat(MediaFormat mediaFormat) {
+ Format.Builder formatBuilder =
+ new Format.Builder().setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME));
+ if (mediaFormat.containsKey(MediaFormat.KEY_CAPTION_SERVICE_NUMBER)) {
+ formatBuilder.setAccessibilityChannel(
+ mediaFormat.getInteger(MediaFormat.KEY_CAPTION_SERVICE_NUMBER));
+ }
+ return formatBuilder.build();
+ }
+
+ private static MediaFormat toMediaFormat(Format format) {
+ MediaFormat result = new MediaFormat();
+ setOptionalMediaFormatInt(result, MediaFormat.KEY_BIT_RATE, format.bitrate);
+ setOptionalMediaFormatInt(result, MediaFormat.KEY_CHANNEL_COUNT, format.channelCount);
+
+ ColorInfo colorInfo = format.colorInfo;
+ if (colorInfo != null) {
+ setOptionalMediaFormatInt(
+ result, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer);
+ setOptionalMediaFormatInt(result, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange);
+ setOptionalMediaFormatInt(result, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace);
+
+ if (format.colorInfo.hdrStaticInfo != null) {
+ result.setByteBuffer(
+ MediaFormat.KEY_HDR_STATIC_INFO,
+ ByteBuffer.wrap(format.colorInfo.hdrStaticInfo));
+ }
+ }
+
+ setOptionalMediaFormatString(result, MediaFormat.KEY_MIME, format.sampleMimeType);
+ setOptionalMediaFormatString(result, MediaFormat.KEY_CODECS_STRING, format.codecs);
+ if (format.frameRate != Format.NO_VALUE) {
+ result.setFloat(MediaFormat.KEY_FRAME_RATE, format.frameRate);
+ }
+ setOptionalMediaFormatInt(result, MediaFormat.KEY_WIDTH, format.width);
+ setOptionalMediaFormatInt(result, MediaFormat.KEY_HEIGHT, format.height);
+
+ List<byte[]> initData = format.initializationData;
+ for (int i = 0; i < initData.size(); i++) {
+ result.setByteBuffer("csd-" + i, ByteBuffer.wrap(initData.get(i)));
+ }
+ setPcmEncoding(format, result);
+ setOptionalMediaFormatString(result, MediaFormat.KEY_LANGUAGE, format.language);
+ setOptionalMediaFormatInt(result, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize);
+ setOptionalMediaFormatInt(result, MediaFormat.KEY_ROTATION, format.rotationDegrees);
+ setOptionalMediaFormatInt(result, MediaFormat.KEY_SAMPLE_RATE, format.sampleRate);
+ setOptionalMediaFormatInt(
+ result, MediaFormat.KEY_CAPTION_SERVICE_NUMBER, format.accessibilityChannel);
+
+ int selectionFlags = format.selectionFlags;
+ result.setInteger(
+ MediaFormat.KEY_IS_AUTOSELECT, selectionFlags & C.SELECTION_FLAG_AUTOSELECT);
+ result.setInteger(MediaFormat.KEY_IS_DEFAULT, selectionFlags & C.SELECTION_FLAG_DEFAULT);
+ result.setInteger(
+ MediaFormat.KEY_IS_FORCED_SUBTITLE, selectionFlags & C.SELECTION_FLAG_FORCED);
+
+ setOptionalMediaFormatInt(result, MediaFormat.KEY_ENCODER_DELAY, format.encoderDelay);
+ setOptionalMediaFormatInt(result, MediaFormat.KEY_ENCODER_PADDING, format.encoderPadding);
+
+ if (format.pixelWidthHeightRatio != Format.NO_VALUE && format.pixelWidthHeightRatio != 0) {
+ int parWidth = 1;
+ int parHeight = 1;
+ if (format.pixelWidthHeightRatio < 1.0f) {
+ parHeight = 1 << 30;
+ parWidth = (int) (format.pixelWidthHeightRatio * parHeight);
+ } else if (format.pixelWidthHeightRatio > 1.0f) {
+ parWidth = 1 << 30;
+ parHeight = (int) (parWidth / format.pixelWidthHeightRatio);
+ }
+ result.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH, parWidth);
+ result.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT, parHeight);
+ result.setFloat("pixel-width-height-ratio-float", format.pixelWidthHeightRatio);
+ }
+ if (format.drmInitData != null) {
+ // The crypto mode is propagated along with sample metadata. We also include it in the
+ // format for convenient use from ExoPlayer.
+ result.setString("crypto-mode-fourcc", format.drmInitData.schemeType);
+ }
+ if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
+ result.setLong("subsample-offset-us-long", format.subsampleOffsetUs);
+ }
+ // LACK OF SUPPORT FOR:
+ // format.id;
+ // format.metadata;
+ // format.stereoMode;
+ return result;
+ }
+
+ private static ByteBuffer toByteBuffer(long[] longArray) {
+ ByteBuffer byteBuffer = ByteBuffer.allocateDirect(longArray.length * Long.BYTES);
+ for (long element : longArray) {
+ byteBuffer.putLong(element);
+ }
+ byteBuffer.flip();
+ return byteBuffer;
+ }
+
+ private static ByteBuffer toByteBuffer(int[] intArray) {
+ ByteBuffer byteBuffer = ByteBuffer.allocateDirect(intArray.length * Integer.BYTES);
+ for (int element : intArray) {
+ byteBuffer.putInt(element);
+ }
+ byteBuffer.flip();
+ return byteBuffer;
+ }
+
+ private static String toTypeString(int type) {
+ switch (type) {
+ case C.TRACK_TYPE_VIDEO:
+ return "video";
+ case C.TRACK_TYPE_AUDIO:
+ return "audio";
+ case C.TRACK_TYPE_TEXT:
+ return "text";
+ case C.TRACK_TYPE_METADATA:
+ return "metadata";
+ default:
+ return "unknown";
+ }
+ }
+
+ private static void setPcmEncoding(Format format, MediaFormat result) {
+ int exoPcmEncoding = format.pcmEncoding;
+ setOptionalMediaFormatInt(result, "exo-pcm-encoding", format.pcmEncoding);
+ int mediaFormatPcmEncoding;
+ switch (exoPcmEncoding) {
+ case C.ENCODING_PCM_8BIT:
+ mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_8BIT;
+ break;
+ case C.ENCODING_PCM_16BIT:
+ mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_16BIT;
+ break;
+ case C.ENCODING_PCM_FLOAT:
+ mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_FLOAT;
+ break;
+ default:
+ // No matching value. Do nothing.
+ return;
+ }
+ result.setInteger(MediaFormat.KEY_PCM_ENCODING, mediaFormatPcmEncoding);
+ }
+
+ private static void setOptionalMediaFormatInt(MediaFormat mediaFormat, String key, int value) {
+ if (value != Format.NO_VALUE) {
+ mediaFormat.setInteger(key, value);
+ }
+ }
+
+ private static void setOptionalMediaFormatString(
+ MediaFormat mediaFormat, String key, @Nullable String value) {
+ if (value != null) {
+ mediaFormat.setString(key, value);
+ }
+ }
+
+ private DrmInitData toFrameworkDrmInitData(
+ com.google.android.exoplayer2.drm.DrmInitData exoDrmInitData) {
+ try {
+ return exoDrmInitData != null && mSchemeInitDataConstructor != null
+ ? new MediaParserDrmInitData(exoDrmInitData)
+ : null;
+ } catch (Throwable e) {
+ if (!mLoggedSchemeInitDataCreationException) {
+ mLoggedSchemeInitDataCreationException = true;
+ Log.e(TAG, "Unable to create SchemeInitData instance.");
+ }
+ return null;
+ }
+ }
+
+ /** Returns a new {@link SeekPoint} equivalent to the given {@code exoPlayerSeekPoint}. */
+ private static SeekPoint toSeekPoint(
+ com.google.android.exoplayer2.extractor.SeekPoint exoPlayerSeekPoint) {
+ return new SeekPoint(exoPlayerSeekPoint.timeUs, exoPlayerSeekPoint.position);
+ }
+
+ private static void assertValidNames(@NonNull String[] names) {
+ for (String name : names) {
+ if (!EXTRACTOR_FACTORIES_BY_NAME.containsKey(name)) {
+ throw new IllegalArgumentException(
+ "Invalid extractor name: "
+ + name
+ + ". Supported parsers are: "
+ + TextUtils.join(", ", EXTRACTOR_FACTORIES_BY_NAME.keySet())
+ + ".");
+ }
+ }
+ }
+
+ private int getMediaParserFlags(int flags) {
+ @SampleFlags int result = 0;
+ result |= (flags & C.BUFFER_FLAG_ENCRYPTED) != 0 ? SAMPLE_FLAG_ENCRYPTED : 0;
+ result |= (flags & C.BUFFER_FLAG_KEY_FRAME) != 0 ? SAMPLE_FLAG_KEY_FRAME : 0;
+ result |= (flags & C.BUFFER_FLAG_DECODE_ONLY) != 0 ? SAMPLE_FLAG_DECODE_ONLY : 0;
+ result |=
+ (flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0 && mIncludeSupplementalData
+ ? SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA
+ : 0;
+ result |= (flags & C.BUFFER_FLAG_LAST_SAMPLE) != 0 ? SAMPLE_FLAG_LAST_SAMPLE : 0;
+ return result;
+ }
+
+ @Nullable
+ private static Constructor<DrmInitData.SchemeInitData> getSchemeInitDataConstructor() {
+ // TODO: Use constructor statically when available.
+ Constructor<DrmInitData.SchemeInitData> constructor;
+ try {
+ return DrmInitData.SchemeInitData.class.getConstructor(
+ UUID.class, String.class, byte[].class);
+ } catch (Throwable e) {
+ Log.e(TAG, "Unable to get SchemeInitData constructor.");
+ return null;
+ }
+ }
+
+ // Static initialization.
+
+ static {
+ // Using a LinkedHashMap to keep the insertion order when iterating over the keys.
+ LinkedHashMap<String, ExtractorFactory> extractorFactoriesByName = new LinkedHashMap<>();
+ // Parsers are ordered to match ExoPlayer's DefaultExtractorsFactory extractor ordering,
+ // which in turn aims to minimize the chances of incorrect extractor selections.
+ extractorFactoriesByName.put(PARSER_NAME_MATROSKA, MatroskaExtractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_FMP4, FragmentedMp4Extractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_MP4, Mp4Extractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_MP3, Mp3Extractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_ADTS, AdtsExtractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_AC3, Ac3Extractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_TS, TsExtractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_FLV, FlvExtractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_OGG, OggExtractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_PS, PsExtractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_WAV, WavExtractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_AMR, AmrExtractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_AC4, Ac4Extractor::new);
+ extractorFactoriesByName.put(PARSER_NAME_FLAC, FlacExtractor::new);
+ EXTRACTOR_FACTORIES_BY_NAME = Collections.unmodifiableMap(extractorFactoriesByName);
+
+ HashMap<String, Class> expectedTypeByParameterName = new HashMap<>();
+ expectedTypeByParameterName.put(PARAMETER_ADTS_ENABLE_CBR_SEEKING, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_AMR_ENABLE_CBR_SEEKING, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_FLAC_DISABLE_ID3, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_MP4_IGNORE_EDIT_LISTS, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_MP4_IGNORE_TFDT_BOX, Boolean.class);
+ expectedTypeByParameterName.put(
+ PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_MATROSKA_DISABLE_CUES_SEEKING, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_MP3_DISABLE_ID3, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_MP3_ENABLE_CBR_SEEKING, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_MP3_ENABLE_INDEX_SEEKING, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_TS_MODE, String.class);
+ expectedTypeByParameterName.put(PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_TS_IGNORE_AAC_STREAM, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_TS_IGNORE_AVC_STREAM, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_TS_DETECT_ACCESS_UNITS, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_IN_BAND_CRYPTO_INFO, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_INCLUDE_SUPPLEMENTAL_DATA, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_IGNORE_TIMESTAMP_OFFSET, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_EAGERLY_EXPOSE_TRACKTYPE, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_EXPOSE_DUMMY_SEEKMAP, Boolean.class);
+ expectedTypeByParameterName.put(
+ PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT, Boolean.class);
+ expectedTypeByParameterName.put(
+ PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS, Boolean.class);
+ expectedTypeByParameterName.put(PARAMETER_EXPOSE_EMSG_TRACK, Boolean.class);
+ // We do not check PARAMETER_EXPOSE_CAPTION_FORMATS here, and we do it in setParameters
+ // instead. Checking that the value is a List is insufficient to catch wrong parameter
+ // value types.
+ EXPECTED_TYPE_BY_PARAMETER_NAME = Collections.unmodifiableMap(expectedTypeByParameterName);
+ }
+}
diff --git a/apex/media/framework/java/android/media/MediaSession2.java b/apex/media/framework/java/android/media/MediaSession2.java
new file mode 100644
index 000000000000..6560afedab0f
--- /dev/null
+++ b/apex/media/framework/java/android/media/MediaSession2.java
@@ -0,0 +1,907 @@
+/*
+ * Copyright 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.media;
+
+import static android.media.MediaConstants.KEY_ALLOWED_COMMANDS;
+import static android.media.MediaConstants.KEY_CONNECTION_HINTS;
+import static android.media.MediaConstants.KEY_PACKAGE_NAME;
+import static android.media.MediaConstants.KEY_PID;
+import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE;
+import static android.media.MediaConstants.KEY_SESSION2LINK;
+import static android.media.MediaConstants.KEY_TOKEN_EXTRAS;
+import static android.media.Session2Command.Result.RESULT_ERROR_UNKNOWN_ERROR;
+import static android.media.Session2Command.Result.RESULT_INFO_SKIPPED;
+import static android.media.Session2Token.TYPE_SESSION;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.session.MediaSessionManager;
+import android.media.session.MediaSessionManager.RemoteUserInfo;
+import android.os.BadParcelableException;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Allows a media app to expose its transport controls and playback information in a process to
+ * other processes including the Android framework and other apps.
+ */
+public class MediaSession2 implements AutoCloseable {
+ static final String TAG = "MediaSession2";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ // Note: This checks the uniqueness of a session ID only in a single process.
+ // When the framework becomes able to check the uniqueness, this logic should be removed.
+ //@GuardedBy("MediaSession.class")
+ private static final List<String> SESSION_ID_LIST = new ArrayList<>();
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Object mLock = new Object();
+ //@GuardedBy("mLock")
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Map<Controller2Link, ControllerInfo> mConnectedControllers = new HashMap<>();
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Context mContext;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Executor mCallbackExecutor;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final SessionCallback mCallback;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Session2Link mSessionStub;
+
+ private final String mSessionId;
+ private final PendingIntent mSessionActivity;
+ private final Session2Token mSessionToken;
+ private final MediaSessionManager mSessionManager;
+ private final Handler mResultHandler;
+
+ //@GuardedBy("mLock")
+ private boolean mClosed;
+ //@GuardedBy("mLock")
+ private boolean mPlaybackActive;
+ //@GuardedBy("mLock")
+ private ForegroundServiceEventCallback mForegroundServiceEventCallback;
+
+ MediaSession2(@NonNull Context context, @NonNull String id, PendingIntent sessionActivity,
+ @NonNull Executor callbackExecutor, @NonNull SessionCallback callback,
+ @NonNull Bundle tokenExtras) {
+ synchronized (MediaSession2.class) {
+ if (SESSION_ID_LIST.contains(id)) {
+ throw new IllegalStateException("Session ID must be unique. ID=" + id);
+ }
+ SESSION_ID_LIST.add(id);
+ }
+
+ mContext = context;
+ mSessionId = id;
+ mSessionActivity = sessionActivity;
+ mCallbackExecutor = callbackExecutor;
+ mCallback = callback;
+ mSessionStub = new Session2Link(this);
+ mSessionToken = new Session2Token(Process.myUid(), TYPE_SESSION, context.getPackageName(),
+ mSessionStub, tokenExtras);
+ mSessionManager = (MediaSessionManager) mContext.getSystemService(
+ Context.MEDIA_SESSION_SERVICE);
+ // NOTE: mResultHandler uses main looper, so this MUST NOT be blocked.
+ mResultHandler = new Handler(context.getMainLooper());
+ mClosed = false;
+ }
+
+ @Override
+ public void close() {
+ try {
+ List<ControllerInfo> controllerInfos;
+ ForegroundServiceEventCallback callback;
+ synchronized (mLock) {
+ if (mClosed) {
+ return;
+ }
+ mClosed = true;
+ controllerInfos = getConnectedControllers();
+ mConnectedControllers.clear();
+ callback = mForegroundServiceEventCallback;
+ mForegroundServiceEventCallback = null;
+ }
+ synchronized (MediaSession2.class) {
+ SESSION_ID_LIST.remove(mSessionId);
+ }
+ if (callback != null) {
+ callback.onSessionClosed(this);
+ }
+ for (ControllerInfo info : controllerInfos) {
+ info.notifyDisconnected();
+ }
+ } catch (Exception e) {
+ // Should not be here.
+ }
+ }
+
+ /**
+ * Returns the session ID
+ */
+ @NonNull
+ public String getId() {
+ return mSessionId;
+ }
+
+ /**
+ * Returns the {@link Session2Token} for creating {@link MediaController2}.
+ */
+ @NonNull
+ public Session2Token getToken() {
+ return mSessionToken;
+ }
+
+ /**
+ * Broadcasts a session command to all the connected controllers
+ * <p>
+ * @param command the session command
+ * @param args optional arguments
+ */
+ public void broadcastSessionCommand(@NonNull Session2Command command, @Nullable Bundle args) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ List<ControllerInfo> controllerInfos = getConnectedControllers();
+ for (ControllerInfo controller : controllerInfos) {
+ controller.sendSessionCommand(command, args, null);
+ }
+ }
+
+ /**
+ * Sends a session command to a specific controller
+ * <p>
+ * @param controller the controller to get the session command
+ * @param command the session command
+ * @param args optional arguments
+ * @return a token which will be sent together in {@link SessionCallback#onCommandResult}
+ * when its result is received.
+ */
+ @NonNull
+ public Object sendSessionCommand(@NonNull ControllerInfo controller,
+ @NonNull Session2Command command, @Nullable Bundle args) {
+ if (controller == null) {
+ throw new IllegalArgumentException("controller shouldn't be null");
+ }
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ ResultReceiver resultReceiver = new ResultReceiver(mResultHandler) {
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ controller.receiveCommandResult(this);
+ mCallbackExecutor.execute(() -> {
+ mCallback.onCommandResult(MediaSession2.this, controller, this,
+ command, new Session2Command.Result(resultCode, resultData));
+ });
+ }
+ };
+ controller.sendSessionCommand(command, args, resultReceiver);
+ return resultReceiver;
+ }
+
+ /**
+ * Cancels the session command previously sent.
+ *
+ * @param controller the controller to get the session command
+ * @param token the token which is returned from {@link #sendSessionCommand}.
+ */
+ public void cancelSessionCommand(@NonNull ControllerInfo controller, @NonNull Object token) {
+ if (controller == null) {
+ throw new IllegalArgumentException("controller shouldn't be null");
+ }
+ if (token == null) {
+ throw new IllegalArgumentException("token shouldn't be null");
+ }
+ controller.cancelSessionCommand(token);
+ }
+
+ /**
+ * Sets whether the playback is active (i.e. playing something)
+ *
+ * @param playbackActive {@code true} if the playback active, {@code false} otherwise.
+ **/
+ public void setPlaybackActive(boolean playbackActive) {
+ final ForegroundServiceEventCallback serviceCallback;
+ synchronized (mLock) {
+ if (mPlaybackActive == playbackActive) {
+ return;
+ }
+ mPlaybackActive = playbackActive;
+ serviceCallback = mForegroundServiceEventCallback;
+ }
+ if (serviceCallback != null) {
+ serviceCallback.onPlaybackActiveChanged(this, playbackActive);
+ }
+ List<ControllerInfo> controllerInfos = getConnectedControllers();
+ for (ControllerInfo controller : controllerInfos) {
+ controller.notifyPlaybackActiveChanged(playbackActive);
+ }
+ }
+
+ /**
+ * Returns whehther the playback is active (i.e. playing something)
+ *
+ * @return {@code true} if the playback active, {@code false} otherwise.
+ */
+ public boolean isPlaybackActive() {
+ synchronized (mLock) {
+ return mPlaybackActive;
+ }
+ }
+
+ /**
+ * Gets the list of the connected controllers
+ *
+ * @return list of the connected controllers.
+ */
+ @NonNull
+ public List<ControllerInfo> getConnectedControllers() {
+ List<ControllerInfo> controllers = new ArrayList<>();
+ synchronized (mLock) {
+ controllers.addAll(mConnectedControllers.values());
+ }
+ return controllers;
+ }
+
+ /**
+ * Returns whether the given bundle includes non-framework Parcelables.
+ */
+ static boolean hasCustomParcelable(@Nullable Bundle bundle) {
+ if (bundle == null) {
+ return false;
+ }
+
+ // Try writing the bundle to parcel, and read it with framework classloader.
+ Parcel parcel = null;
+ try {
+ parcel = Parcel.obtain();
+ parcel.writeBundle(bundle);
+ parcel.setDataPosition(0);
+ Bundle out = parcel.readBundle(null);
+
+ // Calling Bundle#size() will trigger Bundle#unparcel().
+ out.size();
+ } catch (BadParcelableException e) {
+ Log.d(TAG, "Custom parcelable in bundle.", e);
+ return true;
+ } finally {
+ if (parcel != null) {
+ parcel.recycle();
+ }
+ }
+ return false;
+ }
+
+ boolean isClosed() {
+ synchronized (mLock) {
+ return mClosed;
+ }
+ }
+
+ SessionCallback getCallback() {
+ return mCallback;
+ }
+
+ void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
+ synchronized (mLock) {
+ if (mForegroundServiceEventCallback == callback) {
+ return;
+ }
+ if (mForegroundServiceEventCallback != null && callback != null) {
+ throw new IllegalStateException("A session cannot be added to multiple services");
+ }
+ mForegroundServiceEventCallback = callback;
+ }
+ }
+
+ // Called by Session2Link.onConnect and MediaSession2Service.MediaSession2ServiceStub.connect
+ void onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq,
+ Bundle connectionRequest) {
+ if (callingPid == 0) {
+ // The pid here is from Binder.getCallingPid(), which can be 0 for an oneway call from
+ // the remote process. If it's the case, use PID from the connectionRequest.
+ callingPid = connectionRequest.getInt(KEY_PID);
+ }
+ String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME);
+
+ RemoteUserInfo remoteUserInfo = new RemoteUserInfo(callingPkg, callingPid, callingUid);
+
+ Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS);
+ if (connectionHints == null) {
+ Log.w(TAG, "connectionHints shouldn't be null.");
+ connectionHints = Bundle.EMPTY;
+ } else if (hasCustomParcelable(connectionHints)) {
+ Log.w(TAG, "connectionHints contain custom parcelable. Ignoring.");
+ connectionHints = Bundle.EMPTY;
+ }
+
+ final ControllerInfo controllerInfo = new ControllerInfo(
+ remoteUserInfo,
+ mSessionManager.isTrustedForMediaControl(remoteUserInfo),
+ controller,
+ connectionHints);
+ mCallbackExecutor.execute(() -> {
+ boolean connected = false;
+ try {
+ if (isClosed()) {
+ return;
+ }
+ controllerInfo.mAllowedCommands =
+ mCallback.onConnect(MediaSession2.this, controllerInfo);
+ // Don't reject connection for the request from trusted app.
+ // Otherwise server will fail to retrieve session's information to dispatch
+ // media keys to.
+ if (controllerInfo.mAllowedCommands == null && !controllerInfo.isTrusted()) {
+ return;
+ }
+ if (controllerInfo.mAllowedCommands == null) {
+ // For trusted apps, send non-null allowed commands to keep
+ // connection.
+ controllerInfo.mAllowedCommands =
+ new Session2CommandGroup.Builder().build();
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Accepting connection: " + controllerInfo);
+ }
+ // If connection is accepted, notify the current state to the controller.
+ // It's needed because we cannot call synchronous calls between
+ // session/controller.
+ Bundle connectionResult = new Bundle();
+ connectionResult.putParcelable(KEY_SESSION2LINK, mSessionStub);
+ connectionResult.putParcelable(KEY_ALLOWED_COMMANDS,
+ controllerInfo.mAllowedCommands);
+ connectionResult.putBoolean(KEY_PLAYBACK_ACTIVE, isPlaybackActive());
+ connectionResult.putBundle(KEY_TOKEN_EXTRAS, mSessionToken.getExtras());
+
+ // Double check if session is still there, because close() can be called in
+ // another thread.
+ if (isClosed()) {
+ return;
+ }
+ controllerInfo.notifyConnected(connectionResult);
+ synchronized (mLock) {
+ if (mConnectedControllers.containsKey(controller)) {
+ Log.w(TAG, "Controller " + controllerInfo + " has sent connection"
+ + " request multiple times");
+ }
+ mConnectedControllers.put(controller, controllerInfo);
+ }
+ mCallback.onPostConnect(MediaSession2.this, controllerInfo);
+ connected = true;
+ } finally {
+ if (!connected || isClosed()) {
+ if (DEBUG) {
+ Log.d(TAG, "Rejecting connection or notifying that session is closed"
+ + ", controllerInfo=" + controllerInfo);
+ }
+ synchronized (mLock) {
+ mConnectedControllers.remove(controller);
+ }
+ controllerInfo.notifyDisconnected();
+ }
+ }
+ });
+ }
+
+ // Called by Session2Link.onDisconnect
+ void onDisconnect(@NonNull final Controller2Link controller, int seq) {
+ final ControllerInfo controllerInfo;
+ synchronized (mLock) {
+ controllerInfo = mConnectedControllers.remove(controller);
+ }
+ if (controllerInfo == null) {
+ return;
+ }
+ mCallbackExecutor.execute(() -> {
+ mCallback.onDisconnected(MediaSession2.this, controllerInfo);
+ });
+ }
+
+ // Called by Session2Link.onSessionCommand
+ void onSessionCommand(@NonNull final Controller2Link controller, final int seq,
+ final Session2Command command, final Bundle args,
+ @Nullable ResultReceiver resultReceiver) {
+ if (controller == null) {
+ return;
+ }
+ final ControllerInfo controllerInfo;
+ synchronized (mLock) {
+ controllerInfo = mConnectedControllers.get(controller);
+ }
+ if (controllerInfo == null) {
+ return;
+ }
+
+ // TODO: check allowed commands.
+ synchronized (mLock) {
+ controllerInfo.addRequestedCommandSeqNumber(seq);
+ }
+ mCallbackExecutor.execute(() -> {
+ if (!controllerInfo.removeRequestedCommandSeqNumber(seq)) {
+ resultReceiver.send(RESULT_INFO_SKIPPED, null);
+ return;
+ }
+ Session2Command.Result result = mCallback.onSessionCommand(
+ MediaSession2.this, controllerInfo, command, args);
+ if (resultReceiver != null) {
+ if (result == null) {
+ resultReceiver.send(RESULT_INFO_SKIPPED, null);
+ } else {
+ resultReceiver.send(result.getResultCode(), result.getResultData());
+ }
+ }
+ });
+ }
+
+ // Called by Session2Link.onCancelCommand
+ void onCancelCommand(@NonNull final Controller2Link controller, final int seq) {
+ final ControllerInfo controllerInfo;
+ synchronized (mLock) {
+ controllerInfo = mConnectedControllers.get(controller);
+ }
+ if (controllerInfo == null) {
+ return;
+ }
+ controllerInfo.removeRequestedCommandSeqNumber(seq);
+ }
+
+ /**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Builder for {@link MediaSession2}.
+ * <p>
+ * Any incoming event from the {@link MediaController2} will be handled on the callback
+ * executor. If it's not set, {@link Context#getMainExecutor()} will be used by default.
+ */
+ public static final class Builder {
+ private Context mContext;
+ private String mId;
+ private PendingIntent mSessionActivity;
+ private Executor mCallbackExecutor;
+ private SessionCallback mCallback;
+ private Bundle mExtras;
+
+ /**
+ * Creates a builder for {@link MediaSession2}.
+ *
+ * @param context Context
+ * @throws IllegalArgumentException if context is {@code null}.
+ */
+ public Builder(@NonNull Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ mContext = context;
+ }
+
+ /**
+ * Set an intent for launching UI for this Session. This can be used as a
+ * quick link to an ongoing media screen. The intent should be for an
+ * activity that may be started using {@link Context#startActivity(Intent)}.
+ *
+ * @param pi The intent to launch to show UI for this session.
+ * @return The Builder to allow chaining
+ */
+ @NonNull
+ public Builder setSessionActivity(@Nullable PendingIntent pi) {
+ mSessionActivity = pi;
+ return this;
+ }
+
+ /**
+ * Set ID of the session. If it's not set, an empty string will be used to create a session.
+ * <p>
+ * Use this if and only if your app supports multiple playback at the same time and also
+ * wants to provide external apps to have finer controls of them.
+ *
+ * @param id id of the session. Must be unique per package.
+ * @throws IllegalArgumentException if id is {@code null}.
+ * @return The Builder to allow chaining
+ */
+ @NonNull
+ public Builder setId(@NonNull String id) {
+ if (id == null) {
+ throw new IllegalArgumentException("id shouldn't be null");
+ }
+ mId = id;
+ return this;
+ }
+
+ /**
+ * Set callback for the session and its executor.
+ *
+ * @param executor callback executor
+ * @param callback session callback.
+ * @return The Builder to allow chaining
+ */
+ @NonNull
+ public Builder setSessionCallback(@NonNull Executor executor,
+ @NonNull SessionCallback callback) {
+ mCallbackExecutor = executor;
+ mCallback = callback;
+ return this;
+ }
+
+ /**
+ * Set extras for the session token. If null or not set, {@link Session2Token#getExtras()}
+ * will return an empty {@link Bundle}. An {@link IllegalArgumentException} will be thrown
+ * if the bundle contains any non-framework Parcelable objects.
+ *
+ * @return The Builder to allow chaining
+ * @see Session2Token#getExtras()
+ */
+ @NonNull
+ public Builder setExtras(@NonNull Bundle extras) {
+ if (extras == null) {
+ throw new NullPointerException("extras shouldn't be null");
+ }
+ if (hasCustomParcelable(extras)) {
+ throw new IllegalArgumentException(
+ "extras shouldn't contain any custom parcelables");
+ }
+ mExtras = new Bundle(extras);
+ return this;
+ }
+
+ /**
+ * Build {@link MediaSession2}.
+ *
+ * @return a new session
+ * @throws IllegalStateException if the session with the same id is already exists for the
+ * package.
+ */
+ @NonNull
+ public MediaSession2 build() {
+ if (mCallbackExecutor == null) {
+ mCallbackExecutor = mContext.getMainExecutor();
+ }
+ if (mCallback == null) {
+ mCallback = new SessionCallback() {};
+ }
+ if (mId == null) {
+ mId = "";
+ }
+ if (mExtras == null) {
+ mExtras = Bundle.EMPTY;
+ }
+ MediaSession2 session2 = new MediaSession2(mContext, mId, mSessionActivity,
+ mCallbackExecutor, mCallback, mExtras);
+
+ // Notify framework about the newly create session after the constructor is finished.
+ // Otherwise, framework may access the session before the initialization is finished.
+ try {
+ MediaSessionManager manager = (MediaSessionManager) mContext.getSystemService(
+ Context.MEDIA_SESSION_SERVICE);
+ manager.notifySession2Created(session2.getToken());
+ } catch (Exception e) {
+ session2.close();
+ throw e;
+ }
+
+ return session2;
+ }
+ }
+
+ /**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Information of a controller.
+ */
+ public static final class ControllerInfo {
+ private final RemoteUserInfo mRemoteUserInfo;
+ private final boolean mIsTrusted;
+ private final Controller2Link mControllerBinder;
+ private final Bundle mConnectionHints;
+ private final Object mLock = new Object();
+ //@GuardedBy("mLock")
+ private int mNextSeqNumber;
+ //@GuardedBy("mLock")
+ private ArrayMap<ResultReceiver, Integer> mPendingCommands;
+ //@GuardedBy("mLock")
+ private ArraySet<Integer> mRequestedCommandSeqNumbers;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ Session2CommandGroup mAllowedCommands;
+
+ /**
+ * @param remoteUserInfo remote user info
+ * @param trusted {@code true} if trusted, {@code false} otherwise
+ * @param controllerBinder Controller2Link for the connected controller.
+ * @param connectionHints a session-specific argument sent from the controller for the
+ * connection. The contents of this bundle may affect the
+ * connection result.
+ */
+ ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted,
+ @Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints) {
+ mRemoteUserInfo = remoteUserInfo;
+ mIsTrusted = trusted;
+ mControllerBinder = controllerBinder;
+ mConnectionHints = connectionHints;
+ mPendingCommands = new ArrayMap<>();
+ mRequestedCommandSeqNumbers = new ArraySet<>();
+ }
+
+ /**
+ * @return remote user info of the controller.
+ */
+ @NonNull
+ public RemoteUserInfo getRemoteUserInfo() {
+ return mRemoteUserInfo;
+ }
+
+ /**
+ * @return package name of the controller.
+ */
+ @NonNull
+ public String getPackageName() {
+ return mRemoteUserInfo.getPackageName();
+ }
+
+ /**
+ * @return uid of the controller. Can be a negative value if the uid cannot be obtained.
+ */
+ public int getUid() {
+ return mRemoteUserInfo.getUid();
+ }
+
+ /**
+ * @return connection hints sent from controller.
+ */
+ @NonNull
+ public Bundle getConnectionHints() {
+ return new Bundle(mConnectionHints);
+ }
+
+ /**
+ * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or
+ * has a enabled notification listener so can be trusted to accept connection and incoming
+ * command request.
+ *
+ * @return {@code true} if the controller is trusted.
+ * @hide
+ */
+ public boolean isTrusted() {
+ return mIsTrusted;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mControllerBinder, mRemoteUserInfo);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof ControllerInfo)) return false;
+ if (this == obj) return true;
+
+ ControllerInfo other = (ControllerInfo) obj;
+ if (mControllerBinder != null || other.mControllerBinder != null) {
+ return Objects.equals(mControllerBinder, other.mControllerBinder);
+ }
+ return mRemoteUserInfo.equals(other.mRemoteUserInfo);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "ControllerInfo {pkg=" + mRemoteUserInfo.getPackageName() + ", uid="
+ + mRemoteUserInfo.getUid() + ", allowedCommands=" + mAllowedCommands + "})";
+ }
+
+ void notifyConnected(Bundle connectionResult) {
+ if (mControllerBinder == null) return;
+
+ try {
+ mControllerBinder.notifyConnected(getNextSeqNumber(), connectionResult);
+ } catch (RuntimeException e) {
+ // Controller may be died prematurely.
+ }
+ }
+
+ void notifyDisconnected() {
+ if (mControllerBinder == null) return;
+
+ try {
+ mControllerBinder.notifyDisconnected(getNextSeqNumber());
+ } catch (RuntimeException e) {
+ // Controller may be died prematurely.
+ }
+ }
+
+ void notifyPlaybackActiveChanged(boolean playbackActive) {
+ if (mControllerBinder == null) return;
+
+ try {
+ mControllerBinder.notifyPlaybackActiveChanged(getNextSeqNumber(), playbackActive);
+ } catch (RuntimeException e) {
+ // Controller may be died prematurely.
+ }
+ }
+
+ void sendSessionCommand(Session2Command command, Bundle args,
+ ResultReceiver resultReceiver) {
+ if (mControllerBinder == null) return;
+
+ try {
+ int seq = getNextSeqNumber();
+ synchronized (mLock) {
+ mPendingCommands.put(resultReceiver, seq);
+ }
+ mControllerBinder.sendSessionCommand(seq, command, args, resultReceiver);
+ } catch (RuntimeException e) {
+ // Controller may be died prematurely.
+ synchronized (mLock) {
+ mPendingCommands.remove(resultReceiver);
+ }
+ resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null);
+ }
+ }
+
+ void cancelSessionCommand(@NonNull Object token) {
+ if (mControllerBinder == null) return;
+ Integer seq;
+ synchronized (mLock) {
+ seq = mPendingCommands.remove(token);
+ }
+ if (seq != null) {
+ mControllerBinder.cancelSessionCommand(seq);
+ }
+ }
+
+ void receiveCommandResult(ResultReceiver resultReceiver) {
+ synchronized (mLock) {
+ mPendingCommands.remove(resultReceiver);
+ }
+ }
+
+ void addRequestedCommandSeqNumber(int seq) {
+ synchronized (mLock) {
+ mRequestedCommandSeqNumbers.add(seq);
+ }
+ }
+
+ boolean removeRequestedCommandSeqNumber(int seq) {
+ synchronized (mLock) {
+ return mRequestedCommandSeqNumbers.remove(seq);
+ }
+ }
+
+ private int getNextSeqNumber() {
+ synchronized (mLock) {
+ return mNextSeqNumber++;
+ }
+ }
+ }
+
+ /**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Callback to be called for all incoming commands from {@link MediaController2}s.
+ */
+ public abstract static class SessionCallback {
+ /**
+ * Called when a controller is created for this session. Return allowed commands for
+ * controller. By default it returns {@code null}.
+ * <p>
+ * You can reject the connection by returning {@code null}. In that case, controller
+ * receives {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)}
+ * and cannot be used.
+ * <p>
+ * The controller hasn't connected yet in this method, so calls to the controller
+ * (e.g. {@link #sendSessionCommand}) would be ignored. Override {@link #onPostConnect} for
+ * the custom initialization for the controller instead.
+ *
+ * @param session the session for this event
+ * @param controller controller information.
+ * @return allowed commands. Can be {@code null} to reject connection.
+ */
+ @Nullable
+ public Session2CommandGroup onConnect(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ return null;
+ }
+
+ /**
+ * Called immediately after a controller is connected. This is a convenient method to add
+ * custom initialization between the session and a controller.
+ * <p>
+ * Note that calls to the controller (e.g. {@link #sendSessionCommand}) work here but don't
+ * work in {@link #onConnect} because the controller hasn't connected yet in
+ * {@link #onConnect}.
+ *
+ * @param session the session for this event
+ * @param controller controller information.
+ */
+ public void onPostConnect(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ }
+
+ /**
+ * Called when a controller is disconnected
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ */
+ public void onDisconnected(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {}
+
+ /**
+ * Called when a controller sent a session command.
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param command the session command
+ * @param args optional arguments
+ * @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED
+ * will be sent to the session.
+ */
+ @Nullable
+ public Session2Command.Result onSessionCommand(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull Session2Command command,
+ @Nullable Bundle args) {
+ return null;
+ }
+
+ /**
+ * Called when the command sent to the controller is finished.
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param token the token got from {@link MediaSession2#sendSessionCommand}
+ * @param command the session command
+ * @param result the result of the session command
+ */
+ public void onCommandResult(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull Object token,
+ @NonNull Session2Command command, @NonNull Session2Command.Result result) {}
+ }
+
+ abstract static class ForegroundServiceEventCallback {
+ public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {}
+ public void onSessionClosed(MediaSession2 session) {}
+ }
+}
diff --git a/apex/media/framework/java/android/media/MediaSession2Service.java b/apex/media/framework/java/android/media/MediaSession2Service.java
new file mode 100644
index 000000000000..f6fd509fd245
--- /dev/null
+++ b/apex/media/framework/java/android/media/MediaSession2Service.java
@@ -0,0 +1,452 @@
+/*
+ * 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.media;
+
+import static android.media.MediaConstants.KEY_CONNECTION_HINTS;
+import static android.media.MediaConstants.KEY_PACKAGE_NAME;
+import static android.media.MediaConstants.KEY_PID;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.session.MediaSessionManager;
+import android.media.session.MediaSessionManager.RemoteUserInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Service containing {@link MediaSession2}.
+ */
+public abstract class MediaSession2Service extends Service {
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service";
+
+ private static final String TAG = "MediaSession2Service";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final MediaSession2.ForegroundServiceEventCallback mForegroundServiceEventCallback =
+ new MediaSession2.ForegroundServiceEventCallback() {
+ @Override
+ public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {
+ MediaSession2Service.this.onPlaybackActiveChanged(session, playbackActive);
+ }
+
+ @Override
+ public void onSessionClosed(MediaSession2 session) {
+ removeSession(session);
+ }
+ };
+
+ private final Object mLock = new Object();
+ //@GuardedBy("mLock")
+ private NotificationManager mNotificationManager;
+ //@GuardedBy("mLock")
+ private MediaSessionManager mMediaSessionManager;
+ //@GuardedBy("mLock")
+ private Intent mStartSelfIntent;
+ //@GuardedBy("mLock")
+ private Map<String, MediaSession2> mSessions = new ArrayMap<>();
+ //@GuardedBy("mLock")
+ private Map<MediaSession2, MediaNotification> mNotifications = new ArrayMap<>();
+ //@GuardedBy("mLock")
+ private MediaSession2ServiceStub mStub;
+
+ /**
+ * Called by the system when the service is first created. Do not call this method directly.
+ * <p>
+ * Override this method if you need your own initialization. Derived classes MUST call through
+ * to the super class's implementation of this method.
+ */
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ synchronized (mLock) {
+ mStub = new MediaSession2ServiceStub(this);
+ mStartSelfIntent = new Intent(this, this.getClass());
+ mNotificationManager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ mMediaSessionManager =
+ (MediaSessionManager) getSystemService(Context.MEDIA_SESSION_SERVICE);
+ }
+ }
+
+ @CallSuper
+ @Override
+ @Nullable
+ public IBinder onBind(@NonNull Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ synchronized (mLock) {
+ return mStub;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by the system to notify that it is no longer used and is being removed. Do not call
+ * this method directly.
+ * <p>
+ * Override this method if you need your own clean up. Derived classes MUST call through
+ * to the super class's implementation of this method.
+ */
+ @CallSuper
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ synchronized (mLock) {
+ List<MediaSession2> sessions = getSessions();
+ for (MediaSession2 session : sessions) {
+ removeSession(session);
+ }
+ mSessions.clear();
+ mNotifications.clear();
+ }
+ mStub.close();
+ }
+
+ /**
+ * Called when a {@link MediaController2} is created with the this service's
+ * {@link Session2Token}. Return the session for telling the controller which session to
+ * connect. Return {@code null} to reject the connection from this controller.
+ * <p>
+ * Session returned here will be added to this service automatically. You don't need to call
+ * {@link #addSession(MediaSession2)} for that.
+ * <p>
+ * This method is always called on the main thread.
+ *
+ * @param controllerInfo information of the controller which is trying to connect.
+ * @return a {@link MediaSession2} instance for the controller to connect to, or {@code null}
+ * to reject connection
+ * @see MediaSession2.Builder
+ * @see #getSessions()
+ */
+ @Nullable
+ public abstract MediaSession2 onGetSession(@NonNull ControllerInfo controllerInfo);
+
+ /**
+ * Called when notification UI needs update. Override this method to show or cancel your own
+ * notification UI.
+ * <p>
+ * This would be called on {@link MediaSession2}'s callback executor when playback state is
+ * changed.
+ * <p>
+ * With the notification returned here, the service becomes foreground service when the playback
+ * is started. Apps must request the permission
+ * {@link android.Manifest.permission#FOREGROUND_SERVICE} in order to use this API. It becomes
+ * background service after the playback is stopped.
+ *
+ * @param session a session that needs notification update.
+ * @return a {@link MediaNotification}. Can be {@code null}.
+ */
+ @Nullable
+ public abstract MediaNotification onUpdateNotification(@NonNull MediaSession2 session);
+
+ /**
+ * Adds a session to this service.
+ * <p>
+ * Added session will be removed automatically when it's closed, or removed when
+ * {@link #removeSession} is called.
+ *
+ * @param session a session to be added.
+ * @see #removeSession(MediaSession2)
+ */
+ public final void addSession(@NonNull MediaSession2 session) {
+ if (session == null) {
+ throw new IllegalArgumentException("session shouldn't be null");
+ }
+ if (session.isClosed()) {
+ throw new IllegalArgumentException("session is already closed");
+ }
+ synchronized (mLock) {
+ MediaSession2 previousSession = mSessions.get(session.getId());
+ if (previousSession != null) {
+ if (previousSession != session) {
+ Log.w(TAG, "Session ID should be unique, ID=" + session.getId()
+ + ", previous=" + previousSession + ", session=" + session);
+ }
+ return;
+ }
+ mSessions.put(session.getId(), session);
+ session.setForegroundServiceEventCallback(mForegroundServiceEventCallback);
+ }
+ }
+
+ /**
+ * Removes a session from this service.
+ *
+ * @param session a session to be removed.
+ * @see #addSession(MediaSession2)
+ */
+ public final void removeSession(@NonNull MediaSession2 session) {
+ if (session == null) {
+ throw new IllegalArgumentException("session shouldn't be null");
+ }
+ MediaNotification notification;
+ synchronized (mLock) {
+ if (mSessions.get(session.getId()) != session) {
+ // Session isn't added or removed already.
+ return;
+ }
+ mSessions.remove(session.getId());
+ notification = mNotifications.remove(session);
+ }
+ session.setForegroundServiceEventCallback(null);
+ if (notification != null) {
+ mNotificationManager.cancel(notification.getNotificationId());
+ }
+ if (getSessions().isEmpty()) {
+ stopForeground(false);
+ }
+ }
+
+ /**
+ * Gets the list of {@link MediaSession2}s that you've added to this service.
+ *
+ * @return sessions
+ */
+ public final @NonNull List<MediaSession2> getSessions() {
+ List<MediaSession2> list = new ArrayList<>();
+ synchronized (mLock) {
+ list.addAll(mSessions.values());
+ }
+ return list;
+ }
+
+ /**
+ * Returns the {@link MediaSessionManager}.
+ */
+ @NonNull
+ MediaSessionManager getMediaSessionManager() {
+ synchronized (mLock) {
+ return mMediaSessionManager;
+ }
+ }
+
+ /**
+ * Called by registered {@link MediaSession2.ForegroundServiceEventCallback}
+ *
+ * @param session session with change
+ * @param playbackActive {@code true} if playback is active.
+ */
+ void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {
+ MediaNotification mediaNotification = onUpdateNotification(session);
+ if (mediaNotification == null) {
+ // The service implementation doesn't want to use the automatic start/stopForeground
+ // feature.
+ return;
+ }
+ synchronized (mLock) {
+ mNotifications.put(session, mediaNotification);
+ }
+ int id = mediaNotification.getNotificationId();
+ Notification notification = mediaNotification.getNotification();
+ if (!playbackActive) {
+ mNotificationManager.notify(id, notification);
+ return;
+ }
+ // playbackActive == true
+ startForegroundService(mStartSelfIntent);
+ startForeground(id, notification);
+ }
+
+ /**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Returned by {@link #onUpdateNotification(MediaSession2)} for making session service
+ * foreground service to keep playback running in the background. It's highly recommended to
+ * show media style notification here.
+ */
+ public static class MediaNotification {
+ private final int mNotificationId;
+ private final Notification mNotification;
+
+ /**
+ * Default constructor
+ *
+ * @param notificationId notification id to be used for
+ * {@link NotificationManager#notify(int, Notification)}.
+ * @param notification a notification to make session service run in the foreground. Media
+ * style notification is recommended here.
+ */
+ public MediaNotification(int notificationId, @NonNull Notification notification) {
+ if (notification == null) {
+ throw new IllegalArgumentException("notification shouldn't be null");
+ }
+ mNotificationId = notificationId;
+ mNotification = notification;
+ }
+
+ /**
+ * Gets the id of the notification.
+ *
+ * @return the notification id
+ */
+ public int getNotificationId() {
+ return mNotificationId;
+ }
+
+ /**
+ * Gets the notification.
+ *
+ * @return the notification
+ */
+ @NonNull
+ public Notification getNotification() {
+ return mNotification;
+ }
+ }
+
+ private static final class MediaSession2ServiceStub extends IMediaSession2Service.Stub
+ implements AutoCloseable {
+ final WeakReference<MediaSession2Service> mService;
+ final Handler mHandler;
+
+ MediaSession2ServiceStub(MediaSession2Service service) {
+ mService = new WeakReference<>(service);
+ mHandler = new Handler(service.getMainLooper());
+ }
+
+ @Override
+ public void connect(Controller2Link caller, int seq, Bundle connectionRequest) {
+ if (mService.get() == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Service is already destroyed");
+ }
+ return;
+ }
+ if (caller == null || connectionRequest == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Ignoring calls with illegal arguments, caller=" + caller
+ + ", connectionRequest=" + connectionRequest);
+ }
+ return;
+ }
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mHandler.post(() -> {
+ boolean shouldNotifyDisconnected = true;
+ try {
+ final MediaSession2Service service = mService.get();
+ if (service == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Service isn't available");
+ }
+ return;
+ }
+
+ String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME);
+ // The Binder.getCallingPid() can be 0 for an oneway call from the
+ // remote process. If it's the case, use PID from the connectionRequest.
+ RemoteUserInfo remoteUserInfo = new RemoteUserInfo(
+ callingPkg,
+ pid == 0 ? connectionRequest.getInt(KEY_PID) : pid,
+ uid);
+
+ Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS);
+ if (connectionHints == null) {
+ Log.w(TAG, "connectionHints shouldn't be null.");
+ connectionHints = Bundle.EMPTY;
+ } else if (MediaSession2.hasCustomParcelable(connectionHints)) {
+ Log.w(TAG, "connectionHints contain custom parcelable. Ignoring.");
+ connectionHints = Bundle.EMPTY;
+ }
+
+ final ControllerInfo controllerInfo = new ControllerInfo(
+ remoteUserInfo,
+ service.getMediaSessionManager()
+ .isTrustedForMediaControl(remoteUserInfo),
+ caller,
+ connectionHints);
+
+ if (DEBUG) {
+ Log.d(TAG, "Handling incoming connection request from the"
+ + " controller=" + controllerInfo);
+ }
+
+ final MediaSession2 session;
+ session = service.onGetSession(controllerInfo);
+
+ if (session == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Rejecting incoming connection request from the"
+ + " controller=" + controllerInfo);
+ }
+ // Note: Trusted controllers also can be rejected according to the
+ // service implementation.
+ return;
+ }
+ service.addSession(session);
+ shouldNotifyDisconnected = false;
+ session.onConnect(caller, pid, uid, seq, connectionRequest);
+ } catch (Exception e) {
+ // Don't propagate exception in service to the controller.
+ Log.w(TAG, "Failed to add a session to session service", e);
+ } finally {
+ // Trick to call onDisconnected() in one place.
+ if (shouldNotifyDisconnected) {
+ if (DEBUG) {
+ Log.d(TAG, "Notifying the controller of its disconnection");
+ }
+ try {
+ caller.notifyDisconnected(0);
+ } catch (RuntimeException e) {
+ // Controller may be died prematurely.
+ // Not an issue because we'll ignore it anyway.
+ }
+ }
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void close() {
+ mHandler.removeCallbacksAndMessages(null);
+ mService.clear();
+ }
+ }
+}
diff --git a/apex/media/framework/java/android/media/ProxyDataSourceCallback.java b/apex/media/framework/java/android/media/ProxyDataSourceCallback.java
new file mode 100644
index 000000000000..14d3ce87f03d
--- /dev/null
+++ b/apex/media/framework/java/android/media/ProxyDataSourceCallback.java
@@ -0,0 +1,68 @@
+/*
+ * 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.media;
+
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * A DataSourceCallback that is backed by a ParcelFileDescriptor.
+ */
+class ProxyDataSourceCallback extends DataSourceCallback {
+ private static final String TAG = "TestDataSourceCallback";
+
+ ParcelFileDescriptor mPFD;
+ FileDescriptor mFD;
+
+ ProxyDataSourceCallback(ParcelFileDescriptor pfd) throws IOException {
+ mPFD = pfd.dup();
+ mFD = mPFD.getFileDescriptor();
+ }
+
+ @Override
+ public synchronized int readAt(long position, byte[] buffer, int offset, int size)
+ throws IOException {
+ try {
+ Os.lseek(mFD, position, OsConstants.SEEK_SET);
+ int ret = Os.read(mFD, buffer, offset, size);
+ return (ret == 0) ? END_OF_STREAM : ret;
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public synchronized long getSize() throws IOException {
+ return mPFD.getStatSize();
+ }
+
+ @Override
+ public synchronized void close() {
+ try {
+ mPFD.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to close the PFD.", e);
+ }
+ }
+}
+
diff --git a/apex/media/framework/java/android/media/RoutingDelegate.java b/apex/media/framework/java/android/media/RoutingDelegate.java
new file mode 100644
index 000000000000..23598130f391
--- /dev/null
+++ b/apex/media/framework/java/android/media/RoutingDelegate.java
@@ -0,0 +1,48 @@
+/*
+ * 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.media;
+
+import android.os.Handler;
+
+class RoutingDelegate implements AudioRouting.OnRoutingChangedListener {
+ private AudioRouting mAudioRouting;
+ private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener;
+ private Handler mHandler;
+
+ RoutingDelegate(final AudioRouting audioRouting,
+ final AudioRouting.OnRoutingChangedListener listener,
+ Handler handler) {
+ mAudioRouting = audioRouting;
+ mOnRoutingChangedListener = listener;
+ mHandler = handler;
+ }
+
+ public AudioRouting.OnRoutingChangedListener getListener() {
+ return mOnRoutingChangedListener;
+ }
+
+ public Handler getHandler() {
+ return mHandler;
+ }
+
+ @Override
+ public void onRoutingChanged(AudioRouting router) {
+ if (mOnRoutingChangedListener != null) {
+ mOnRoutingChangedListener.onRoutingChanged(mAudioRouting);
+ }
+ }
+}
diff --git a/apex/media/framework/java/android/media/Session2Command.java b/apex/media/framework/java/android/media/Session2Command.java
new file mode 100644
index 000000000000..26f4568fa7e5
--- /dev/null
+++ b/apex/media/framework/java/android/media/Session2Command.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Define a command that a {@link MediaController2} can send to a {@link MediaSession2}.
+ * <p>
+ * If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command.
+ * If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and
+ * {@link #getCustomAction()} shouldn't be {@code null}.
+ * <p>
+ * Refer to the
+ * <a href="{@docRoot}reference/androidx/media2/SessionCommand2.html">AndroidX SessionCommand</a>
+ * class for the list of valid commands.
+ */
+public final class Session2Command implements Parcelable {
+ /**
+ * Command code for the custom command which can be defined by string action in the
+ * {@link Session2Command}.
+ */
+ public static final int COMMAND_CODE_CUSTOM = 0;
+
+ public static final @android.annotation.NonNull Parcelable.Creator<Session2Command> CREATOR =
+ new Parcelable.Creator<Session2Command>() {
+ @Override
+ public Session2Command createFromParcel(Parcel in) {
+ return new Session2Command(in);
+ }
+
+ @Override
+ public Session2Command[] newArray(int size) {
+ return new Session2Command[size];
+ }
+ };
+
+ private final int mCommandCode;
+ // Nonnull if it's custom command
+ private final String mCustomAction;
+ private final Bundle mCustomExtras;
+
+ /**
+ * Constructor for creating a command predefined in AndroidX media2.
+ *
+ * @param commandCode A command code for a command predefined in AndroidX media2.
+ */
+ public Session2Command(int commandCode) {
+ if (commandCode == COMMAND_CODE_CUSTOM) {
+ throw new IllegalArgumentException("commandCode shouldn't be COMMAND_CODE_CUSTOM");
+ }
+ mCommandCode = commandCode;
+ mCustomAction = null;
+ mCustomExtras = null;
+ }
+
+ /**
+ * Constructor for creating a custom command.
+ *
+ * @param action The action of this custom command.
+ * @param extras An extra bundle for this custom command.
+ */
+ public Session2Command(@NonNull String action, @Nullable Bundle extras) {
+ if (action == null) {
+ throw new IllegalArgumentException("action shouldn't be null");
+ }
+ mCommandCode = COMMAND_CODE_CUSTOM;
+ mCustomAction = action;
+ mCustomExtras = extras;
+ }
+
+ /**
+ * Used by parcelable creator.
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ Session2Command(Parcel in) {
+ mCommandCode = in.readInt();
+ mCustomAction = in.readString();
+ mCustomExtras = in.readBundle();
+ }
+
+ /**
+ * Gets the command code of a predefined command.
+ * This will return {@link #COMMAND_CODE_CUSTOM} for a custom command.
+ */
+ public int getCommandCode() {
+ return mCommandCode;
+ }
+
+ /**
+ * Gets the action of a custom command.
+ * This will return {@code null} for a predefined command.
+ */
+ @Nullable
+ public String getCustomAction() {
+ return mCustomAction;
+ }
+
+ /**
+ * Gets the extra bundle of a custom command.
+ * This will return {@code null} for a predefined command.
+ */
+ @Nullable
+ public Bundle getCustomExtras() {
+ return mCustomExtras;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ if (dest == null) {
+ throw new IllegalArgumentException("parcel shouldn't be null");
+ }
+ dest.writeInt(mCommandCode);
+ dest.writeString(mCustomAction);
+ dest.writeBundle(mCustomExtras);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof Session2Command)) {
+ return false;
+ }
+ Session2Command other = (Session2Command) obj;
+ return mCommandCode == other.mCommandCode
+ && TextUtils.equals(mCustomAction, other.mCustomAction);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCustomAction, mCommandCode);
+ }
+
+ /**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Contains the result of {@link Session2Command}.
+ */
+ public static final class Result {
+ private final int mResultCode;
+ private final Bundle mResultData;
+
+ /**
+ * Result code representing that the command is skipped or canceled. For an example, a seek
+ * command can be skipped if it is followed by another seek command.
+ */
+ public static final int RESULT_INFO_SKIPPED = 1;
+
+ /**
+ * Result code representing that the command is successfully completed.
+ */
+ public static final int RESULT_SUCCESS = 0;
+
+ /**
+ * Result code represents that call is ended with an unknown error.
+ */
+ public static final int RESULT_ERROR_UNKNOWN_ERROR = -1;
+
+ /**
+ * Constructor of {@link Result}.
+ *
+ * @param resultCode result code
+ * @param resultData result data
+ */
+ public Result(int resultCode, @Nullable Bundle resultData) {
+ mResultCode = resultCode;
+ mResultData = resultData;
+ }
+
+ /**
+ * Returns the result code.
+ */
+ public int getResultCode() {
+ return mResultCode;
+ }
+
+ /**
+ * Returns the result data.
+ */
+ @Nullable
+ public Bundle getResultData() {
+ return mResultData;
+ }
+ }
+}
diff --git a/apex/media/framework/java/android/media/Session2CommandGroup.java b/apex/media/framework/java/android/media/Session2CommandGroup.java
new file mode 100644
index 000000000000..13aabfc45ab7
--- /dev/null
+++ b/apex/media/framework/java/android/media/Session2CommandGroup.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 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.media;
+
+import static android.media.Session2Command.COMMAND_CODE_CUSTOM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * A set of {@link Session2Command} which represents a command group.
+ */
+public final class Session2CommandGroup implements Parcelable {
+ private static final String TAG = "Session2CommandGroup";
+
+ public static final @android.annotation.NonNull Parcelable.Creator<Session2CommandGroup>
+ CREATOR = new Parcelable.Creator<Session2CommandGroup>() {
+ @Override
+ public Session2CommandGroup createFromParcel(Parcel in) {
+ return new Session2CommandGroup(in);
+ }
+
+ @Override
+ public Session2CommandGroup[] newArray(int size) {
+ return new Session2CommandGroup[size];
+ }
+ };
+
+ Set<Session2Command> mCommands = new HashSet<>();
+
+ /**
+ * Creates a new Session2CommandGroup with commands copied from another object.
+ *
+ * @param commands The collection of commands to copy.
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ Session2CommandGroup(@Nullable Collection<Session2Command> commands) {
+ if (commands != null) {
+ mCommands.addAll(commands);
+ }
+ }
+
+ /**
+ * Used by parcelable creator.
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ Session2CommandGroup(Parcel in) {
+ Parcelable[] commands = in.readParcelableArray(Session2Command.class.getClassLoader());
+ if (commands != null) {
+ for (Parcelable command : commands) {
+ mCommands.add((Session2Command) command);
+ }
+ }
+ }
+
+ /**
+ * Checks whether this command group has a command that matches given {@code command}.
+ *
+ * @param command A command to find. Shouldn't be {@code null}.
+ */
+ public boolean hasCommand(@NonNull Session2Command command) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ return mCommands.contains(command);
+ }
+
+ /**
+ * Checks whether this command group has a command that matches given {@code commandCode}.
+ *
+ * @param commandCode A command code to find.
+ * Shouldn't be {@link Session2Command#COMMAND_CODE_CUSTOM}.
+ */
+ public boolean hasCommand(int commandCode) {
+ if (commandCode == COMMAND_CODE_CUSTOM) {
+ throw new IllegalArgumentException("Use hasCommand(Command) for custom command");
+ }
+ for (Session2Command command : mCommands) {
+ if (command.getCommandCode() == commandCode) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets all commands of this command group.
+ */
+ @NonNull
+ public Set<Session2Command> getCommands() {
+ return new HashSet<>(mCommands);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ if (dest == null) {
+ throw new IllegalArgumentException("parcel shouldn't be null");
+ }
+ dest.writeParcelableArray(mCommands.toArray(new Session2Command[0]), 0);
+ }
+
+ /**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Builds a {@link Session2CommandGroup} object.
+ */
+ public static final class Builder {
+ private Set<Session2Command> mCommands;
+
+ public Builder() {
+ mCommands = new HashSet<>();
+ }
+
+ /**
+ * Creates a new builder for {@link Session2CommandGroup} with commands copied from another
+ * {@link Session2CommandGroup} object.
+ * @param commandGroup
+ */
+ public Builder(@NonNull Session2CommandGroup commandGroup) {
+ if (commandGroup == null) {
+ throw new IllegalArgumentException("command group shouldn't be null");
+ }
+ mCommands = commandGroup.getCommands();
+ }
+
+ /**
+ * Adds a command to this command group.
+ *
+ * @param command A command to add. Shouldn't be {@code null}.
+ */
+ @NonNull
+ public Builder addCommand(@NonNull Session2Command command) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ mCommands.add(command);
+ return this;
+ }
+
+ /**
+ * Removes a command from this group which matches given {@code command}.
+ *
+ * @param command A command to find. Shouldn't be {@code null}.
+ */
+ @NonNull
+ public Builder removeCommand(@NonNull Session2Command command) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ mCommands.remove(command);
+ return this;
+ }
+
+ /**
+ * Builds {@link Session2CommandGroup}.
+ *
+ * @return a new {@link Session2CommandGroup}.
+ */
+ @NonNull
+ public Session2CommandGroup build() {
+ return new Session2CommandGroup(mCommands);
+ }
+ }
+}
diff --git a/apex/media/framework/java/android/media/Session2Link.java b/apex/media/framework/java/android/media/Session2Link.java
new file mode 100644
index 000000000000..6e550e86a9fe
--- /dev/null
+++ b/apex/media/framework/java/android/media/Session2Link.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import java.util.Objects;
+
+/**
+ * Handles incoming commands from {@link MediaController2} to {@link MediaSession2}.
+ * @hide
+ */
+// @SystemApi
+public final class Session2Link implements Parcelable {
+ private static final String TAG = "Session2Link";
+ private static final boolean DEBUG = MediaSession2.DEBUG;
+
+ public static final @android.annotation.NonNull Parcelable.Creator<Session2Link> CREATOR =
+ new Parcelable.Creator<Session2Link>() {
+ @Override
+ public Session2Link createFromParcel(Parcel in) {
+ return new Session2Link(in);
+ }
+
+ @Override
+ public Session2Link[] newArray(int size) {
+ return new Session2Link[size];
+ }
+ };
+
+ private final MediaSession2 mSession;
+ private final IMediaSession2 mISession;
+
+ public Session2Link(MediaSession2 session) {
+ mSession = session;
+ mISession = new Session2Stub();
+ }
+
+ Session2Link(Parcel in) {
+ mSession = null;
+ mISession = IMediaSession2.Stub.asInterface(in.readStrongBinder());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mISession.asBinder());
+ }
+
+ @Override
+ public int hashCode() {
+ return mISession.asBinder().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Session2Link)) {
+ return false;
+ }
+ Session2Link other = (Session2Link) obj;
+ return Objects.equals(mISession.asBinder(), other.mISession.asBinder());
+ }
+
+ /** Link to death with mISession */
+ public void linkToDeath(@NonNull IBinder.DeathRecipient recipient, int flags) {
+ if (mISession != null) {
+ try {
+ mISession.asBinder().linkToDeath(recipient, flags);
+ } catch (RemoteException e) {
+ if (DEBUG) {
+ Log.d(TAG, "Session died too early.", e);
+ }
+ }
+ }
+ }
+
+ /** Unlink to death with mISession */
+ public boolean unlinkToDeath(@NonNull IBinder.DeathRecipient recipient, int flags) {
+ if (mISession != null) {
+ return mISession.asBinder().unlinkToDeath(recipient, flags);
+ }
+ return true;
+ }
+
+ /** Interface method for IMediaSession2.connect */
+ public void connect(final Controller2Link caller, int seq, Bundle connectionRequest) {
+ try {
+ mISession.connect(caller, seq, connectionRequest);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Interface method for IMediaSession2.disconnect */
+ public void disconnect(final Controller2Link caller, int seq) {
+ try {
+ mISession.disconnect(caller, seq);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Interface method for IMediaSession2.sendSessionCommand */
+ public void sendSessionCommand(final Controller2Link caller, final int seq,
+ final Session2Command command, final Bundle args, ResultReceiver resultReceiver) {
+ try {
+ mISession.sendSessionCommand(caller, seq, command, args, resultReceiver);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Interface method for IMediaSession2.sendSessionCommand */
+ public void cancelSessionCommand(final Controller2Link caller, final int seq) {
+ try {
+ mISession.cancelSessionCommand(caller, seq);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Stub implementation for IMediaSession2.connect */
+ public void onConnect(final Controller2Link caller, int pid, int uid, int seq,
+ Bundle connectionRequest) {
+ mSession.onConnect(caller, pid, uid, seq, connectionRequest);
+ }
+
+ /** Stub implementation for IMediaSession2.disconnect */
+ public void onDisconnect(final Controller2Link caller, int seq) {
+ mSession.onDisconnect(caller, seq);
+ }
+
+ /** Stub implementation for IMediaSession2.sendSessionCommand */
+ public void onSessionCommand(final Controller2Link caller, final int seq,
+ final Session2Command command, final Bundle args, ResultReceiver resultReceiver) {
+ mSession.onSessionCommand(caller, seq, command, args, resultReceiver);
+ }
+
+ /** Stub implementation for IMediaSession2.cancelSessionCommand */
+ public void onCancelCommand(final Controller2Link caller, final int seq) {
+ mSession.onCancelCommand(caller, seq);
+ }
+
+ private class Session2Stub extends IMediaSession2.Stub {
+ @Override
+ public void connect(final Controller2Link caller, int seq, Bundle connectionRequest) {
+ if (caller == null || connectionRequest == null) {
+ return;
+ }
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Session2Link.this.onConnect(caller, pid, uid, seq, connectionRequest);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void disconnect(final Controller2Link caller, int seq) {
+ if (caller == null) {
+ return;
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Session2Link.this.onDisconnect(caller, seq);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void sendSessionCommand(final Controller2Link caller, final int seq,
+ final Session2Command command, final Bundle args, ResultReceiver resultReceiver) {
+ if (caller == null) {
+ return;
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Session2Link.this.onSessionCommand(caller, seq, command, args, resultReceiver);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void cancelSessionCommand(final Controller2Link caller, final int seq) {
+ if (caller == null) {
+ return;
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Session2Link.this.onCancelCommand(caller, seq);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+}
diff --git a/apex/media/framework/java/android/media/Session2Token.java b/apex/media/framework/java/android/media/Session2Token.java
new file mode 100644
index 000000000000..aae2e1bcb6df
--- /dev/null
+++ b/apex/media/framework/java/android/media/Session2Token.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+ * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+ * Library</a> for consistent behavior across all devices.
+ * <p>
+ * Represents an ongoing {@link MediaSession2} or a {@link MediaSession2Service}.
+ * If it's representing a session service, it may not be ongoing.
+ * <p>
+ * This may be passed to apps by the session owner to allow them to create a
+ * {@link MediaController2} to communicate with the session.
+ * <p>
+ * It can be also obtained by {@link android.media.session.MediaSessionManager}.
+ */
+public final class Session2Token implements Parcelable {
+ private static final String TAG = "Session2Token";
+
+ public static final @android.annotation.NonNull Creator<Session2Token> CREATOR =
+ new Creator<Session2Token>() {
+ @Override
+ public Session2Token createFromParcel(Parcel p) {
+ return new Session2Token(p);
+ }
+
+ @Override
+ public Session2Token[] newArray(int size) {
+ return new Session2Token[size];
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "TYPE_", value = {TYPE_SESSION, TYPE_SESSION_SERVICE})
+ public @interface TokenType {
+ }
+
+ /**
+ * Type for {@link MediaSession2}.
+ */
+ public static final int TYPE_SESSION = 0;
+
+ /**
+ * Type for {@link MediaSession2Service}.
+ */
+ public static final int TYPE_SESSION_SERVICE = 1;
+
+ private final int mUid;
+ @TokenType
+ private final int mType;
+ private final String mPackageName;
+ private final String mServiceName;
+ private final Session2Link mSessionLink;
+ private final ComponentName mComponentName;
+ private final Bundle mExtras;
+
+ /**
+ * Constructor for the token with type {@link #TYPE_SESSION_SERVICE}.
+ *
+ * @param context The context.
+ * @param serviceComponent The component name of the service.
+ */
+ public Session2Token(@NonNull Context context, @NonNull ComponentName serviceComponent) {
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ if (serviceComponent == null) {
+ throw new IllegalArgumentException("serviceComponent shouldn't be null");
+ }
+
+ final PackageManager manager = context.getPackageManager();
+ final int uid = getUid(manager, serviceComponent.getPackageName());
+
+ if (!isInterfaceDeclared(manager, MediaSession2Service.SERVICE_INTERFACE,
+ serviceComponent)) {
+ Log.w(TAG, serviceComponent + " doesn't implement MediaSession2Service.");
+ }
+ mComponentName = serviceComponent;
+ mPackageName = serviceComponent.getPackageName();
+ mServiceName = serviceComponent.getClassName();
+ mUid = uid;
+ mType = TYPE_SESSION_SERVICE;
+ mSessionLink = null;
+ mExtras = Bundle.EMPTY;
+ }
+
+ Session2Token(int uid, int type, String packageName, Session2Link sessionLink,
+ @NonNull Bundle tokenExtras) {
+ mUid = uid;
+ mType = type;
+ mPackageName = packageName;
+ mServiceName = null;
+ mComponentName = null;
+ mSessionLink = sessionLink;
+ mExtras = tokenExtras;
+ }
+
+ Session2Token(Parcel in) {
+ mUid = in.readInt();
+ mType = in.readInt();
+ mPackageName = in.readString();
+ mServiceName = in.readString();
+ mSessionLink = in.readParcelable(null);
+ mComponentName = ComponentName.unflattenFromString(in.readString());
+
+ Bundle extras = in.readBundle();
+ if (extras == null) {
+ Log.w(TAG, "extras shouldn't be null.");
+ extras = Bundle.EMPTY;
+ } else if (MediaSession2.hasCustomParcelable(extras)) {
+ Log.w(TAG, "extras contain custom parcelable. Ignoring.");
+ extras = Bundle.EMPTY;
+ }
+ mExtras = extras;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mUid);
+ dest.writeInt(mType);
+ dest.writeString(mPackageName);
+ dest.writeString(mServiceName);
+ dest.writeParcelable(mSessionLink, flags);
+ dest.writeString(mComponentName == null ? "" : mComponentName.flattenToString());
+ dest.writeBundle(mExtras);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mUid, mPackageName, mServiceName, mSessionLink);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Session2Token)) {
+ return false;
+ }
+ Session2Token other = (Session2Token) obj;
+ return mUid == other.mUid
+ && TextUtils.equals(mPackageName, other.mPackageName)
+ && TextUtils.equals(mServiceName, other.mServiceName)
+ && mType == other.mType
+ && Objects.equals(mSessionLink, other.mSessionLink);
+ }
+
+ @Override
+ public String toString() {
+ return "Session2Token {pkg=" + mPackageName + " type=" + mType
+ + " service=" + mServiceName + " Session2Link=" + mSessionLink + "}";
+ }
+
+ /**
+ * @return uid of the session
+ */
+ public int getUid() {
+ return mUid;
+ }
+
+ /**
+ * @return package name of the session
+ */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * @return service name of the session. Can be {@code null} for {@link #TYPE_SESSION}.
+ */
+ @Nullable
+ public String getServiceName() {
+ return mServiceName;
+ }
+
+ /**
+ * @return type of the token
+ * @see #TYPE_SESSION
+ * @see #TYPE_SESSION_SERVICE
+ */
+ public @TokenType int getType() {
+ return mType;
+ }
+
+ /**
+ * @return extras of the token
+ * @see MediaSession2.Builder#setExtras(Bundle)
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return new Bundle(mExtras);
+ }
+
+ Session2Link getSessionLink() {
+ return mSessionLink;
+ }
+
+ private static boolean isInterfaceDeclared(PackageManager manager, String serviceInterface,
+ ComponentName serviceComponent) {
+ Intent serviceIntent = new Intent(serviceInterface);
+ // Use queryIntentServices to find services with MediaSession2Service.SERVICE_INTERFACE.
+ // We cannot use resolveService with intent specified class name, because resolveService
+ // ignores actions if Intent.setClassName() is specified.
+ serviceIntent.setPackage(serviceComponent.getPackageName());
+
+ List<ResolveInfo> list = manager.queryIntentServices(
+ serviceIntent, PackageManager.GET_META_DATA);
+ if (list != null) {
+ for (int i = 0; i < list.size(); i++) {
+ ResolveInfo resolveInfo = list.get(i);
+ if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+ continue;
+ }
+ if (TextUtils.equals(
+ resolveInfo.serviceInfo.name, serviceComponent.getClassName())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static int getUid(PackageManager manager, String packageName) {
+ try {
+ return manager.getApplicationInfo(packageName, 0).uid;
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new IllegalArgumentException("Cannot find package " + packageName);
+ }
+ }
+}
diff --git a/apex/media/framework/updatable-media-proguard.flags b/apex/media/framework/updatable-media-proguard.flags
new file mode 100644
index 000000000000..4e7d8422bf44
--- /dev/null
+++ b/apex/media/framework/updatable-media-proguard.flags
@@ -0,0 +1,2 @@
+# Keep all symbols in android.media.
+-keep class android.media.* {*;}
diff --git a/apex/permission/Android.bp b/apex/permission/Android.bp
new file mode 100644
index 000000000000..71a52bb216ea
--- /dev/null
+++ b/apex/permission/Android.bp
@@ -0,0 +1,43 @@
+// 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",
+ defaults: ["com.android.permission-defaults"],
+ manifest: "apex_manifest.json",
+}
+
+apex_defaults {
+ name: "com.android.permission-defaults",
+ updatable: true,
+ min_sdk_version: "R",
+ key: "com.android.permission.key",
+ certificate: ":com.android.permission.certificate",
+ java_libs: [
+ "framework-permission",
+ "service-permission",
+ ],
+ apps: ["PermissionController"],
+}
+
+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/TEST_MAPPING b/apex/permission/TEST_MAPPING
new file mode 100644
index 000000000000..6e67ce92a27e
--- /dev/null
+++ b/apex/permission/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit" : [
+ {
+ "name" : "PermissionApexTests"
+ }
+ ]
+}
diff --git a/apex/permission/apex_manifest.json b/apex/permission/apex_manifest.json
new file mode 100644
index 000000000000..7960598affa3
--- /dev/null
+++ b/apex/permission/apex_manifest.json
@@ -0,0 +1,4 @@
+{
+ "name": "com.android.permission",
+ "version": 300000000
+}
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/permission/framework/Android.bp b/apex/permission/framework/Android.bp
new file mode 100644
index 000000000000..c0560f61460f
--- /dev/null
+++ b/apex/permission/framework/Android.bp
@@ -0,0 +1,45 @@
+// Copyright (C) 2020 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-permission-sources",
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.aidl",
+ ],
+ path: "java",
+}
+
+java_sdk_library {
+ name: "framework-permission",
+ defaults: ["framework-module-defaults"],
+
+ // Restrict access to implementation library.
+ impl_library_visibility: ["//frameworks/base/apex/permission:__subpackages__"],
+
+ srcs: [
+ ":framework-permission-sources",
+ ],
+
+ apex_available: [
+ "com.android.permission",
+ "test_com.android.permission",
+ ],
+ permitted_packages: [
+ "android.permission",
+ "android.app.role",
+ ],
+ hostdex: true,
+ installable: true,
+}
diff --git a/apex/permission/framework/api/current.txt b/apex/permission/framework/api/current.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/permission/framework/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/permission/framework/api/module-lib-current.txt b/apex/permission/framework/api/module-lib-current.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/permission/framework/api/module-lib-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/permission/framework/api/module-lib-removed.txt b/apex/permission/framework/api/module-lib-removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/permission/framework/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/permission/framework/api/removed.txt b/apex/permission/framework/api/removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/permission/framework/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/permission/framework/api/system-current.txt b/apex/permission/framework/api/system-current.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/permission/framework/api/system-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/permission/framework/api/system-removed.txt b/apex/permission/framework/api/system-removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/permission/framework/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/permission/framework/java/android/permission/PermissionState.java b/apex/permission/framework/java/android/permission/PermissionState.java
new file mode 100644
index 000000000000..e810db8ecbfe
--- /dev/null
+++ b/apex/permission/framework/java/android/permission/PermissionState.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 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.permission;
+
+/**
+ * @hide
+ */
+public class PermissionState {}
diff --git a/apex/permission/service/Android.bp b/apex/permission/service/Android.bp
new file mode 100644
index 000000000000..b7d843352d8e
--- /dev/null
+++ b/apex/permission/service/Android.bp
@@ -0,0 +1,42 @@
+// Copyright (C) 2020 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: "service-permission-sources",
+ srcs: [
+ "java/**/*.java",
+ ],
+ path: "java",
+}
+
+java_sdk_library {
+ name: "service-permission",
+ defaults: ["framework-system-server-module-defaults"],
+ impl_library_visibility: [
+ "//frameworks/base/apex/permission/tests",
+ "//frameworks/base/services/tests/mockingservicestests",
+ "//frameworks/base/services/tests/servicestests",
+ ],
+ srcs: [
+ ":service-permission-sources",
+ ],
+ libs: [
+ "framework-permission",
+ ],
+ apex_available: [
+ "com.android.permission",
+ "test_com.android.permission",
+ ],
+ installable: true,
+}
diff --git a/apex/permission/service/api/current.txt b/apex/permission/service/api/current.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/permission/service/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/permission/service/api/removed.txt b/apex/permission/service/api/removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/permission/service/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/permission/service/api/system-server-current.txt b/apex/permission/service/api/system-server-current.txt
new file mode 100644
index 000000000000..c76cc3275737
--- /dev/null
+++ b/apex/permission/service/api/system-server-current.txt
@@ -0,0 +1,46 @@
+// Signature format: 2.0
+package com.android.permission.persistence {
+
+ public interface RuntimePermissionsPersistence {
+ method @NonNull public static com.android.permission.persistence.RuntimePermissionsPersistence createInstance();
+ method public void deleteForUser(@NonNull android.os.UserHandle);
+ method @Nullable public com.android.permission.persistence.RuntimePermissionsState readForUser(@NonNull android.os.UserHandle);
+ method public void writeForUser(@NonNull com.android.permission.persistence.RuntimePermissionsState, @NonNull android.os.UserHandle);
+ }
+
+ public final class RuntimePermissionsState {
+ ctor public RuntimePermissionsState(int, @Nullable String, @NonNull java.util.Map<java.lang.String,java.util.List<com.android.permission.persistence.RuntimePermissionsState.PermissionState>>, @NonNull java.util.Map<java.lang.String,java.util.List<com.android.permission.persistence.RuntimePermissionsState.PermissionState>>);
+ method @Nullable public String getFingerprint();
+ method @NonNull public java.util.Map<java.lang.String,java.util.List<com.android.permission.persistence.RuntimePermissionsState.PermissionState>> getPackagePermissions();
+ method @NonNull public java.util.Map<java.lang.String,java.util.List<com.android.permission.persistence.RuntimePermissionsState.PermissionState>> getSharedUserPermissions();
+ method public int getVersion();
+ field public static final int NO_VERSION = -1; // 0xffffffff
+ }
+
+ public static final class RuntimePermissionsState.PermissionState {
+ ctor public RuntimePermissionsState.PermissionState(@NonNull String, boolean, int);
+ method public int getFlags();
+ method @NonNull public String getName();
+ method public boolean isGranted();
+ }
+
+}
+
+package com.android.role.persistence {
+
+ public interface RolesPersistence {
+ method @NonNull public static com.android.role.persistence.RolesPersistence createInstance();
+ method public void deleteForUser(@NonNull android.os.UserHandle);
+ method @Nullable public com.android.role.persistence.RolesState readForUser(@NonNull android.os.UserHandle);
+ method public void writeForUser(@NonNull com.android.role.persistence.RolesState, @NonNull android.os.UserHandle);
+ }
+
+ public final class RolesState {
+ ctor public RolesState(int, @Nullable String, @NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>>);
+ method @Nullable public String getPackagesHash();
+ method @NonNull public java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getRoles();
+ method public int getVersion();
+ }
+
+}
+
diff --git a/apex/permission/service/api/system-server-removed.txt b/apex/permission/service/api/system-server-removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/permission/service/api/system-server-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/permission/service/java/com/android/permission/persistence/IoUtils.java b/apex/permission/service/java/com/android/permission/persistence/IoUtils.java
new file mode 100644
index 000000000000..569a78c0ab41
--- /dev/null
+++ b/apex/permission/service/java/com/android/permission/persistence/IoUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 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.permission.persistence;
+
+import android.annotation.NonNull;
+
+/**
+ * Utility class for IO.
+ *
+ * @hide
+ */
+public class IoUtils {
+
+ private IoUtils() {}
+
+ /**
+ * Close 'closeable' ignoring any exceptions.
+ */
+ public static void closeQuietly(@NonNull AutoCloseable closeable) {
+ try {
+ closeable.close();
+ } catch (Exception ignored) {
+ // Ignored.
+ }
+ }
+}
diff --git a/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistence.java b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistence.java
new file mode 100644
index 000000000000..aedba290db1f
--- /dev/null
+++ b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistence.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 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.permission.persistence;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.SystemApi.Client;
+import android.os.UserHandle;
+
+/**
+ * Persistence for runtime permissions.
+ *
+ * TODO(b/147914847): Remove @hide when it becomes the default.
+ * @hide
+ */
+@SystemApi(client = Client.SYSTEM_SERVER)
+public interface RuntimePermissionsPersistence {
+
+ /**
+ * Read the runtime permissions from persistence.
+ *
+ * This will perform I/O operations synchronously.
+ *
+ * @param user the user to read for
+ * @return the runtime permissions read
+ */
+ @Nullable
+ RuntimePermissionsState readForUser(@NonNull UserHandle user);
+
+ /**
+ * Write the runtime permissions to persistence.
+ *
+ * This will perform I/O operations synchronously.
+ *
+ * @param runtimePermissions the runtime permissions to write
+ * @param user the user to write for
+ */
+ void writeForUser(@NonNull RuntimePermissionsState runtimePermissions,
+ @NonNull UserHandle user);
+
+ /**
+ * Delete the runtime permissions from persistence.
+ *
+ * This will perform I/O operations synchronously.
+ *
+ * @param user the user to delete for
+ */
+ void deleteForUser(@NonNull UserHandle user);
+
+ /**
+ * Create a new instance of {@link RuntimePermissionsPersistence} implementation.
+ *
+ * @return the new instance.
+ */
+ @NonNull
+ static RuntimePermissionsPersistence createInstance() {
+ return new RuntimePermissionsPersistenceImpl();
+ }
+}
diff --git a/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistenceImpl.java b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistenceImpl.java
new file mode 100644
index 000000000000..e43f59a3377a
--- /dev/null
+++ b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistenceImpl.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2020 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.permission.persistence;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ApexEnvironment;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+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.Map;
+
+/**
+ * Persistence implementation for runtime permissions.
+ *
+ * TODO(b/147914847): Remove @hide when it becomes the default.
+ * @hide
+ */
+public class RuntimePermissionsPersistenceImpl implements RuntimePermissionsPersistence {
+
+ private static final String LOG_TAG = RuntimePermissionsPersistenceImpl.class.getSimpleName();
+
+ private static final String APEX_MODULE_NAME = "com.android.permission";
+
+ private static final String RUNTIME_PERMISSIONS_FILE_NAME = "runtime-permissions.xml";
+
+ private static final String TAG_PACKAGE = "package";
+ private static final String TAG_PERMISSION = "permission";
+ private static final String TAG_RUNTIME_PERMISSIONS = "runtime-permissions";
+ private static final String TAG_SHARED_USER = "shared-user";
+
+ private static final String ATTRIBUTE_FINGERPRINT = "fingerprint";
+ private static final String ATTRIBUTE_FLAGS = "flags";
+ private static final String ATTRIBUTE_GRANTED = "granted";
+ private static final String ATTRIBUTE_NAME = "name";
+ private static final String ATTRIBUTE_VERSION = "version";
+
+ @Nullable
+ @Override
+ public RuntimePermissionsState readForUser(@NonNull UserHandle user) {
+ File file = getFile(user);
+ try (FileInputStream inputStream = new AtomicFile(file).openRead()) {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(inputStream, null);
+ return parseXml(parser);
+ } catch (FileNotFoundException e) {
+ Log.i(LOG_TAG, "runtime-permissions.xml not found");
+ return null;
+ } catch (XmlPullParserException | IOException e) {
+ throw new IllegalStateException("Failed to read runtime-permissions.xml: " + file , e);
+ }
+ }
+
+ @NonNull
+ private static RuntimePermissionsState parseXml(@NonNull XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ int type;
+ int depth;
+ int innerDepth = parser.getDepth() + 1;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
+ if (depth > innerDepth || type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (parser.getName().equals(TAG_RUNTIME_PERMISSIONS)) {
+ return parseRuntimePermissions(parser);
+ }
+ }
+ throw new IllegalStateException("Missing <" + TAG_RUNTIME_PERMISSIONS
+ + "> in runtime-permissions.xml");
+ }
+
+ @NonNull
+ private static RuntimePermissionsState parseRuntimePermissions(@NonNull XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ String versionValue = parser.getAttributeValue(null, ATTRIBUTE_VERSION);
+ int version = versionValue != null ? Integer.parseInt(versionValue)
+ : RuntimePermissionsState.NO_VERSION;
+ String fingerprint = parser.getAttributeValue(null, ATTRIBUTE_FINGERPRINT);
+
+ Map<String, List<RuntimePermissionsState.PermissionState>> packagePermissions =
+ new ArrayMap<>();
+ Map<String, List<RuntimePermissionsState.PermissionState>> sharedUserPermissions =
+ new ArrayMap<>();
+ int type;
+ int depth;
+ int innerDepth = parser.getDepth() + 1;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
+ if (depth > innerDepth || type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ switch (parser.getName()) {
+ case TAG_PACKAGE: {
+ String packageName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
+ List<RuntimePermissionsState.PermissionState> permissions = parsePermissions(
+ parser);
+ packagePermissions.put(packageName, permissions);
+ break;
+ }
+ case TAG_SHARED_USER: {
+ String sharedUserName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
+ List<RuntimePermissionsState.PermissionState> permissions = parsePermissions(
+ parser);
+ sharedUserPermissions.put(sharedUserName, permissions);
+ break;
+ }
+ }
+ }
+
+ return new RuntimePermissionsState(version, fingerprint, packagePermissions,
+ sharedUserPermissions);
+ }
+
+ @NonNull
+ private static List<RuntimePermissionsState.PermissionState> parsePermissions(
+ @NonNull XmlPullParser parser) throws IOException, XmlPullParserException {
+ List<RuntimePermissionsState.PermissionState> permissions = new ArrayList<>();
+ int type;
+ int depth;
+ int innerDepth = parser.getDepth() + 1;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
+ if (depth > innerDepth || type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (parser.getName().equals(TAG_PERMISSION)) {
+ String name = parser.getAttributeValue(null, ATTRIBUTE_NAME);
+ boolean granted = Boolean.parseBoolean(parser.getAttributeValue(null,
+ ATTRIBUTE_GRANTED));
+ int flags = Integer.parseInt(parser.getAttributeValue(null,
+ ATTRIBUTE_FLAGS), 16);
+ RuntimePermissionsState.PermissionState permission =
+ new RuntimePermissionsState.PermissionState(name, granted, flags);
+ permissions.add(permission);
+ }
+ }
+ return permissions;
+ }
+
+ @Override
+ public void writeForUser(@NonNull RuntimePermissionsState runtimePermissions,
+ @NonNull UserHandle user) {
+ File file = getFile(user);
+ AtomicFile atomicFile = new AtomicFile(file);
+ FileOutputStream outputStream = null;
+ try {
+ outputStream = atomicFile.startWrite();
+
+ XmlSerializer serializer = Xml.newSerializer();
+ serializer.setOutput(outputStream, StandardCharsets.UTF_8.name());
+ serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+ serializer.startDocument(null, true);
+
+ serializeRuntimePermissions(serializer, runtimePermissions);
+
+ serializer.endDocument();
+ atomicFile.finishWrite(outputStream);
+ } catch (Exception e) {
+ Log.wtf(LOG_TAG, "Failed to write runtime-permissions.xml, restoring backup: " + file,
+ e);
+ atomicFile.failWrite(outputStream);
+ } finally {
+ IoUtils.closeQuietly(outputStream);
+ }
+ }
+
+ private static void serializeRuntimePermissions(@NonNull XmlSerializer serializer,
+ @NonNull RuntimePermissionsState runtimePermissions) throws IOException {
+ serializer.startTag(null, TAG_RUNTIME_PERMISSIONS);
+
+ int version = runtimePermissions.getVersion();
+ serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version));
+ String fingerprint = runtimePermissions.getFingerprint();
+ if (fingerprint != null) {
+ serializer.attribute(null, ATTRIBUTE_FINGERPRINT, fingerprint);
+ }
+
+ for (Map.Entry<String, List<RuntimePermissionsState.PermissionState>> entry
+ : runtimePermissions.getPackagePermissions().entrySet()) {
+ String packageName = entry.getKey();
+ List<RuntimePermissionsState.PermissionState> permissions = entry.getValue();
+
+ serializer.startTag(null, TAG_PACKAGE);
+ serializer.attribute(null, ATTRIBUTE_NAME, packageName);
+ serializePermissions(serializer, permissions);
+ serializer.endTag(null, TAG_PACKAGE);
+ }
+
+ for (Map.Entry<String, List<RuntimePermissionsState.PermissionState>> entry
+ : runtimePermissions.getSharedUserPermissions().entrySet()) {
+ String sharedUserName = entry.getKey();
+ List<RuntimePermissionsState.PermissionState> permissions = entry.getValue();
+
+ serializer.startTag(null, TAG_SHARED_USER);
+ serializer.attribute(null, ATTRIBUTE_NAME, sharedUserName);
+ serializePermissions(serializer, permissions);
+ serializer.endTag(null, TAG_SHARED_USER);
+ }
+
+ serializer.endTag(null, TAG_RUNTIME_PERMISSIONS);
+ }
+
+ private static void serializePermissions(@NonNull XmlSerializer serializer,
+ @NonNull List<RuntimePermissionsState.PermissionState> permissions) throws IOException {
+ int permissionsSize = permissions.size();
+ for (int i = 0; i < permissionsSize; i++) {
+ RuntimePermissionsState.PermissionState permissionState = permissions.get(i);
+
+ serializer.startTag(null, TAG_PERMISSION);
+ serializer.attribute(null, ATTRIBUTE_NAME, permissionState.getName());
+ serializer.attribute(null, ATTRIBUTE_GRANTED, Boolean.toString(
+ permissionState.isGranted() && (permissionState.getFlags()
+ & PackageManager.FLAG_PERMISSION_ONE_TIME) == 0));
+ serializer.attribute(null, ATTRIBUTE_FLAGS, Integer.toHexString(
+ permissionState.getFlags()));
+ serializer.endTag(null, TAG_PERMISSION);
+ }
+ }
+
+ @Override
+ public void deleteForUser(@NonNull UserHandle user) {
+ getFile(user).delete();
+ }
+
+ @NonNull
+ private static File getFile(@NonNull UserHandle user) {
+ ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME);
+ File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user);
+ return new File(dataDirectory, RUNTIME_PERMISSIONS_FILE_NAME);
+ }
+}
diff --git a/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsState.java b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsState.java
new file mode 100644
index 000000000000..c6bfc6d32989
--- /dev/null
+++ b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsState.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2020 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.permission.persistence;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.SystemApi.Client;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * State of all runtime permissions.
+ *
+ * TODO(b/147914847): Remove @hide when it becomes the default.
+ * @hide
+ */
+@SystemApi(client = Client.SYSTEM_SERVER)
+public final class RuntimePermissionsState {
+
+ /**
+ * Special value for {@link #mVersion} to indicate that no version was read.
+ */
+ public static final int NO_VERSION = -1;
+
+ /**
+ * The version of the runtime permissions.
+ */
+ private final int mVersion;
+
+ /**
+ * The fingerprint of the runtime permissions.
+ */
+ @Nullable
+ private final String mFingerprint;
+
+ /**
+ * The runtime permissions by packages.
+ */
+ @NonNull
+ private final Map<String, List<PermissionState>> mPackagePermissions;
+
+ /**
+ * The runtime permissions by shared users.
+ */
+ @NonNull
+ private final Map<String, List<PermissionState>> mSharedUserPermissions;
+
+ /**
+ * Create a new instance of this class.
+ *
+ * @param version the version of the runtime permissions
+ * @param fingerprint the fingerprint of the runtime permissions
+ * @param packagePermissions the runtime permissions by packages
+ * @param sharedUserPermissions the runtime permissions by shared users
+ */
+ public RuntimePermissionsState(int version, @Nullable String fingerprint,
+ @NonNull Map<String, List<PermissionState>> packagePermissions,
+ @NonNull Map<String, List<PermissionState>> sharedUserPermissions) {
+ mVersion = version;
+ mFingerprint = fingerprint;
+ mPackagePermissions = packagePermissions;
+ mSharedUserPermissions = sharedUserPermissions;
+ }
+
+ /**
+ * Get the version of the runtime permissions.
+ *
+ * @return the version of the runtime permissions
+ */
+ public int getVersion() {
+ return mVersion;
+ }
+
+ /**
+ * Get the fingerprint of the runtime permissions.
+ *
+ * @return the fingerprint of the runtime permissions
+ */
+ @Nullable
+ public String getFingerprint() {
+ return mFingerprint;
+ }
+
+ /**
+ * Get the runtime permissions by packages.
+ *
+ * @return the runtime permissions by packages
+ */
+ @NonNull
+ public Map<String, List<PermissionState>> getPackagePermissions() {
+ return mPackagePermissions;
+ }
+
+ /**
+ * Get the runtime permissions by shared users.
+ *
+ * @return the runtime permissions by shared users
+ */
+ @NonNull
+ public Map<String, List<PermissionState>> getSharedUserPermissions() {
+ return mSharedUserPermissions;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ RuntimePermissionsState that = (RuntimePermissionsState) object;
+ return mVersion == that.mVersion
+ && Objects.equals(mFingerprint, that.mFingerprint)
+ && Objects.equals(mPackagePermissions, that.mPackagePermissions)
+ && Objects.equals(mSharedUserPermissions, that.mSharedUserPermissions);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mVersion, mFingerprint, mPackagePermissions, mSharedUserPermissions);
+ }
+
+ /**
+ * State of a single permission.
+ */
+ public static final class PermissionState {
+
+ /**
+ * The name of the permission.
+ */
+ @NonNull
+ private final String mName;
+
+ /**
+ * Whether the permission is granted.
+ */
+ private final boolean mGranted;
+
+ /**
+ * The flags of the permission.
+ */
+ private final int mFlags;
+
+ /**
+ * Create a new instance of this class.
+ *
+ * @param name the name of the permission
+ * @param granted whether the permission is granted
+ * @param flags the flags of the permission
+ */
+ public PermissionState(@NonNull String name, boolean granted, int flags) {
+ mName = name;
+ mGranted = granted;
+ mFlags = flags;
+ }
+
+ /**
+ * Get the name of the permission.
+ *
+ * @return the name of the permission
+ */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Get whether the permission is granted.
+ *
+ * @return whether the permission is granted
+ */
+ public boolean isGranted() {
+ return mGranted;
+ }
+
+ /**
+ * Get the flags of the permission.
+ *
+ * @return the flags of the permission
+ */
+ public int getFlags() {
+ return mFlags;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ PermissionState that = (PermissionState) object;
+ return mGranted == that.mGranted
+ && mFlags == that.mFlags
+ && Objects.equals(mName, that.mName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mName, mGranted, mFlags);
+ }
+ }
+}
diff --git a/apex/permission/service/java/com/android/role/persistence/RolesPersistence.java b/apex/permission/service/java/com/android/role/persistence/RolesPersistence.java
new file mode 100644
index 000000000000..2e5a28aa1d6a
--- /dev/null
+++ b/apex/permission/service/java/com/android/role/persistence/RolesPersistence.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 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.role.persistence;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.SystemApi.Client;
+import android.os.UserHandle;
+
+/**
+ * Persistence for roles.
+ *
+ * TODO(b/147914847): Remove @hide when it becomes the default.
+ * @hide
+ */
+@SystemApi(client = Client.SYSTEM_SERVER)
+public interface RolesPersistence {
+
+ /**
+ * Read the roles from persistence.
+ *
+ * This will perform I/O operations synchronously.
+ *
+ * @param user the user to read for
+ * @return the roles read
+ */
+ @Nullable
+ RolesState readForUser(@NonNull UserHandle user);
+
+ /**
+ * Write the roles to persistence.
+ *
+ * This will perform I/O operations synchronously.
+ *
+ * @param roles the roles to write
+ * @param user the user to write for
+ */
+ void writeForUser(@NonNull RolesState roles, @NonNull UserHandle user);
+
+ /**
+ * Delete the roles from persistence.
+ *
+ * This will perform I/O operations synchronously.
+ *
+ * @param user the user to delete for
+ */
+ void deleteForUser(@NonNull UserHandle user);
+
+ /**
+ * Create a new instance of {@link RolesPersistence} implementation.
+ *
+ * @return the new instance.
+ */
+ @NonNull
+ static RolesPersistence createInstance() {
+ return new RolesPersistenceImpl();
+ }
+}
diff --git a/apex/permission/service/java/com/android/role/persistence/RolesPersistenceImpl.java b/apex/permission/service/java/com/android/role/persistence/RolesPersistenceImpl.java
new file mode 100644
index 000000000000..f66257f13ef6
--- /dev/null
+++ b/apex/permission/service/java/com/android/role/persistence/RolesPersistenceImpl.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2020 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.role.persistence;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ApexEnvironment;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.permission.persistence.IoUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+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.Map;
+import java.util.Set;
+
+/**
+ * Persistence implementation for roles.
+ *
+ * TODO(b/147914847): Remove @hide when it becomes the default.
+ * @hide
+ */
+public class RolesPersistenceImpl implements RolesPersistence {
+
+ private static final String LOG_TAG = RolesPersistenceImpl.class.getSimpleName();
+
+ private static final String APEX_MODULE_NAME = "com.android.permission";
+
+ private static final String ROLES_FILE_NAME = "roles.xml";
+
+ private static final String TAG_ROLES = "roles";
+ private static final String TAG_ROLE = "role";
+ private static final String TAG_HOLDER = "holder";
+
+ private static final String ATTRIBUTE_VERSION = "version";
+ private static final String ATTRIBUTE_NAME = "name";
+ private static final String ATTRIBUTE_PACKAGES_HASH = "packagesHash";
+
+ @Nullable
+ @Override
+ public RolesState readForUser(@NonNull UserHandle user) {
+ File file = getFile(user);
+ try (FileInputStream inputStream = new AtomicFile(file).openRead()) {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(inputStream, null);
+ return parseXml(parser);
+ } catch (FileNotFoundException e) {
+ Log.i(LOG_TAG, "roles.xml not found");
+ return null;
+ } catch (XmlPullParserException | IOException e) {
+ throw new IllegalStateException("Failed to read roles.xml: " + file , e);
+ }
+ }
+
+ @NonNull
+ private static RolesState parseXml(@NonNull XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ int type;
+ int depth;
+ int innerDepth = parser.getDepth() + 1;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
+ if (depth > innerDepth || type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (parser.getName().equals(TAG_ROLES)) {
+ return parseRoles(parser);
+ }
+ }
+ throw new IllegalStateException("Missing <" + TAG_ROLES + "> in roles.xml");
+ }
+
+ @NonNull
+ private static RolesState parseRoles(@NonNull XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ int version = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_VERSION));
+ String packagesHash = parser.getAttributeValue(null, ATTRIBUTE_PACKAGES_HASH);
+
+ Map<String, Set<String>> roles = new ArrayMap<>();
+ int type;
+ int depth;
+ int innerDepth = parser.getDepth() + 1;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
+ if (depth > innerDepth || type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (parser.getName().equals(TAG_ROLE)) {
+ String roleName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
+ Set<String> roleHolders = parseRoleHolders(parser);
+ roles.put(roleName, roleHolders);
+ }
+ }
+
+ return new RolesState(version, packagesHash, roles);
+ }
+
+ @NonNull
+ private static Set<String> parseRoleHolders(@NonNull XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ Set<String> roleHolders = new ArraySet<>();
+ int type;
+ int depth;
+ int innerDepth = parser.getDepth() + 1;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
+ if (depth > innerDepth || type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (parser.getName().equals(TAG_HOLDER)) {
+ String roleHolder = parser.getAttributeValue(null, ATTRIBUTE_NAME);
+ roleHolders.add(roleHolder);
+ }
+ }
+ return roleHolders;
+ }
+
+ @Override
+ public void writeForUser(@NonNull RolesState roles, @NonNull UserHandle user) {
+ File file = getFile(user);
+ AtomicFile atomicFile = new AtomicFile(file);
+ FileOutputStream outputStream = null;
+ try {
+ outputStream = atomicFile.startWrite();
+
+ XmlSerializer serializer = Xml.newSerializer();
+ serializer.setOutput(outputStream, StandardCharsets.UTF_8.name());
+ serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+ serializer.startDocument(null, true);
+
+ serializeRoles(serializer, roles);
+
+ serializer.endDocument();
+ atomicFile.finishWrite(outputStream);
+ } catch (Exception e) {
+ Log.wtf(LOG_TAG, "Failed to write roles.xml, restoring backup: " + file,
+ e);
+ atomicFile.failWrite(outputStream);
+ } finally {
+ IoUtils.closeQuietly(outputStream);
+ }
+ }
+
+ private static void serializeRoles(@NonNull XmlSerializer serializer,
+ @NonNull RolesState roles) throws IOException {
+ serializer.startTag(null, TAG_ROLES);
+
+ int version = roles.getVersion();
+ serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version));
+ String packagesHash = roles.getPackagesHash();
+ if (packagesHash != null) {
+ serializer.attribute(null, ATTRIBUTE_PACKAGES_HASH, packagesHash);
+ }
+
+ for (Map.Entry<String, Set<String>> entry : roles.getRoles().entrySet()) {
+ String roleName = entry.getKey();
+ Set<String> roleHolders = entry.getValue();
+
+ serializer.startTag(null, TAG_ROLE);
+ serializer.attribute(null, ATTRIBUTE_NAME, roleName);
+ serializeRoleHolders(serializer, roleHolders);
+ serializer.endTag(null, TAG_ROLE);
+ }
+
+ serializer.endTag(null, TAG_ROLES);
+ }
+
+ private static void serializeRoleHolders(@NonNull XmlSerializer serializer,
+ @NonNull Set<String> roleHolders) throws IOException {
+ for (String roleHolder : roleHolders) {
+ serializer.startTag(null, TAG_HOLDER);
+ serializer.attribute(null, ATTRIBUTE_NAME, roleHolder);
+ serializer.endTag(null, TAG_HOLDER);
+ }
+ }
+
+ @Override
+ public void deleteForUser(@NonNull UserHandle user) {
+ getFile(user).delete();
+ }
+
+ @NonNull
+ private static File getFile(@NonNull UserHandle user) {
+ ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME);
+ File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user);
+ return new File(dataDirectory, ROLES_FILE_NAME);
+ }
+}
diff --git a/apex/permission/service/java/com/android/role/persistence/RolesState.java b/apex/permission/service/java/com/android/role/persistence/RolesState.java
new file mode 100644
index 000000000000..f61efa0e840d
--- /dev/null
+++ b/apex/permission/service/java/com/android/role/persistence/RolesState.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2020 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.role.persistence;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.SystemApi.Client;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * State of all roles.
+ *
+ * TODO(b/147914847): Remove @hide when it becomes the default.
+ * @hide
+ */
+@SystemApi(client = Client.SYSTEM_SERVER)
+public final class RolesState {
+
+ /**
+ * The version of the roles.
+ */
+ private final int mVersion;
+
+ /**
+ * The hash of all packages in the system.
+ */
+ @Nullable
+ private final String mPackagesHash;
+
+ /**
+ * The roles.
+ */
+ @NonNull
+ private final Map<String, Set<String>> mRoles;
+
+ /**
+ * Create a new instance of this class.
+ *
+ * @param version the version of the roles
+ * @param packagesHash the hash of all packages in the system
+ * @param roles the roles
+ */
+ public RolesState(int version, @Nullable String packagesHash,
+ @NonNull Map<String, Set<String>> roles) {
+ mVersion = version;
+ mPackagesHash = packagesHash;
+ mRoles = roles;
+ }
+
+ /**
+ * Get the version of the roles.
+ *
+ * @return the version of the roles
+ */
+ public int getVersion() {
+ return mVersion;
+ }
+
+ /**
+ * Get the hash of all packages in the system.
+ *
+ * @return the hash of all packages in the system
+ */
+ @Nullable
+ public String getPackagesHash() {
+ return mPackagesHash;
+ }
+
+ /**
+ * Get the roles.
+ *
+ * @return the roles
+ */
+ @NonNull
+ public Map<String, Set<String>> getRoles() {
+ return mRoles;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ RolesState that = (RolesState) object;
+ return mVersion == that.mVersion
+ && Objects.equals(mPackagesHash, that.mPackagesHash)
+ && Objects.equals(mRoles, that.mRoles);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mVersion, mPackagesHash, mRoles);
+ }
+}
diff --git a/apex/permission/testing/Android.bp b/apex/permission/testing/Android.bp
new file mode 100644
index 000000000000..63bf0a08e956
--- /dev/null
+++ b/apex/permission/testing/Android.bp
@@ -0,0 +1,25 @@
+// 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_test {
+ name: "test_com.android.permission",
+ visibility: [
+ "//system/apex/tests",
+ ],
+ defaults: ["com.android.permission-defaults"],
+ manifest: "test_manifest.json",
+ file_contexts: ":com.android.permission-file_contexts",
+ // Test APEX, should never be installed
+ installable: false,
+}
diff --git a/apex/permission/testing/test_manifest.json b/apex/permission/testing/test_manifest.json
new file mode 100644
index 000000000000..bc19a9ea0172
--- /dev/null
+++ b/apex/permission/testing/test_manifest.json
@@ -0,0 +1,4 @@
+{
+ "name": "com.android.permission",
+ "version": 2147483647
+}
diff --git a/apex/permission/tests/Android.bp b/apex/permission/tests/Android.bp
new file mode 100644
index 000000000000..271e328c1139
--- /dev/null
+++ b/apex/permission/tests/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2020 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.
+
+android_test {
+ name: "PermissionApexTests",
+ sdk_version: "test_current",
+ srcs: [
+ "java/**/*.kt",
+ ],
+ static_libs: [
+ "service-permission.impl",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "androidx.test.ext.truth",
+ "mockito-target-extended-minus-junit4",
+ ],
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+ compile_multilib: "both",
+ test_suites: [
+ "general-tests",
+ "mts",
+ ],
+}
diff --git a/apex/permission/tests/AndroidManifest.xml b/apex/permission/tests/AndroidManifest.xml
new file mode 100644
index 000000000000..57ee6417aeb3
--- /dev/null
+++ b/apex/permission/tests/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2020 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.
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.permission.test">
+
+ <!-- The application has to be debuggable for static mocking to work. -->
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.permission.test"
+ android:label="Permission APEX Tests" />
+</manifest>
diff --git a/apex/permission/tests/java/com/android/permission/persistence/RuntimePermissionsPersistenceTest.kt b/apex/permission/tests/java/com/android/permission/persistence/RuntimePermissionsPersistenceTest.kt
new file mode 100644
index 000000000000..2987da087e51
--- /dev/null
+++ b/apex/permission/tests/java/com/android/permission/persistence/RuntimePermissionsPersistenceTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2020 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.permission.persistence
+
+import android.content.ApexEnvironment
+import android.content.Context
+import android.os.Process
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations.initMocks
+import org.mockito.MockitoSession
+import org.mockito.quality.Strictness
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+class RuntimePermissionsPersistenceTest {
+ private val context = InstrumentationRegistry.getInstrumentation().context
+
+ private lateinit var mockDataDirectory: File
+
+ private lateinit var mockitoSession: MockitoSession
+ @Mock
+ lateinit var apexEnvironment: ApexEnvironment
+
+ private val persistence = RuntimePermissionsPersistence.createInstance()
+ private val permissionState = RuntimePermissionsState.PermissionState("permission", true, 3)
+ private val state = RuntimePermissionsState(
+ 1, "fingerprint", mapOf("package" to listOf(permissionState)),
+ mapOf("sharedUser" to listOf(permissionState))
+ )
+ private val user = Process.myUserHandle()
+
+ @Before
+ fun createMockDataDirectory() {
+ mockDataDirectory = context.getDir("mock_data", Context.MODE_PRIVATE)
+ mockDataDirectory.listFiles()!!.forEach { assertThat(it.deleteRecursively()).isTrue() }
+ }
+
+ @Before
+ fun mockApexEnvironment() {
+ initMocks(this)
+ mockitoSession = mockitoSession()
+ .mockStatic(ApexEnvironment::class.java)
+ .strictness(Strictness.LENIENT)
+ .startMocking()
+ `when`(ApexEnvironment.getApexEnvironment(eq(APEX_MODULE_NAME))).thenReturn(apexEnvironment)
+ `when`(apexEnvironment.getDeviceProtectedDataDirForUser(any(UserHandle::class.java))).then {
+ File(mockDataDirectory, it.arguments[0].toString()).also { it.mkdirs() }
+ }
+ }
+
+ @After
+ fun finishMockingApexEnvironment() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ fun testReadWrite() {
+ persistence.writeForUser(state, user)
+ val persistedState = persistence.readForUser(user)
+
+ assertThat(persistedState).isEqualTo(state)
+ assertThat(persistedState!!.version).isEqualTo(state.version)
+ assertThat(persistedState.fingerprint).isEqualTo(state.fingerprint)
+ assertThat(persistedState.packagePermissions).isEqualTo(state.packagePermissions)
+ val persistedPermissionState = persistedState.packagePermissions.values.first().first()
+ assertThat(persistedPermissionState.name).isEqualTo(permissionState.name)
+ assertThat(persistedPermissionState.isGranted).isEqualTo(permissionState.isGranted)
+ assertThat(persistedPermissionState.flags).isEqualTo(permissionState.flags)
+ assertThat(persistedState.sharedUserPermissions).isEqualTo(state.sharedUserPermissions)
+ }
+
+ @Test
+ fun testDelete() {
+ persistence.writeForUser(state, user)
+ persistence.deleteForUser(user)
+ val persistedState = persistence.readForUser(user)
+
+ assertThat(persistedState).isNull()
+ }
+
+ companion object {
+ private const val APEX_MODULE_NAME = "com.android.permission"
+ }
+}
diff --git a/apex/permission/tests/java/com/android/role/persistence/RolesPersistenceTest.kt b/apex/permission/tests/java/com/android/role/persistence/RolesPersistenceTest.kt
new file mode 100644
index 000000000000..f9d9d5afb25d
--- /dev/null
+++ b/apex/permission/tests/java/com/android/role/persistence/RolesPersistenceTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2020 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.role.persistence
+
+import android.content.ApexEnvironment
+import android.content.Context
+import android.os.Process
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations.initMocks
+import org.mockito.MockitoSession
+import org.mockito.quality.Strictness
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+class RolesPersistenceTest {
+ private val context = InstrumentationRegistry.getInstrumentation().context
+
+ private lateinit var mockDataDirectory: File
+
+ private lateinit var mockitoSession: MockitoSession
+ @Mock
+ lateinit var apexEnvironment: ApexEnvironment
+
+ private val persistence = RolesPersistence.createInstance()
+ private val state = RolesState(1, "packagesHash", mapOf("role" to setOf("holder1", "holder2")))
+ private val user = Process.myUserHandle()
+
+ @Before
+ fun createMockDataDirectory() {
+ mockDataDirectory = context.getDir("mock_data", Context.MODE_PRIVATE)
+ mockDataDirectory.listFiles()!!.forEach { assertThat(it.deleteRecursively()).isTrue() }
+ }
+
+ @Before
+ fun mockApexEnvironment() {
+ initMocks(this)
+ mockitoSession = mockitoSession()
+ .mockStatic(ApexEnvironment::class.java)
+ .strictness(Strictness.LENIENT)
+ .startMocking()
+ `when`(ApexEnvironment.getApexEnvironment(eq(APEX_MODULE_NAME))).thenReturn(apexEnvironment)
+ `when`(apexEnvironment.getDeviceProtectedDataDirForUser(any(UserHandle::class.java))).then {
+ File(mockDataDirectory, it.arguments[0].toString()).also { it.mkdirs() }
+ }
+ }
+
+ @After
+ fun finishMockingApexEnvironment() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ fun testReadWrite() {
+ persistence.writeForUser(state, user)
+ val persistedState = persistence.readForUser(user)
+
+ assertThat(persistedState).isEqualTo(state)
+ assertThat(persistedState!!.version).isEqualTo(state.version)
+ assertThat(persistedState.packagesHash).isEqualTo(state.packagesHash)
+ assertThat(persistedState.roles).isEqualTo(state.roles)
+ }
+
+ @Test
+ fun testDelete() {
+ persistence.writeForUser(state, user)
+ persistence.deleteForUser(user)
+ val persistedState = persistence.readForUser(user)
+
+ assertThat(persistedState).isNull()
+ }
+
+ companion object {
+ private const val APEX_MODULE_NAME = "com.android.permission"
+ }
+}
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..ede8852c5905
--- /dev/null
+++ b/apex/statsd/Android.bp
@@ -0,0 +1,83 @@
+// 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",
+ defaults: ["com.android.os.statsd-defaults"],
+ manifest: "apex_manifest.json",
+}
+
+apex_defaults {
+ jni_libs: [
+ "libstats_jni",
+ ],
+ native_shared_libs: [
+ "libstatspull",
+ "libstatssocket",
+ ],
+ binaries: ["statsd"],
+ java_libs: [
+ "framework-statsd",
+ "service-statsd",
+ ],
+ compile_multilib: "both",
+ prebuilts: ["com.android.os.statsd.init.rc"],
+ name: "com.android.os.statsd-defaults",
+ updatable: true,
+ min_sdk_version: "R",
+ 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",
+}
+
+prebuilt_etc {
+ name: "com.android.os.statsd.init.rc",
+ src: "statsd.rc",
+ filename: "init.rc",
+ installable: false,
+}
+
+// JNI library for StatsLog.write
+cc_library_shared {
+ name: "libstats_jni",
+ srcs: ["jni/**/*.cpp"],
+ header_libs: ["libnativehelper_header_only"],
+ shared_libs: [
+ "liblog", // Has a stable abi - should not be copied into apex.
+ "libstatssocket",
+ ],
+ stl: "libc++_static",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wextra",
+ "-Wno-unused-parameter",
+ ],
+ apex_available: [
+ "com.android.os.statsd",
+ "test_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/TEST_MAPPING b/apex/statsd/TEST_MAPPING
new file mode 100644
index 000000000000..93f108707d9e
--- /dev/null
+++ b/apex/statsd/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+ "presubmit" : [
+ {
+ "name" : "FrameworkStatsdTest"
+ },
+ {
+ "name" : "LibStatsPullTests"
+ }
+ ]
+}
diff --git a/apex/statsd/aidl/Android.bp b/apex/statsd/aidl/Android.bp
new file mode 100644
index 000000000000..04339e67d799
--- /dev/null
+++ b/apex/statsd/aidl/Android.bp
@@ -0,0 +1,51 @@
+//
+// 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-statsd-aidl-sources",
+ srcs: ["**/*.aidl"],
+}
+
+aidl_interface {
+ name: "statsd-aidl",
+ unstable: true,
+ srcs: [
+ "android/os/IPendingIntentRef.aidl",
+ "android/os/IPullAtomCallback.aidl",
+ "android/os/IPullAtomResultReceiver.aidl",
+ "android/os/IStatsCompanionService.aidl",
+ "android/os/IStatsd.aidl",
+ "android/os/StatsDimensionsValueParcel.aidl",
+ "android/util/StatsEventParcel.aidl",
+ ],
+ backend: {
+ java: {
+ enabled: false, // framework-statsd and service-statsd use framework-statsd-aidl-sources
+ },
+ cpp: {
+ enabled: false,
+ },
+ ndk: {
+ enabled: true,
+ apex_available: [
+ // TODO(b/145923087): Remove this once statsd binary is in apex
+ "//apex_available:platform",
+
+ "com.android.os.statsd",
+ "test_com.android.os.statsd",
+ ],
+ },
+ }
+}
diff --git a/apex/statsd/aidl/android/os/IPendingIntentRef.aidl b/apex/statsd/aidl/android/os/IPendingIntentRef.aidl
new file mode 100644
index 000000000000..000a69992a49
--- /dev/null
+++ b/apex/statsd/aidl/android/os/IPendingIntentRef.aidl
@@ -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.
+ */
+
+package android.os;
+
+import android.os.StatsDimensionsValueParcel;
+
+/**
+ * Binder interface to hold a PendingIntent for StatsCompanionService.
+ * {@hide}
+ */
+interface IPendingIntentRef {
+
+ /**
+ * Sends a broadcast to the specified PendingIntent that it should getData now.
+ * This should be only called from StatsCompanionService.
+ */
+ oneway void sendDataBroadcast(long lastReportTimeNs);
+
+ /**
+ * Send a broadcast to the specified PendingIntent notifying it that the list of active configs
+ * has changed. This should be only called from StatsCompanionService.
+ */
+ oneway void sendActiveConfigsChangedBroadcast(in long[] configIds);
+
+ /**
+ * Send a broadcast to the specified PendingIntent, along with the other information
+ * specified. This should only be called from StatsCompanionService.
+ */
+ oneway void sendSubscriberBroadcast(long configUid, long configId, long subscriptionId,
+ long subscriptionRuleId, in String[] cookies,
+ in StatsDimensionsValueParcel dimensionsValueParcel);
+}
diff --git a/apex/statsd/aidl/android/os/IPullAtomCallback.aidl b/apex/statsd/aidl/android/os/IPullAtomCallback.aidl
new file mode 100644
index 000000000000..ff0b97bb5b84
--- /dev/null
+++ b/apex/statsd/aidl/android/os/IPullAtomCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * 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.os;
+
+import android.os.IPullAtomResultReceiver;
+
+/**
+ * Binder interface to pull atoms for the stats service.
+ * {@hide}
+ */
+interface IPullAtomCallback {
+ /**
+ * Initiate a request for a pull for an atom.
+ */
+ oneway void onPullAtom(int atomTag, IPullAtomResultReceiver resultReceiver);
+
+}
diff --git a/apex/statsd/aidl/android/os/IPullAtomResultReceiver.aidl b/apex/statsd/aidl/android/os/IPullAtomResultReceiver.aidl
new file mode 100644
index 000000000000..00d026e25df3
--- /dev/null
+++ b/apex/statsd/aidl/android/os/IPullAtomResultReceiver.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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.os;
+
+import android.util.StatsEventParcel;
+
+/**
+ * Binder interface to pull atoms for the stats service.
+ * {@hide}
+ */
+interface IPullAtomResultReceiver {
+
+ /**
+ * Indicate that a pull request for an atom is complete.
+ */
+ oneway void pullFinished(int atomTag, boolean success, in StatsEventParcel[] output);
+
+}
diff --git a/apex/statsd/aidl/android/os/IStatsCompanionService.aidl b/apex/statsd/aidl/android/os/IStatsCompanionService.aidl
new file mode 100644
index 000000000000..5cdb3249501b
--- /dev/null
+++ b/apex/statsd/aidl/android/os/IStatsCompanionService.aidl
@@ -0,0 +1,68 @@
+/*
+ * 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.os;
+
+/**
+ * Binder interface to communicate with the Java-based statistics service helper.
+ * {@hide}
+ */
+interface IStatsCompanionService {
+ /**
+ * Tell statscompanion that stastd is up and running.
+ */
+ oneway void statsdReady();
+
+ /**
+ * Register an alarm for anomaly detection to fire at the given timestamp (ms since epoch).
+ * If anomaly alarm had already been registered, it will be replaced with the new timestamp.
+ * Uses AlarmManager.set API, so if the timestamp is in the past, alarm fires immediately, and
+ * alarm is inexact.
+ */
+ oneway void setAnomalyAlarm(long timestampMs);
+
+ /** Cancel any anomaly detection alarm. */
+ oneway void cancelAnomalyAlarm();
+
+ /**
+ * Register a repeating alarm for pulling to fire at the given timestamp and every
+ * intervalMs thereafter (in ms since epoch).
+ * If polling alarm had already been registered, it will be replaced by new one.
+ * Uses AlarmManager.setRepeating API, so if the timestamp is in past, alarm fires immediately,
+ * and alarm is inexact.
+ */
+ oneway void setPullingAlarm(long nextPullTimeMs);
+
+ /** Cancel any repeating pulling alarm. */
+ oneway void cancelPullingAlarm();
+
+ /**
+ * Register an alarm when we want to trigger subscribers at the given
+ * timestamp (in ms since epoch).
+ * If an alarm had already been registered, it will be replaced by new one.
+ */
+ oneway void setAlarmForSubscriberTriggering(long timestampMs);
+
+ /** Cancel any alarm for the purpose of subscriber triggering. */
+ oneway void cancelAlarmForSubscriberTriggering();
+
+ /**
+ * Ask StatsCompanionService if the given permission is allowed for a particular process
+ * and user ID. statsd is incapable of doing this check itself because checkCallingPermission
+ * is not currently supported by libbinder_ndk.
+ */
+ boolean checkPermission(String permission, int pid, int uid);
+}
diff --git a/apex/statsd/aidl/android/os/IStatsManagerService.aidl b/apex/statsd/aidl/android/os/IStatsManagerService.aidl
new file mode 100644
index 000000000000..b59a97e25bd0
--- /dev/null
+++ b/apex/statsd/aidl/android/os/IStatsManagerService.aidl
@@ -0,0 +1,136 @@
+/**
+ * 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.os;
+
+import android.app.PendingIntent;
+import android.os.IPullAtomCallback;
+
+/**
+ * Binder interface to communicate with the Java-based statistics service helper.
+ * Contains parcelable objects available only in Java.
+ * {@hide}
+ */
+interface IStatsManagerService {
+
+ /**
+ * Registers the given pending intent for this config key. This intent is invoked when the
+ * memory consumed by the metrics for this configuration approach the pre-defined limits. There
+ * can be at most one listener per config key.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ void setDataFetchOperation(long configId, in PendingIntent pendingIntent,
+ in String packageName);
+
+ /**
+ * Removes the data fetch operation for the specified configuration.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ void removeDataFetchOperation(long configId, in String packageName);
+
+ /**
+ * Registers the given pending intent for this packagename. This intent is invoked when the
+ * active status of any of the configs sent by this package changes and will contain a list of
+ * config ids that are currently active. It also returns the list of configs that are currently
+ * active. There can be at most one active configs changed listener per package.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ long[] setActiveConfigsChangedOperation(in PendingIntent pendingIntent, in String packageName);
+
+ /**
+ * Removes the active configs changed operation for the specified package name.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ void removeActiveConfigsChangedOperation(in String packageName);
+
+ /**
+ * Set the PendingIntent to be used when broadcasting subscriber
+ * information to the given subscriberId within the given config.
+ *
+ * Suppose that the calling uid has added a config with key configKey, and that in this config
+ * it is specified that when a particular anomaly is detected, a broadcast should be sent to
+ * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with
+ * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast
+ * when the anomaly is detected.
+ *
+ * This function can only be called by the owner (uid) of the config. It must be called each
+ * time statsd starts. Later calls overwrite previous calls; only one PendingIntent is stored.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ void setBroadcastSubscriber(long configKey, long subscriberId, in PendingIntent pendingIntent,
+ in String packageName);
+
+ /**
+ * Undoes setBroadcastSubscriber() for the (configKey, subscriberId) pair.
+ * Any broadcasts associated with subscriberId will henceforth not be sent.
+ * No-op if this (configKey, subscriberId) pair was not associated with an PendingIntent.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ void unsetBroadcastSubscriber(long configKey, long subscriberId, in String packageName);
+
+ /**
+ * Returns the most recently registered experiment IDs.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ long[] getRegisteredExperimentIds();
+
+ /**
+ * Fetches metadata across statsd. Returns byte array representing wire-encoded proto.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ byte[] getMetadata(in String packageName);
+
+ /**
+ * Fetches data for the specified configuration key. Returns a byte array representing proto
+ * wire-encoded of ConfigMetricsReportList.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ byte[] getData(in long key, in String packageName);
+
+ /**
+ * Sets a configuration with the specified config id and subscribes to updates for this
+ * configuration id. Broadcasts will be sent if this configuration needs to be collected.
+ * The configuration must be a wire-encoded StatsdConfig. The receiver for this data is
+ * registered in a separate function.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ void addConfiguration(in long configId, in byte[] config, in String packageName);
+
+ /**
+ * Removes the configuration with the matching config id. No-op if this config id does not
+ * exist.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ void removeConfiguration(in long configId, in String packageName);
+
+ /** Tell StatsManagerService to register a puller for the given atom tag with statsd. */
+ oneway void registerPullAtomCallback(int atomTag, long coolDownMillis, long timeoutMillis,
+ in int[] additiveFields, IPullAtomCallback pullerCallback);
+
+ /** Tell StatsManagerService to unregister the pulller for the given atom tag from statsd. */
+ oneway void unregisterPullAtomCallback(int atomTag);
+}
diff --git a/apex/statsd/aidl/android/os/IStatsd.aidl b/apex/statsd/aidl/android/os/IStatsd.aidl
new file mode 100644
index 000000000000..0d3f4208a2ab
--- /dev/null
+++ b/apex/statsd/aidl/android/os/IStatsd.aidl
@@ -0,0 +1,237 @@
+/**
+ * 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.os;
+
+import android.os.IPendingIntentRef;
+import android.os.IPullAtomCallback;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Binder interface to communicate with the statistics management service.
+ * {@hide}
+ */
+interface IStatsd {
+ /**
+ * Tell the stats daemon that the android system server is up and running.
+ */
+ oneway void systemRunning();
+
+ /**
+ * Tell the stats daemon that the android system has finished booting.
+ */
+ oneway void bootCompleted();
+
+ /**
+ * Tell the stats daemon that the StatsCompanionService is up and running.
+ * Two-way binder call so that caller knows message received.
+ */
+ void statsCompanionReady();
+
+ /**
+ * Tells statsd that an anomaly may have occurred, so statsd can check whether this is so and
+ * act accordingly.
+ * Two-way binder call so that caller's method (and corresponding wakelocks) will linger.
+ */
+ void informAnomalyAlarmFired();
+
+ /**
+ * Tells statsd that it is time to poll some stats. Statsd will be responsible for determing
+ * what stats to poll and initiating the polling.
+ * Two-way binder call so that caller's method (and corresponding wakelocks) will linger.
+ */
+ void informPollAlarmFired();
+
+ /**
+ * Tells statsd that it is time to handle periodic alarms. Statsd will be responsible for
+ * determing what alarm subscriber to trigger.
+ * Two-way binder call so that caller's method (and corresponding wakelocks) will linger.
+ */
+ void informAlarmForSubscriberTriggeringFired();
+
+ /**
+ * Tells statsd that the device is about to shutdown.
+ */
+ void informDeviceShutdown();
+
+ /**
+ * Inform statsd about a file descriptor for a pipe through which we will pipe version
+ * and package information for each uid.
+ * Versions and package information are supplied via UidData proto where info for each app
+ * is captured in its own element of a repeated ApplicationInfo message.
+ */
+ oneway void informAllUidData(in ParcelFileDescriptor fd);
+
+ /**
+ * Inform statsd what the uid, version, version_string, and installer are for one app that was
+ * updated.
+ */
+ oneway void informOnePackage(in String app, in int uid, in long version,
+ in String version_string, in String installer);
+
+ /**
+ * Inform stats that an app was removed.
+ */
+ oneway void informOnePackageRemoved(in String app, in int uid);
+
+ /**
+ * Fetches data for the specified configuration key. Returns a byte array representing proto
+ * wire-encoded of ConfigMetricsReportList.
+ *
+ * Requires Manifest.permission.DUMP.
+ */
+ byte[] getData(in long key, int callingUid);
+
+ /**
+ * Fetches metadata across statsd. Returns byte array representing wire-encoded proto.
+ *
+ * Requires Manifest.permission.DUMP.
+ */
+ byte[] getMetadata();
+
+ /**
+ * Sets a configuration with the specified config id and subscribes to updates for this
+ * configuration key. Broadcasts will be sent if this configuration needs to be collected.
+ * The configuration must be a wire-encoded StatsdConfig. The receiver for this data is
+ * registered in a separate function.
+ *
+ * Requires Manifest.permission.DUMP.
+ */
+ void addConfiguration(in long configId, in byte[] config, in int callingUid);
+
+ /**
+ * Registers the given pending intent for this config key. This intent is invoked when the
+ * memory consumed by the metrics for this configuration approach the pre-defined limits. There
+ * can be at most one listener per config key.
+ *
+ * Requires Manifest.permission.DUMP.
+ */
+ void setDataFetchOperation(long configId, in IPendingIntentRef pendingIntentRef,
+ int callingUid);
+
+ /**
+ * Removes the data fetch operation for the specified configuration.
+ *
+ * Requires Manifest.permission.DUMP.
+ */
+ void removeDataFetchOperation(long configId, int callingUid);
+
+ /**
+ * Registers the given pending intent for this packagename. This intent is invoked when the
+ * active status of any of the configs sent by this package changes and will contain a list of
+ * config ids that are currently active. It also returns the list of configs that are currently
+ * active. There can be at most one active configs changed listener per package.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ long[] setActiveConfigsChangedOperation(in IPendingIntentRef pendingIntentRef, int callingUid);
+
+ /**
+ * Removes the active configs changed operation for the specified package name.
+ *
+ * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS.
+ */
+ void removeActiveConfigsChangedOperation(int callingUid);
+
+ /**
+ * Removes the configuration with the matching config id. No-op if this config id does not
+ * exist.
+ *
+ * Requires Manifest.permission.DUMP.
+ */
+ void removeConfiguration(in long configId, in int callingUid);
+
+ /**
+ * Set the PendingIntentRef to be used when broadcasting subscriber
+ * information to the given subscriberId within the given config.
+ *
+ * Suppose that the calling uid has added a config with key configId, and that in this config
+ * it is specified that when a particular anomaly is detected, a broadcast should be sent to
+ * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with
+ * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast
+ * when the anomaly is detected.
+ *
+ * This function can only be called by the owner (uid) of the config. It must be called each
+ * time statsd starts. Later calls overwrite previous calls; only one pendingIntent is stored.
+ *
+ * Requires Manifest.permission.DUMP.
+ */
+ void setBroadcastSubscriber(long configId, long subscriberId, in IPendingIntentRef pir,
+ int callingUid);
+
+ /**
+ * Undoes setBroadcastSubscriber() for the (configId, subscriberId) pair.
+ * Any broadcasts associated with subscriberId will henceforth not be sent.
+ * No-op if this (configKey, subscriberId) pair was not associated with an PendingIntentRef.
+ *
+ * Requires Manifest.permission.DUMP.
+ */
+ void unsetBroadcastSubscriber(long configId, long subscriberId, int callingUid);
+
+ /**
+ * Tell the stats daemon that all the pullers registered during boot have been sent.
+ */
+ oneway void allPullersFromBootRegistered();
+
+ /**
+ * Registers a puller callback function that, when invoked, pulls the data
+ * for the specified atom tag.
+ */
+ oneway void registerPullAtomCallback(int uid, int atomTag, long coolDownMillis,
+ long timeoutMillis,in int[] additiveFields,
+ IPullAtomCallback pullerCallback);
+
+ /**
+ * Registers a puller callback function that, when invoked, pulls the data
+ * for the specified atom tag.
+ *
+ * Enforces the REGISTER_STATS_PULL_ATOM permission.
+ */
+ oneway void registerNativePullAtomCallback(int atomTag, long coolDownMillis, long timeoutMillis,
+ in int[] additiveFields, IPullAtomCallback pullerCallback);
+
+ /**
+ * Unregisters any pullAtomCallback for the given uid/atom.
+ */
+ oneway void unregisterPullAtomCallback(int uid, int atomTag);
+
+ /**
+ * Unregisters any pullAtomCallback for the given atom + caller.
+ *
+ * Enforces the REGISTER_STATS_PULL_ATOM permission.
+ */
+ oneway void unregisterNativePullAtomCallback(int atomTag);
+
+ /**
+ * The install requires staging.
+ */
+ const int FLAG_REQUIRE_STAGING = 0x01;
+
+ /**
+ * Rollback is enabled with this install.
+ */
+ const int FLAG_ROLLBACK_ENABLED = 0x02;
+
+ /**
+ * Requires low latency monitoring.
+ */
+ const int FLAG_REQUIRE_LOW_LATENCY_MONITOR = 0x04;
+
+ /**
+ * Returns the most recently registered experiment IDs.
+ */
+ long[] getRegisteredExperimentIds();
+}
diff --git a/apex/statsd/aidl/android/os/StatsDimensionsValueParcel.aidl b/apex/statsd/aidl/android/os/StatsDimensionsValueParcel.aidl
new file mode 100644
index 000000000000..05f78d00348e
--- /dev/null
+++ b/apex/statsd/aidl/android/os/StatsDimensionsValueParcel.aidl
@@ -0,0 +1,21 @@
+package android.os;
+
+/**
+ * @hide
+ */
+parcelable StatsDimensionsValueParcel {
+ // Field equals atomTag for top level StatsDimensionsValueParcels or
+ // positions in depth (1-indexed) for lower level parcels.
+ int field;
+
+ // Indicator for which type of value is stored. Should be set to one
+ // of the constants in StatsDimensionsValue.java.
+ int valueType;
+
+ String stringValue;
+ int intValue;
+ long longValue;
+ boolean boolValue;
+ float floatValue;
+ StatsDimensionsValueParcel[] tupleValue;
+}
diff --git a/apex/statsd/aidl/android/util/StatsEventParcel.aidl b/apex/statsd/aidl/android/util/StatsEventParcel.aidl
new file mode 100644
index 000000000000..add8bfb47b1a
--- /dev/null
+++ b/apex/statsd/aidl/android/util/StatsEventParcel.aidl
@@ -0,0 +1,8 @@
+package android.util;
+
+/**
+ * @hide
+ */
+parcelable StatsEventParcel {
+ byte[] buffer;
+}
diff --git a/apex/statsd/apex_manifest.json b/apex/statsd/apex_manifest.json
new file mode 100644
index 000000000000..e2972e700880
--- /dev/null
+++ b/apex/statsd/apex_manifest.json
@@ -0,0 +1,5 @@
+{
+ "name": "com.android.os.statsd",
+ "version": 300000000
+}
+
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/framework/Android.bp b/apex/statsd/framework/Android.bp
new file mode 100644
index 000000000000..bf4323ddfb0b
--- /dev/null
+++ b/apex/statsd/framework/Android.bp
@@ -0,0 +1,81 @@
+// 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 {
+ default_visibility: [ ":__pkg__" ]
+}
+
+genrule {
+ name: "statslog-statsd-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module statsd" +
+ " --javaPackage com.android.internal.statsd --javaClass StatsdStatsLog",
+ out: ["com/android/internal/statsd/StatsdStatsLog.java"],
+}
+
+java_library_static {
+ name: "statslog-statsd",
+ srcs: [
+ ":statslog-statsd-java-gen",
+ ],
+ visibility: [
+ "//cts/hostsidetests/statsd/apps:__subpackages__",
+ "//vendor:__subpackages__",
+ ],
+}
+
+filegroup {
+ name: "framework-statsd-sources",
+ srcs: [
+ "java/**/*.java",
+ ":framework-statsd-aidl-sources",
+ ":statslog-statsd-java-gen",
+ ],
+ visibility: [
+ "//frameworks/base", // For the "global" stubs.
+ "//frameworks/base/apex/statsd:__subpackages__",
+ ],
+}
+java_sdk_library {
+ name: "framework-statsd",
+ defaults: ["framework-module-defaults"],
+ installable: true,
+
+ srcs: [
+ ":framework-statsd-sources",
+ ],
+
+ permitted_packages: [
+ "android.app",
+ "android.os",
+ "android.util",
+ // From :statslog-statsd-java-gen
+ "com.android.internal.statsd",
+ ],
+
+ api_packages: [
+ "android.app",
+ "android.os",
+ "android.util",
+ ],
+
+ hostdex: true, // for hiddenapi check
+
+ impl_library_visibility: ["//frameworks/base/apex/statsd/framework/test:__subpackages__"],
+
+ apex_available: [
+ "com.android.os.statsd",
+ "test_com.android.os.statsd",
+ ],
+}
diff --git a/apex/statsd/framework/api/current.txt b/apex/statsd/framework/api/current.txt
new file mode 100644
index 000000000000..a65569347e7d
--- /dev/null
+++ b/apex/statsd/framework/api/current.txt
@@ -0,0 +1,12 @@
+// Signature format: 2.0
+package android.util {
+
+ public final class StatsLog {
+ method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public static boolean logBinaryPushStateChanged(@NonNull String, long, int, int, @NonNull long[]);
+ method public static boolean logEvent(int);
+ method public static boolean logStart(int);
+ method public static boolean logStop(int);
+ }
+
+}
+
diff --git a/apex/statsd/framework/api/module-lib-current.txt b/apex/statsd/framework/api/module-lib-current.txt
new file mode 100644
index 000000000000..8b6e2170002e
--- /dev/null
+++ b/apex/statsd/framework/api/module-lib-current.txt
@@ -0,0 +1,10 @@
+// Signature format: 2.0
+package android.os {
+
+ public class StatsFrameworkInitializer {
+ method public static void registerServiceWrappers();
+ method public static void setStatsServiceManager(@NonNull android.os.StatsServiceManager);
+ }
+
+}
+
diff --git a/apex/statsd/framework/api/module-lib-removed.txt b/apex/statsd/framework/api/module-lib-removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/statsd/framework/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/statsd/framework/api/removed.txt b/apex/statsd/framework/api/removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/statsd/framework/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/statsd/framework/api/system-current.txt b/apex/statsd/framework/api/system-current.txt
new file mode 100644
index 000000000000..3ea572450c1c
--- /dev/null
+++ b/apex/statsd/framework/api/system-current.txt
@@ -0,0 +1,111 @@
+// Signature format: 2.0
+package android.app {
+
+ public final class StatsManager {
+ method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public void addConfig(long, byte[]) throws android.app.StatsManager.StatsUnavailableException;
+ method @Deprecated @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public boolean addConfiguration(long, byte[]);
+ method @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM) public void clearPullAtomCallback(int);
+ method @Deprecated @Nullable @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public byte[] getData(long);
+ method @Deprecated @Nullable @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public byte[] getMetadata();
+ method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public long[] getRegisteredExperimentIds() throws android.app.StatsManager.StatsUnavailableException;
+ method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public byte[] getReports(long) throws android.app.StatsManager.StatsUnavailableException;
+ method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public byte[] getStatsMetadata() throws android.app.StatsManager.StatsUnavailableException;
+ method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public void removeConfig(long) throws android.app.StatsManager.StatsUnavailableException;
+ method @Deprecated @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public boolean removeConfiguration(long);
+ method @NonNull @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public long[] setActiveConfigsChangedOperation(@Nullable android.app.PendingIntent) throws android.app.StatsManager.StatsUnavailableException;
+ method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public void setBroadcastSubscriber(android.app.PendingIntent, long, long) throws android.app.StatsManager.StatsUnavailableException;
+ method @Deprecated @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public boolean setBroadcastSubscriber(long, long, android.app.PendingIntent);
+ method @Deprecated @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public boolean setDataFetchOperation(long, android.app.PendingIntent);
+ method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public void setFetchReportsOperation(android.app.PendingIntent, long) throws android.app.StatsManager.StatsUnavailableException;
+ method @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM) public void setPullAtomCallback(int, @Nullable android.app.StatsManager.PullAtomMetadata, @NonNull java.util.concurrent.Executor, @NonNull android.app.StatsManager.StatsPullAtomCallback);
+ field public static final String ACTION_STATSD_STARTED = "android.app.action.STATSD_STARTED";
+ field public static final String EXTRA_STATS_ACTIVE_CONFIG_KEYS = "android.app.extra.STATS_ACTIVE_CONFIG_KEYS";
+ field public static final String EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES = "android.app.extra.STATS_BROADCAST_SUBSCRIBER_COOKIES";
+ field public static final String EXTRA_STATS_CONFIG_KEY = "android.app.extra.STATS_CONFIG_KEY";
+ field public static final String EXTRA_STATS_CONFIG_UID = "android.app.extra.STATS_CONFIG_UID";
+ field public static final String EXTRA_STATS_DIMENSIONS_VALUE = "android.app.extra.STATS_DIMENSIONS_VALUE";
+ field public static final String EXTRA_STATS_SUBSCRIPTION_ID = "android.app.extra.STATS_SUBSCRIPTION_ID";
+ field public static final String EXTRA_STATS_SUBSCRIPTION_RULE_ID = "android.app.extra.STATS_SUBSCRIPTION_RULE_ID";
+ field public static final int PULL_SKIP = 1; // 0x1
+ field public static final int PULL_SUCCESS = 0; // 0x0
+ }
+
+ public static class StatsManager.PullAtomMetadata {
+ method @Nullable public int[] getAdditiveFields();
+ method public long getCoolDownMillis();
+ method public long getTimeoutMillis();
+ }
+
+ public static class StatsManager.PullAtomMetadata.Builder {
+ ctor public StatsManager.PullAtomMetadata.Builder();
+ method @NonNull public android.app.StatsManager.PullAtomMetadata build();
+ method @NonNull public android.app.StatsManager.PullAtomMetadata.Builder setAdditiveFields(@NonNull int[]);
+ method @NonNull public android.app.StatsManager.PullAtomMetadata.Builder setCoolDownMillis(long);
+ method @NonNull public android.app.StatsManager.PullAtomMetadata.Builder setTimeoutMillis(long);
+ }
+
+ public static interface StatsManager.StatsPullAtomCallback {
+ method public int onPullAtom(int, @NonNull java.util.List<android.util.StatsEvent>);
+ }
+
+ public static class StatsManager.StatsUnavailableException extends android.util.AndroidException {
+ ctor public StatsManager.StatsUnavailableException(String);
+ ctor public StatsManager.StatsUnavailableException(String, Throwable);
+ }
+
+}
+
+package android.os {
+
+ public final class StatsDimensionsValue implements android.os.Parcelable {
+ method public int describeContents();
+ method public boolean getBooleanValue();
+ method public int getField();
+ method public float getFloatValue();
+ method public int getIntValue();
+ method public long getLongValue();
+ method public String getStringValue();
+ method public java.util.List<android.os.StatsDimensionsValue> getTupleValueList();
+ method public int getValueType();
+ method public boolean isValueType(int);
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final int BOOLEAN_VALUE_TYPE = 5; // 0x5
+ field @NonNull public static final android.os.Parcelable.Creator<android.os.StatsDimensionsValue> CREATOR;
+ field public static final int FLOAT_VALUE_TYPE = 6; // 0x6
+ field public static final int INT_VALUE_TYPE = 3; // 0x3
+ field public static final int LONG_VALUE_TYPE = 4; // 0x4
+ field public static final int STRING_VALUE_TYPE = 2; // 0x2
+ field public static final int TUPLE_VALUE_TYPE = 7; // 0x7
+ }
+
+}
+
+package android.util {
+
+ public final class StatsEvent {
+ method @NonNull public static android.util.StatsEvent.Builder newBuilder();
+ }
+
+ public static final class StatsEvent.Builder {
+ method @NonNull public android.util.StatsEvent.Builder addBooleanAnnotation(byte, boolean);
+ method @NonNull public android.util.StatsEvent.Builder addIntAnnotation(byte, int);
+ method @NonNull public android.util.StatsEvent build();
+ method @NonNull public android.util.StatsEvent.Builder setAtomId(int);
+ method @NonNull public android.util.StatsEvent.Builder usePooledBuffer();
+ method @NonNull public android.util.StatsEvent.Builder writeAttributionChain(@NonNull int[], @NonNull String[]);
+ method @NonNull public android.util.StatsEvent.Builder writeBoolean(boolean);
+ method @NonNull public android.util.StatsEvent.Builder writeByteArray(@NonNull byte[]);
+ method @NonNull public android.util.StatsEvent.Builder writeFloat(float);
+ method @NonNull public android.util.StatsEvent.Builder writeInt(int);
+ method @NonNull public android.util.StatsEvent.Builder writeKeyValuePairs(@Nullable android.util.SparseIntArray, @Nullable android.util.SparseLongArray, @Nullable android.util.SparseArray<java.lang.String>, @Nullable android.util.SparseArray<java.lang.Float>);
+ method @NonNull public android.util.StatsEvent.Builder writeLong(long);
+ method @NonNull public android.util.StatsEvent.Builder writeString(@NonNull String);
+ }
+
+ public final class StatsLog {
+ method public static void write(@NonNull android.util.StatsEvent);
+ method public static void writeRaw(@NonNull byte[], int);
+ }
+
+}
+
diff --git a/apex/statsd/framework/api/system-removed.txt b/apex/statsd/framework/api/system-removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/apex/statsd/framework/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/statsd/framework/java/android/app/StatsManager.java b/apex/statsd/framework/java/android/app/StatsManager.java
new file mode 100644
index 000000000000..a7d20572ca96
--- /dev/null
+++ b/apex/statsd/framework/java/android/app/StatsManager.java
@@ -0,0 +1,725 @@
+/*
+ * Copyright 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;
+
+import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.PACKAGE_USAGE_STATS;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IPullAtomCallback;
+import android.os.IPullAtomResultReceiver;
+import android.os.IStatsManagerService;
+import android.os.RemoteException;
+import android.os.StatsFrameworkInitializer;
+import android.util.AndroidException;
+import android.util.Log;
+import android.util.StatsEvent;
+import android.util.StatsEventParcel;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * API for statsd clients to send configurations and retrieve data.
+ *
+ * @hide
+ */
+@SystemApi
+public final class StatsManager {
+ private static final String TAG = "StatsManager";
+ private static final boolean DEBUG = false;
+
+ private static final Object sLock = new Object();
+ private final Context mContext;
+
+ @GuardedBy("sLock")
+ private IStatsManagerService mStatsManagerService;
+
+ /**
+ * Long extra of uid that added the relevant stats config.
+ */
+ public static final String EXTRA_STATS_CONFIG_UID = "android.app.extra.STATS_CONFIG_UID";
+ /**
+ * Long extra of the relevant stats config's configKey.
+ */
+ public static final String EXTRA_STATS_CONFIG_KEY = "android.app.extra.STATS_CONFIG_KEY";
+ /**
+ * Long extra of the relevant statsd_config.proto's Subscription.id.
+ */
+ public static final String EXTRA_STATS_SUBSCRIPTION_ID =
+ "android.app.extra.STATS_SUBSCRIPTION_ID";
+ /**
+ * Long extra of the relevant statsd_config.proto's Subscription.rule_id.
+ */
+ public static final String EXTRA_STATS_SUBSCRIPTION_RULE_ID =
+ "android.app.extra.STATS_SUBSCRIPTION_RULE_ID";
+ /**
+ * List<String> of the relevant statsd_config.proto's BroadcastSubscriberDetails.cookie.
+ * Obtain using {@link android.content.Intent#getStringArrayListExtra(String)}.
+ */
+ public static final String EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES =
+ "android.app.extra.STATS_BROADCAST_SUBSCRIBER_COOKIES";
+ /**
+ * Extra of a {@link android.os.StatsDimensionsValue} representing sliced dimension value
+ * information.
+ */
+ public static final String EXTRA_STATS_DIMENSIONS_VALUE =
+ "android.app.extra.STATS_DIMENSIONS_VALUE";
+ /**
+ * Long array extra of the active configs for the uid that added those configs.
+ */
+ public static final String EXTRA_STATS_ACTIVE_CONFIG_KEYS =
+ "android.app.extra.STATS_ACTIVE_CONFIG_KEYS";
+
+ /**
+ * Broadcast Action: Statsd has started.
+ * Configurations and PendingIntents can now be sent to it.
+ */
+ public static final String ACTION_STATSD_STARTED = "android.app.action.STATSD_STARTED";
+
+ // Pull atom callback return codes.
+ /**
+ * Value indicating that this pull was successful and that the result should be used.
+ *
+ **/
+ public static final int PULL_SUCCESS = 0;
+
+ /**
+ * Value indicating that this pull was unsuccessful and that the result should not be used.
+ **/
+ public static final int PULL_SKIP = 1;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting public static final long DEFAULT_COOL_DOWN_MILLIS = 1_000L; // 1 second.
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting public static final long DEFAULT_TIMEOUT_MILLIS = 2_000L; // 2 seconds.
+
+ /**
+ * Constructor for StatsManagerClient.
+ *
+ * @hide
+ */
+ public StatsManager(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Adds the given configuration and associates it with the given configKey. If a config with the
+ * given configKey already exists for the caller's uid, it is replaced with the new one.
+ *
+ * @param configKey An arbitrary integer that allows clients to track the configuration.
+ * @param config Wire-encoded StatsdConfig proto that specifies metrics (and all
+ * dependencies eg, conditions and matchers).
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ * @throws IllegalArgumentException if config is not a wire-encoded StatsdConfig proto
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public void addConfig(long configKey, byte[] config) throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ // can throw IllegalArgumentException
+ service.addConfiguration(configKey, config, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when adding configuration");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to addConfig in statsmanager");
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #addConfig(long, byte[])}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public boolean addConfiguration(long configKey, byte[] config) {
+ try {
+ addConfig(configKey, config);
+ return true;
+ } catch (StatsUnavailableException | IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Remove a configuration from logging.
+ *
+ * @param configKey Configuration key to remove.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public void removeConfig(long configKey) throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ service.removeConfiguration(configKey, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when removing configuration");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to removeConfig in statsmanager");
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #removeConfig(long)}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public boolean removeConfiguration(long configKey) {
+ try {
+ removeConfig(configKey);
+ return true;
+ } catch (StatsUnavailableException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Set the PendingIntent to be used when broadcasting subscriber information to the given
+ * subscriberId within the given config.
+ * <p>
+ * Suppose that the calling uid has added a config with key configKey, and that in this config
+ * it is specified that when a particular anomaly is detected, a broadcast should be sent to
+ * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with
+ * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast
+ * when the anomaly is detected.
+ * <p>
+ * When statsd sends the broadcast, the PendingIntent will used to send an intent with
+ * information of
+ * {@link #EXTRA_STATS_CONFIG_UID},
+ * {@link #EXTRA_STATS_CONFIG_KEY},
+ * {@link #EXTRA_STATS_SUBSCRIPTION_ID},
+ * {@link #EXTRA_STATS_SUBSCRIPTION_RULE_ID},
+ * {@link #EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES}, and
+ * {@link #EXTRA_STATS_DIMENSIONS_VALUE}.
+ * <p>
+ * This function can only be called by the owner (uid) of the config. It must be called each
+ * time statsd starts. The config must have been added first (via {@link #addConfig}).
+ *
+ * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
+ * associated with the given subscriberId. May be null, in which case
+ * it undoes any previous setting of this subscriberId.
+ * @param configKey The integer naming the config to which this subscriber is attached.
+ * @param subscriberId ID of the subscriber, as used in the config.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public void setBroadcastSubscriber(
+ PendingIntent pendingIntent, long configKey, long subscriberId)
+ throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ if (pendingIntent != null) {
+ service.setBroadcastSubscriber(configKey, subscriberId, pendingIntent,
+ mContext.getOpPackageName());
+ } else {
+ service.unsetBroadcastSubscriber(configKey, subscriberId,
+ mContext.getOpPackageName());
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when adding broadcast subscriber",
+ e);
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #setBroadcastSubscriber(PendingIntent, long, long)}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public boolean setBroadcastSubscriber(
+ long configKey, long subscriberId, PendingIntent pendingIntent) {
+ try {
+ setBroadcastSubscriber(pendingIntent, configKey, subscriberId);
+ return true;
+ } catch (StatsUnavailableException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Registers the operation that is called to retrieve the metrics data. This must be called
+ * each time statsd starts. The config must have been added first (via {@link #addConfig},
+ * although addConfig could have been called on a previous boot). This operation allows
+ * statsd to send metrics data whenever statsd determines that the metrics in memory are
+ * approaching the memory limits. The fetch operation should call {@link #getReports} to fetch
+ * the data, which also deletes the retrieved metrics from statsd's memory.
+ *
+ * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
+ * associated with the given subscriberId. May be null, in which case
+ * it removes any associated pending intent with this configKey.
+ * @param configKey The integer naming the config to which this operation is attached.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public void setFetchReportsOperation(PendingIntent pendingIntent, long configKey)
+ throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ if (pendingIntent == null) {
+ service.removeDataFetchOperation(configKey, mContext.getOpPackageName());
+ } else {
+ service.setDataFetchOperation(configKey, pendingIntent,
+ mContext.getOpPackageName());
+ }
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when registering data listener.");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Registers the operation that is called whenever there is a change in which configs are
+ * active. This must be called each time statsd starts. This operation allows
+ * statsd to inform clients that they should pull data of the configs that are currently
+ * active. The activeConfigsChangedOperation should set periodic alarms to pull data of configs
+ * that are active and stop pulling data of configs that are no longer active.
+ *
+ * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
+ * associated with the given subscriberId. May be null, in which case
+ * it removes any associated pending intent for this client.
+ * @return A list of configs that are currently active for this client. If the pendingIntent is
+ * null, this will be an empty list.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public @NonNull long[] setActiveConfigsChangedOperation(@Nullable PendingIntent pendingIntent)
+ throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ if (pendingIntent == null) {
+ service.removeActiveConfigsChangedOperation(mContext.getOpPackageName());
+ return new long[0];
+ } else {
+ return service.setActiveConfigsChangedOperation(pendingIntent,
+ mContext.getOpPackageName());
+ }
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager "
+ + "when registering active configs listener.");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #setFetchReportsOperation(PendingIntent, long)}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) {
+ try {
+ setFetchReportsOperation(pendingIntent, configKey);
+ return true;
+ } catch (StatsUnavailableException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Request the data collected for the given configKey.
+ * This getter is destructive - it also clears the retrieved metrics from statsd's memory.
+ *
+ * @param configKey Configuration key to retrieve data from.
+ * @return Serialized ConfigMetricsReportList proto.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public byte[] getReports(long configKey) throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ return service.getData(configKey, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when getting data");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to getReports in statsmanager");
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #getReports(long)}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public @Nullable byte[] getData(long configKey) {
+ try {
+ return getReports(configKey);
+ } catch (StatsUnavailableException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Clients can request metadata for statsd. Will contain stats across all configurations but not
+ * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}.
+ * This getter is not destructive and will not reset any metrics/counters.
+ *
+ * @return Serialized StatsdStatsReport proto.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public byte[] getStatsMetadata() throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ return service.getMetadata(mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when getting metadata");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to getStatsMetadata in statsmanager");
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #getStatsMetadata()}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public @Nullable byte[] getMetadata() {
+ try {
+ return getStatsMetadata();
+ } catch (StatsUnavailableException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the experiments IDs registered with statsd, or an empty array if there aren't any.
+ *
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = {DUMP, PACKAGE_USAGE_STATS})
+ public long[] getRegisteredExperimentIds()
+ throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ return service.getRegisteredExperimentIds();
+ } catch (RemoteException e) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "Failed to connect to StatsManagerService when getting "
+ + "registered experiment IDs");
+ }
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to getRegisteredExperimentIds in statsmanager");
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Sets a callback for an atom when that atom is to be pulled. The stats service will
+ * invoke pullData in the callback when the stats service determines that this atom needs to be
+ * pulled. This method should not be called by third-party apps.
+ *
+ * @param atomTag The tag of the atom for this puller callback.
+ * @param metadata Optional metadata specifying the timeout, cool down time, and
+ * additive fields for mapping isolated to host uids.
+ * @param executor The executor in which to run the callback.
+ * @param callback The callback to be invoked when the stats service pulls the atom.
+ *
+ */
+ @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM)
+ public void setPullAtomCallback(int atomTag, @Nullable PullAtomMetadata metadata,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull StatsPullAtomCallback callback) {
+ long coolDownMillis =
+ metadata == null ? DEFAULT_COOL_DOWN_MILLIS : metadata.mCoolDownMillis;
+ long timeoutMillis = metadata == null ? DEFAULT_TIMEOUT_MILLIS : metadata.mTimeoutMillis;
+ int[] additiveFields = metadata == null ? new int[0] : metadata.mAdditiveFields;
+ if (additiveFields == null) {
+ additiveFields = new int[0];
+ }
+
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ PullAtomCallbackInternal rec =
+ new PullAtomCallbackInternal(atomTag, callback, executor);
+ service.registerPullAtomCallback(
+ atomTag, coolDownMillis, timeoutMillis, additiveFields, rec);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Unable to register pull callback", e);
+ }
+ }
+ }
+
+ /**
+ * Clears a callback for an atom when that atom is to be pulled. Note that any ongoing
+ * pulls will still occur. This method should not be called by third-party apps.
+ *
+ * @param atomTag The tag of the atom of which to unregister
+ *
+ */
+ @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM)
+ public void clearPullAtomCallback(int atomTag) {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ service.unregisterPullAtomCallback(atomTag);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Unable to unregister pull atom callback");
+ }
+ }
+ }
+
+ private static class PullAtomCallbackInternal extends IPullAtomCallback.Stub {
+ public final int mAtomId;
+ public final StatsPullAtomCallback mCallback;
+ public final Executor mExecutor;
+
+ PullAtomCallbackInternal(int atomId, StatsPullAtomCallback callback, Executor executor) {
+ mAtomId = atomId;
+ mCallback = callback;
+ mExecutor = executor;
+ }
+
+ @Override
+ public void onPullAtom(int atomTag, IPullAtomResultReceiver resultReceiver) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ List<StatsEvent> data = new ArrayList<>();
+ int successInt = mCallback.onPullAtom(atomTag, data);
+ boolean success = successInt == PULL_SUCCESS;
+ StatsEventParcel[] parcels = new StatsEventParcel[data.size()];
+ for (int i = 0; i < data.size(); i++) {
+ parcels[i] = new StatsEventParcel();
+ parcels[i].buffer = data.get(i).getBytes();
+ }
+ try {
+ resultReceiver.pullFinished(atomTag, success, parcels);
+ } catch (RemoteException e) {
+ Log.w(TAG, "StatsPullResultReceiver failed for tag " + mAtomId
+ + " due to TransactionTooLarge. Calling pullFinish with no data");
+ StatsEventParcel[] emptyData = new StatsEventParcel[0];
+ try {
+ resultReceiver.pullFinished(atomTag, /*success=*/false, emptyData);
+ } catch (RemoteException nestedException) {
+ Log.w(TAG, "StatsPullResultReceiver failed for tag " + mAtomId
+ + " with empty payload");
+ }
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+
+ /**
+ * Metadata required for registering a StatsPullAtomCallback.
+ * All fields are optional, and defaults will be used for fields that are unspecified.
+ *
+ */
+ public static class PullAtomMetadata {
+ private final long mCoolDownMillis;
+ private final long mTimeoutMillis;
+ private final int[] mAdditiveFields;
+
+ // Private Constructor for builder
+ private PullAtomMetadata(long coolDownMillis, long timeoutMillis, int[] additiveFields) {
+ mCoolDownMillis = coolDownMillis;
+ mTimeoutMillis = timeoutMillis;
+ mAdditiveFields = additiveFields;
+ }
+
+ /**
+ * Builder for PullAtomMetadata.
+ */
+ public static class Builder {
+ private long mCoolDownMillis;
+ private long mTimeoutMillis;
+ private int[] mAdditiveFields;
+
+ /**
+ * Returns a new PullAtomMetadata.Builder object for constructing PullAtomMetadata for
+ * StatsManager#registerPullAtomCallback
+ */
+ public Builder() {
+ mCoolDownMillis = DEFAULT_COOL_DOWN_MILLIS;
+ mTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
+ mAdditiveFields = null;
+ }
+
+ /**
+ * Set the cool down time of the pull in milliseconds. If two successive pulls are
+ * issued within the cool down, a cached version of the first pull will be used for the
+ * second pull. The minimum allowed cool down is 1 second.
+ */
+ @NonNull
+ public Builder setCoolDownMillis(long coolDownMillis) {
+ mCoolDownMillis = coolDownMillis;
+ return this;
+ }
+
+ /**
+ * Set the maximum time the pull can take in milliseconds. The maximum allowed timeout
+ * is 10 seconds.
+ */
+ @NonNull
+ public Builder setTimeoutMillis(long timeoutMillis) {
+ mTimeoutMillis = timeoutMillis;
+ return this;
+ }
+
+ /**
+ * Set the additive fields of this pulled atom.
+ *
+ * This is only applicable for atoms which have a uid field. When tasks are run in
+ * isolated processes, the data will be attributed to the host uid. Additive fields
+ * will be combined when the non-additive fields are the same.
+ */
+ @NonNull
+ public Builder setAdditiveFields(@NonNull int[] additiveFields) {
+ mAdditiveFields = additiveFields;
+ return this;
+ }
+
+ /**
+ * Builds and returns a PullAtomMetadata object with the values set in the builder and
+ * defaults for unset fields.
+ */
+ @NonNull
+ public PullAtomMetadata build() {
+ return new PullAtomMetadata(mCoolDownMillis, mTimeoutMillis, mAdditiveFields);
+ }
+ }
+
+ /**
+ * Return the cool down time of this pull in milliseconds.
+ */
+ public long getCoolDownMillis() {
+ return mCoolDownMillis;
+ }
+
+ /**
+ * Return the maximum amount of time this pull can take in milliseconds.
+ */
+ public long getTimeoutMillis() {
+ return mTimeoutMillis;
+ }
+
+ /**
+ * Return the additive fields of this pulled atom.
+ *
+ * This is only applicable for atoms that have a uid field. When tasks are run in
+ * isolated processes, the data will be attributed to the host uid. Additive fields
+ * will be combined when the non-additive fields are the same.
+ */
+ @Nullable
+ public int[] getAdditiveFields() {
+ return mAdditiveFields;
+ }
+ }
+
+ /**
+ * Callback interface for pulling atoms requested by the stats service.
+ *
+ */
+ public interface StatsPullAtomCallback {
+ /**
+ * Pull data for the specified atom tag, filling in the provided list of StatsEvent data.
+ * @return {@link #PULL_SUCCESS} if the pull was successful, or {@link #PULL_SKIP} if not.
+ */
+ int onPullAtom(int atomTag, @NonNull List<StatsEvent> data);
+ }
+
+ @GuardedBy("sLock")
+ private IStatsManagerService getIStatsManagerServiceLocked() {
+ if (mStatsManagerService != null) {
+ return mStatsManagerService;
+ }
+ mStatsManagerService = IStatsManagerService.Stub.asInterface(
+ StatsFrameworkInitializer
+ .getStatsServiceManager()
+ .getStatsManagerServiceRegisterer()
+ .get());
+ return mStatsManagerService;
+ }
+
+ /**
+ * Exception thrown when communication with the stats service fails (eg if it is not available).
+ * This might be thrown early during boot before the stats service has started or if it crashed.
+ */
+ public static class StatsUnavailableException extends AndroidException {
+ public StatsUnavailableException(String reason) {
+ super("Failed to connect to statsd: " + reason);
+ }
+
+ public StatsUnavailableException(String reason, Throwable e) {
+ super("Failed to connect to statsd: " + reason, e);
+ }
+ }
+}
diff --git a/apex/statsd/framework/java/android/os/StatsDimensionsValue.java b/apex/statsd/framework/java/android/os/StatsDimensionsValue.java
new file mode 100644
index 000000000000..7d9349cefa48
--- /dev/null
+++ b/apex/statsd/framework/java/android/os/StatsDimensionsValue.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright 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.SystemApi;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Container for statsd dimension value information, corresponding to a
+ * stats_log.proto's DimensionValue.
+ *
+ * This consists of a field (an int representing a statsd atom field)
+ * and a value (which may be one of a number of types).
+ *
+ * <p>
+ * Only a single value is held, and it is necessarily one of the following types:
+ * {@link String}, int, long, boolean, float,
+ * or tuple (i.e. {@link List} of {@code StatsDimensionsValue}).
+ *
+ * The type of value held can be retrieved using {@link #getValueType()}, which returns one of the
+ * following ints, depending on the type of value:
+ * <ul>
+ * <li>{@link #STRING_VALUE_TYPE}</li>
+ * <li>{@link #INT_VALUE_TYPE}</li>
+ * <li>{@link #LONG_VALUE_TYPE}</li>
+ * <li>{@link #BOOLEAN_VALUE_TYPE}</li>
+ * <li>{@link #FLOAT_VALUE_TYPE}</li>
+ * <li>{@link #TUPLE_VALUE_TYPE}</li>
+ * </ul>
+ * Alternatively, this can be determined using {@link #isValueType(int)} with one of these constants
+ * as a parameter.
+ * The value itself can be retrieved using the correct get...Value() function for its type.
+ *
+ * <p>
+ * The field is always an int, and always exists; it can be obtained using {@link #getField()}.
+ *
+ *
+ * @hide
+ */
+@SystemApi
+public final class StatsDimensionsValue implements Parcelable {
+ private static final String TAG = "StatsDimensionsValue";
+
+ // Values of the value type correspond to stats_log.proto's DimensionValue fields.
+ // Keep constants in sync with frameworks/base/cmds/statsd/src/HashableDimensionKey.cpp.
+ /** Indicates that this holds a String. */
+ public static final int STRING_VALUE_TYPE = 2;
+ /** Indicates that this holds an int. */
+ public static final int INT_VALUE_TYPE = 3;
+ /** Indicates that this holds a long. */
+ public static final int LONG_VALUE_TYPE = 4;
+ /** Indicates that this holds a boolean. */
+ public static final int BOOLEAN_VALUE_TYPE = 5;
+ /** Indicates that this holds a float. */
+ public static final int FLOAT_VALUE_TYPE = 6;
+ /** Indicates that this holds a List of StatsDimensionsValues. */
+ public static final int TUPLE_VALUE_TYPE = 7;
+
+ private final StatsDimensionsValueParcel mInner;
+
+ /**
+ * Creates a {@code StatsDimensionValue} from a parcel.
+ *
+ * @hide
+ */
+ public StatsDimensionsValue(Parcel in) {
+ mInner = StatsDimensionsValueParcel.CREATOR.createFromParcel(in);
+ }
+
+ /**
+ * Creates a {@code StatsDimensionsValue} from a StatsDimensionsValueParcel
+ *
+ * @hide
+ */
+ public StatsDimensionsValue(StatsDimensionsValueParcel parcel) {
+ mInner = parcel;
+ }
+
+ /**
+ * Return the field, i.e. the tag of a statsd atom.
+ *
+ * @return the field
+ */
+ public int getField() {
+ return mInner.field;
+ }
+
+ /**
+ * Retrieve the String held, if any.
+ *
+ * @return the {@link String} held if {@link #getValueType()} == {@link #STRING_VALUE_TYPE},
+ * null otherwise
+ */
+ public String getStringValue() {
+ if (mInner.valueType == STRING_VALUE_TYPE) {
+ return mInner.stringValue;
+ } else {
+ Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not string.");
+ return null;
+ }
+ }
+
+ /**
+ * Retrieve the int held, if any.
+ *
+ * @return the int held if {@link #getValueType()} == {@link #INT_VALUE_TYPE}, 0 otherwise
+ */
+ public int getIntValue() {
+ if (mInner.valueType == INT_VALUE_TYPE) {
+ return mInner.intValue;
+ } else {
+ Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not int.");
+ return 0;
+ }
+ }
+
+ /**
+ * Retrieve the long held, if any.
+ *
+ * @return the long held if {@link #getValueType()} == {@link #LONG_VALUE_TYPE}, 0 otherwise
+ */
+ public long getLongValue() {
+ if (mInner.valueType == LONG_VALUE_TYPE) {
+ return mInner.longValue;
+ } else {
+ Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not long.");
+ return 0;
+ }
+ }
+
+ /**
+ * Retrieve the boolean held, if any.
+ *
+ * @return the boolean held if {@link #getValueType()} == {@link #BOOLEAN_VALUE_TYPE},
+ * false otherwise
+ */
+ public boolean getBooleanValue() {
+ if (mInner.valueType == BOOLEAN_VALUE_TYPE) {
+ return mInner.boolValue;
+ } else {
+ Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not boolean.");
+ return false;
+ }
+ }
+
+ /**
+ * Retrieve the float held, if any.
+ *
+ * @return the float held if {@link #getValueType()} == {@link #FLOAT_VALUE_TYPE}, 0 otherwise
+ */
+ public float getFloatValue() {
+ if (mInner.valueType == FLOAT_VALUE_TYPE) {
+ return mInner.floatValue;
+ } else {
+ Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not float.");
+ return 0;
+ }
+ }
+
+ /**
+ * Retrieve the tuple, in the form of a {@link List} of {@link StatsDimensionsValue}, held,
+ * if any.
+ *
+ * @return the {@link List} of {@link StatsDimensionsValue} held
+ * if {@link #getValueType()} == {@link #TUPLE_VALUE_TYPE},
+ * null otherwise
+ */
+ public List<StatsDimensionsValue> getTupleValueList() {
+ if (mInner.valueType == TUPLE_VALUE_TYPE) {
+ int length = (mInner.tupleValue == null) ? 0 : mInner.tupleValue.length;
+ List<StatsDimensionsValue> tupleValues = new ArrayList<>(length);
+ for (int i = 0; i < length; i++) {
+ tupleValues.add(new StatsDimensionsValue(mInner.tupleValue[i]));
+ }
+ return tupleValues;
+ } else {
+ Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not tuple.");
+ return null;
+ }
+ }
+
+ /**
+ * Returns the constant representing the type of value stored, namely one of
+ * <ul>
+ * <li>{@link #STRING_VALUE_TYPE}</li>
+ * <li>{@link #INT_VALUE_TYPE}</li>
+ * <li>{@link #LONG_VALUE_TYPE}</li>
+ * <li>{@link #BOOLEAN_VALUE_TYPE}</li>
+ * <li>{@link #FLOAT_VALUE_TYPE}</li>
+ * <li>{@link #TUPLE_VALUE_TYPE}</li>
+ * </ul>
+ *
+ * @return the constant representing the type of value stored
+ */
+ public int getValueType() {
+ return mInner.valueType;
+ }
+
+ /**
+ * Returns whether the type of value stored is equal to the given type.
+ *
+ * @param valueType int representing the type of value stored, as used in {@link #getValueType}
+ * @return true if {@link #getValueType()} is equal to {@code valueType}.
+ */
+ public boolean isValueType(int valueType) {
+ return mInner.valueType == valueType;
+ }
+
+ /**
+ * Returns a String representing the information in this StatsDimensionValue.
+ * No guarantees are made about the format of this String.
+ *
+ * @return String representation
+ *
+ * @hide
+ */
+ // Follows the format of statsd's dimension.h toString.
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(mInner.field);
+ sb.append(":");
+ switch (mInner.valueType) {
+ case STRING_VALUE_TYPE:
+ sb.append(mInner.stringValue);
+ break;
+ case INT_VALUE_TYPE:
+ sb.append(String.valueOf(mInner.intValue));
+ break;
+ case LONG_VALUE_TYPE:
+ sb.append(String.valueOf(mInner.longValue));
+ break;
+ case BOOLEAN_VALUE_TYPE:
+ sb.append(String.valueOf(mInner.boolValue));
+ break;
+ case FLOAT_VALUE_TYPE:
+ sb.append(String.valueOf(mInner.floatValue));
+ break;
+ case TUPLE_VALUE_TYPE:
+ sb.append("{");
+ int length = (mInner.tupleValue == null) ? 0 : mInner.tupleValue.length;
+ for (int i = 0; i < length; i++) {
+ StatsDimensionsValue child = new StatsDimensionsValue(mInner.tupleValue[i]);
+ sb.append(child.toString());
+ sb.append("|");
+ }
+ sb.append("}");
+ break;
+ default:
+ Log.w(TAG, "Incorrect value type");
+ break;
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Parcelable Creator for StatsDimensionsValue.
+ */
+ public static final @android.annotation.NonNull
+ Parcelable.Creator<StatsDimensionsValue> CREATOR = new
+ Parcelable.Creator<StatsDimensionsValue>() {
+ public StatsDimensionsValue createFromParcel(Parcel in) {
+ return new StatsDimensionsValue(in);
+ }
+
+ public StatsDimensionsValue[] newArray(int size) {
+ return new StatsDimensionsValue[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ mInner.writeToParcel(out, flags);
+ }
+
+ /**
+ * Returns a string representation of the type of value stored.
+ */
+ private String getValueTypeAsString() {
+ switch (mInner.valueType) {
+ case STRING_VALUE_TYPE:
+ return "string";
+ case INT_VALUE_TYPE:
+ return "int";
+ case LONG_VALUE_TYPE:
+ return "long";
+ case BOOLEAN_VALUE_TYPE:
+ return "boolean";
+ case FLOAT_VALUE_TYPE:
+ return "float";
+ case TUPLE_VALUE_TYPE:
+ return "tuple";
+ default:
+ return "unknown";
+ }
+ }
+}
diff --git a/apex/statsd/framework/java/android/os/StatsFrameworkInitializer.java b/apex/statsd/framework/java/android/os/StatsFrameworkInitializer.java
new file mode 100644
index 000000000000..8dc91239c2e0
--- /dev/null
+++ b/apex/statsd/framework/java/android/os/StatsFrameworkInitializer.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2020 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.SystemApi;
+import android.annotation.SystemApi.Client;
+import android.app.StatsManager;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+
+/**
+ * Class for performing registration for all stats services
+ *
+ * @hide
+ */
+@SystemApi(client = Client.MODULE_LIBRARIES)
+public class StatsFrameworkInitializer {
+ private StatsFrameworkInitializer() {
+ }
+
+ private static volatile StatsServiceManager sStatsServiceManager;
+
+ /**
+ * Sets an instance of {@link StatsServiceManager} that allows
+ * the statsd mainline module to register/obtain stats binder services. This is called
+ * by the platform during the system initialization.
+ *
+ * @param statsServiceManager instance of {@link StatsServiceManager} that allows
+ * the statsd mainline module to register/obtain statsd binder services.
+ */
+ public static void setStatsServiceManager(
+ @NonNull StatsServiceManager statsServiceManager) {
+ if (sStatsServiceManager != null) {
+ throw new IllegalStateException("setStatsServiceManager called twice!");
+ }
+
+ if (statsServiceManager == null) {
+ throw new NullPointerException("statsServiceManager is null");
+ }
+
+ sStatsServiceManager = statsServiceManager;
+ }
+
+ /** @hide */
+ public static StatsServiceManager getStatsServiceManager() {
+ return sStatsServiceManager;
+ }
+
+ /**
+ * Called by {@link SystemServiceRegistry}'s static initializer and registers all statsd
+ * services to {@link Context}, so that {@link Context#getSystemService} can return them.
+ *
+ * @throws IllegalStateException if this is called from anywhere besides
+ * {@link SystemServiceRegistry}
+ */
+ public static void registerServiceWrappers() {
+ SystemServiceRegistry.registerContextAwareService(
+ Context.STATS_MANAGER,
+ StatsManager.class,
+ context -> new StatsManager(context)
+ );
+ }
+}
diff --git a/apex/statsd/framework/java/android/util/StatsEvent.java b/apex/statsd/framework/java/android/util/StatsEvent.java
new file mode 100644
index 000000000000..8be5c63f31e3
--- /dev/null
+++ b/apex/statsd/framework/java/android/util/StatsEvent.java
@@ -0,0 +1,879 @@
+/*
+ * 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.util;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.SystemClock;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+
+/**
+ * StatsEvent builds and stores the buffer sent over the statsd socket.
+ * This class defines and encapsulates the socket protocol.
+ *
+ * <p>Usage:</p>
+ * <pre>
+ * // Pushed event
+ * StatsEvent statsEvent = StatsEvent.newBuilder()
+ * .setAtomId(atomId)
+ * .writeBoolean(false)
+ * .writeString("annotated String field")
+ * .addBooleanAnnotation(annotationId, true)
+ * .usePooledBuffer()
+ * .build();
+ * StatsLog.write(statsEvent);
+ *
+ * // Pulled event
+ * StatsEvent statsEvent = StatsEvent.newBuilder()
+ * .setAtomId(atomId)
+ * .writeBoolean(false)
+ * .writeString("annotated String field")
+ * .addBooleanAnnotation(annotationId, true)
+ * .build();
+ * </pre>
+ * @hide
+ **/
+@SystemApi
+public final class StatsEvent {
+ // Type Ids.
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_INT = 0x00;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_LONG = 0x01;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_STRING = 0x02;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_LIST = 0x03;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_FLOAT = 0x04;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_BOOLEAN = 0x05;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_BYTE_ARRAY = 0x06;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_OBJECT = 0x07;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_KEY_VALUE_PAIRS = 0x08;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_ATTRIBUTION_CHAIN = 0x09;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_ERRORS = 0x0F;
+
+ // Error flags.
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_NO_TIMESTAMP = 0x1;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_NO_ATOM_ID = 0x2;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_OVERFLOW = 0x4;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_ATTRIBUTION_CHAIN_TOO_LONG = 0x8;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_TOO_MANY_KEY_VALUE_PAIRS = 0x10;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD = 0x20;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_INVALID_ANNOTATION_ID = 0x40;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_ANNOTATION_ID_TOO_LARGE = 0x80;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_TOO_MANY_ANNOTATIONS = 0x100;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_TOO_MANY_FIELDS = 0x200;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_ATTRIBUTION_UIDS_TAGS_SIZES_NOT_EQUAL = 0x1000;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_ATOM_ID_INVALID_POSITION = 0x2000;
+
+ // Size limits.
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int MAX_ANNOTATION_COUNT = 15;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int MAX_ATTRIBUTION_NODES = 127;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int MAX_NUM_ELEMENTS = 127;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int MAX_KEY_VALUE_PAIRS = 127;
+
+ private static final int LOGGER_ENTRY_MAX_PAYLOAD = 4068;
+
+ // Max payload size is 4 bytes less as 4 bytes are reserved for statsEventTag.
+ // See android_util_StatsLog.cpp.
+ private static final int MAX_PUSH_PAYLOAD_SIZE = LOGGER_ENTRY_MAX_PAYLOAD - 4;
+
+ private static final int MAX_PULL_PAYLOAD_SIZE = 50 * 1024; // 50 KB
+
+ private final int mAtomId;
+ private final byte[] mPayload;
+ private Buffer mBuffer;
+ private final int mNumBytes;
+
+ private StatsEvent(final int atomId, @Nullable final Buffer buffer,
+ @NonNull final byte[] payload, final int numBytes) {
+ mAtomId = atomId;
+ mBuffer = buffer;
+ mPayload = payload;
+ mNumBytes = numBytes;
+ }
+
+ /**
+ * Returns a new StatsEvent.Builder for building StatsEvent object.
+ **/
+ @NonNull
+ public static StatsEvent.Builder newBuilder() {
+ return new StatsEvent.Builder(Buffer.obtain());
+ }
+
+ /**
+ * Get the atom Id of the atom encoded in this StatsEvent object.
+ *
+ * @hide
+ **/
+ public int getAtomId() {
+ return mAtomId;
+ }
+
+ /**
+ * Get the byte array that contains the encoded payload that can be sent to statsd.
+ *
+ * @hide
+ **/
+ @NonNull
+ public byte[] getBytes() {
+ return mPayload;
+ }
+
+ /**
+ * Get the number of bytes used to encode the StatsEvent payload.
+ *
+ * @hide
+ **/
+ public int getNumBytes() {
+ return mNumBytes;
+ }
+
+ /**
+ * Recycle resources used by this StatsEvent object.
+ * No actions should be taken on this StatsEvent after release() is called.
+ *
+ * @hide
+ **/
+ public void release() {
+ if (mBuffer != null) {
+ mBuffer.release();
+ mBuffer = null;
+ }
+ }
+
+ /**
+ * Builder for constructing a StatsEvent object.
+ *
+ * <p>This class defines and encapsulates the socket encoding for the buffer.
+ * The write methods must be called in the same order as the order of fields in the
+ * atom definition.</p>
+ *
+ * <p>setAtomId() can be called anytime before build().</p>
+ *
+ * <p>Example:</p>
+ * <pre>
+ * // Atom definition.
+ * message MyAtom {
+ * optional int32 field1 = 1;
+ * optional int64 field2 = 2;
+ * optional string field3 = 3 [(annotation1) = true];
+ * }
+ *
+ * // StatsEvent construction for pushed event.
+ * StatsEvent.newBuilder()
+ * StatsEvent statsEvent = StatsEvent.newBuilder()
+ * .setAtomId(atomId)
+ * .writeInt(3) // field1
+ * .writeLong(8L) // field2
+ * .writeString("foo") // field 3
+ * .addBooleanAnnotation(annotation1Id, true)
+ * .usePooledBuffer()
+ * .build();
+ *
+ * // StatsEvent construction for pulled event.
+ * StatsEvent.newBuilder()
+ * StatsEvent statsEvent = StatsEvent.newBuilder()
+ * .setAtomId(atomId)
+ * .writeInt(3) // field1
+ * .writeLong(8L) // field2
+ * .writeString("foo") // field 3
+ * .addBooleanAnnotation(annotation1Id, true)
+ * .build();
+ * </pre>
+ **/
+ public static final class Builder {
+ // Fixed positions.
+ private static final int POS_NUM_ELEMENTS = 1;
+ private static final int POS_TIMESTAMP_NS = POS_NUM_ELEMENTS + Byte.BYTES;
+ private static final int POS_ATOM_ID = POS_TIMESTAMP_NS + Byte.BYTES + Long.BYTES;
+
+ private final Buffer mBuffer;
+ private long mTimestampNs;
+ private int mAtomId;
+ private byte mCurrentAnnotationCount;
+ private int mPos;
+ private int mPosLastField;
+ private byte mLastType;
+ private int mNumElements;
+ private int mErrorMask;
+ private boolean mUsePooledBuffer = false;
+
+ private Builder(final Buffer buffer) {
+ mBuffer = buffer;
+ mCurrentAnnotationCount = 0;
+ mAtomId = 0;
+ mTimestampNs = SystemClock.elapsedRealtimeNanos();
+ mNumElements = 0;
+
+ // Set mPos to 0 for writing TYPE_OBJECT at 0th position.
+ mPos = 0;
+ writeTypeId(TYPE_OBJECT);
+
+ // Write timestamp.
+ mPos = POS_TIMESTAMP_NS;
+ writeLong(mTimestampNs);
+ }
+
+ /**
+ * Sets the atom id for this StatsEvent.
+ *
+ * This should be called immediately after StatsEvent.newBuilder()
+ * and should only be called once.
+ * Not calling setAtomId will result in ERROR_NO_ATOM_ID.
+ * Calling setAtomId out of order will result in ERROR_ATOM_ID_INVALID_POSITION.
+ **/
+ @NonNull
+ public Builder setAtomId(final int atomId) {
+ if (0 == mAtomId) {
+ mAtomId = atomId;
+
+ if (1 == mNumElements) { // Only timestamp is written so far.
+ writeInt(atomId);
+ } else {
+ // setAtomId called out of order.
+ mErrorMask |= ERROR_ATOM_ID_INVALID_POSITION;
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * Write a boolean field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeBoolean(final boolean value) {
+ // Write boolean typeId byte followed by boolean byte representation.
+ writeTypeId(TYPE_BOOLEAN);
+ mPos += mBuffer.putBoolean(mPos, value);
+ mNumElements++;
+ return this;
+ }
+
+ /**
+ * Write an integer field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeInt(final int value) {
+ // Write integer typeId byte followed by 4-byte representation of value.
+ writeTypeId(TYPE_INT);
+ mPos += mBuffer.putInt(mPos, value);
+ mNumElements++;
+ return this;
+ }
+
+ /**
+ * Write a long field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeLong(final long value) {
+ // Write long typeId byte followed by 8-byte representation of value.
+ writeTypeId(TYPE_LONG);
+ mPos += mBuffer.putLong(mPos, value);
+ mNumElements++;
+ return this;
+ }
+
+ /**
+ * Write a float field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeFloat(final float value) {
+ // Write float typeId byte followed by 4-byte representation of value.
+ writeTypeId(TYPE_FLOAT);
+ mPos += mBuffer.putFloat(mPos, value);
+ mNumElements++;
+ return this;
+ }
+
+ /**
+ * Write a String field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeString(@NonNull final String value) {
+ // Write String typeId byte, followed by 4-byte representation of number of bytes
+ // in the UTF-8 encoding, followed by the actual UTF-8 byte encoding of value.
+ final byte[] valueBytes = stringToBytes(value);
+ writeByteArray(valueBytes, TYPE_STRING);
+ return this;
+ }
+
+ /**
+ * Write a byte array field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeByteArray(@NonNull final byte[] value) {
+ // Write byte array typeId byte, followed by 4-byte representation of number of bytes
+ // in value, followed by the actual byte array.
+ writeByteArray(value, TYPE_BYTE_ARRAY);
+ return this;
+ }
+
+ private void writeByteArray(@NonNull final byte[] value, final byte typeId) {
+ writeTypeId(typeId);
+ final int numBytes = value.length;
+ mPos += mBuffer.putInt(mPos, numBytes);
+ mPos += mBuffer.putByteArray(mPos, value);
+ mNumElements++;
+ }
+
+ /**
+ * Write an attribution chain field to this StatsEvent.
+ *
+ * The sizes of uids and tags must be equal. The AttributionNode at position i is
+ * made up of uids[i] and tags[i].
+ *
+ * @param uids array of uids in the attribution nodes.
+ * @param tags array of tags in the attribution nodes.
+ **/
+ @NonNull
+ public Builder writeAttributionChain(
+ @NonNull final int[] uids, @NonNull final String[] tags) {
+ final byte numUids = (byte) uids.length;
+ final byte numTags = (byte) tags.length;
+
+ if (numUids != numTags) {
+ mErrorMask |= ERROR_ATTRIBUTION_UIDS_TAGS_SIZES_NOT_EQUAL;
+ } else if (numUids > MAX_ATTRIBUTION_NODES) {
+ mErrorMask |= ERROR_ATTRIBUTION_CHAIN_TOO_LONG;
+ } else {
+ // Write attribution chain typeId byte, followed by 1-byte representation of
+ // number of attribution nodes, followed by encoding of each attribution node.
+ writeTypeId(TYPE_ATTRIBUTION_CHAIN);
+ mPos += mBuffer.putByte(mPos, numUids);
+ for (int i = 0; i < numUids; i++) {
+ // Each uid is encoded as 4-byte representation of its int value.
+ mPos += mBuffer.putInt(mPos, uids[i]);
+
+ // Each tag is encoded as 4-byte representation of number of bytes in its
+ // UTF-8 encoding, followed by the actual UTF-8 bytes.
+ final byte[] tagBytes = stringToBytes(tags[i]);
+ mPos += mBuffer.putInt(mPos, tagBytes.length);
+ mPos += mBuffer.putByteArray(mPos, tagBytes);
+ }
+ mNumElements++;
+ }
+ return this;
+ }
+
+ /**
+ * Write KeyValuePairsAtom entries to this StatsEvent.
+ *
+ * @param intMap Integer key-value pairs.
+ * @param longMap Long key-value pairs.
+ * @param stringMap String key-value pairs.
+ * @param floatMap Float key-value pairs.
+ **/
+ @NonNull
+ public Builder writeKeyValuePairs(
+ @Nullable final SparseIntArray intMap,
+ @Nullable final SparseLongArray longMap,
+ @Nullable final SparseArray<String> stringMap,
+ @Nullable final SparseArray<Float> floatMap) {
+ final int intMapSize = null == intMap ? 0 : intMap.size();
+ final int longMapSize = null == longMap ? 0 : longMap.size();
+ final int stringMapSize = null == stringMap ? 0 : stringMap.size();
+ final int floatMapSize = null == floatMap ? 0 : floatMap.size();
+ final int totalCount = intMapSize + longMapSize + stringMapSize + floatMapSize;
+
+ if (totalCount > MAX_KEY_VALUE_PAIRS) {
+ mErrorMask |= ERROR_TOO_MANY_KEY_VALUE_PAIRS;
+ } else {
+ writeTypeId(TYPE_KEY_VALUE_PAIRS);
+ mPos += mBuffer.putByte(mPos, (byte) totalCount);
+
+ for (int i = 0; i < intMapSize; i++) {
+ final int key = intMap.keyAt(i);
+ final int value = intMap.valueAt(i);
+ mPos += mBuffer.putInt(mPos, key);
+ writeTypeId(TYPE_INT);
+ mPos += mBuffer.putInt(mPos, value);
+ }
+
+ for (int i = 0; i < longMapSize; i++) {
+ final int key = longMap.keyAt(i);
+ final long value = longMap.valueAt(i);
+ mPos += mBuffer.putInt(mPos, key);
+ writeTypeId(TYPE_LONG);
+ mPos += mBuffer.putLong(mPos, value);
+ }
+
+ for (int i = 0; i < stringMapSize; i++) {
+ final int key = stringMap.keyAt(i);
+ final String value = stringMap.valueAt(i);
+ mPos += mBuffer.putInt(mPos, key);
+ writeTypeId(TYPE_STRING);
+ final byte[] valueBytes = stringToBytes(value);
+ mPos += mBuffer.putInt(mPos, valueBytes.length);
+ mPos += mBuffer.putByteArray(mPos, valueBytes);
+ }
+
+ for (int i = 0; i < floatMapSize; i++) {
+ final int key = floatMap.keyAt(i);
+ final float value = floatMap.valueAt(i);
+ mPos += mBuffer.putInt(mPos, key);
+ writeTypeId(TYPE_FLOAT);
+ mPos += mBuffer.putFloat(mPos, value);
+ }
+
+ mNumElements++;
+ }
+
+ return this;
+ }
+
+ /**
+ * Write a boolean annotation for the last field written.
+ **/
+ @NonNull
+ public Builder addBooleanAnnotation(
+ final byte annotationId, final boolean value) {
+ // Ensure there's a field written to annotate.
+ if (mNumElements < 2) {
+ mErrorMask |= ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD;
+ } else if (mCurrentAnnotationCount >= MAX_ANNOTATION_COUNT) {
+ mErrorMask |= ERROR_TOO_MANY_ANNOTATIONS;
+ } else {
+ mPos += mBuffer.putByte(mPos, annotationId);
+ mPos += mBuffer.putByte(mPos, TYPE_BOOLEAN);
+ mPos += mBuffer.putBoolean(mPos, value);
+ mCurrentAnnotationCount++;
+ writeAnnotationCount();
+ }
+
+ return this;
+ }
+
+ /**
+ * Write an integer annotation for the last field written.
+ **/
+ @NonNull
+ public Builder addIntAnnotation(final byte annotationId, final int value) {
+ if (mNumElements < 2) {
+ mErrorMask |= ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD;
+ } else if (mCurrentAnnotationCount >= MAX_ANNOTATION_COUNT) {
+ mErrorMask |= ERROR_TOO_MANY_ANNOTATIONS;
+ } else {
+ mPos += mBuffer.putByte(mPos, annotationId);
+ mPos += mBuffer.putByte(mPos, TYPE_INT);
+ mPos += mBuffer.putInt(mPos, value);
+ mCurrentAnnotationCount++;
+ writeAnnotationCount();
+ }
+
+ return this;
+ }
+
+ /**
+ * Indicates to reuse Buffer's byte array as the underlying payload in StatsEvent.
+ * This should be called for pushed events to reduce memory allocations and garbage
+ * collections.
+ **/
+ @NonNull
+ public Builder usePooledBuffer() {
+ mUsePooledBuffer = true;
+ mBuffer.setMaxSize(MAX_PUSH_PAYLOAD_SIZE, mPos);
+ return this;
+ }
+
+ /**
+ * Builds a StatsEvent object with values entered in this Builder.
+ **/
+ @NonNull
+ public StatsEvent build() {
+ if (0L == mTimestampNs) {
+ mErrorMask |= ERROR_NO_TIMESTAMP;
+ }
+ if (0 == mAtomId) {
+ mErrorMask |= ERROR_NO_ATOM_ID;
+ }
+ if (mBuffer.hasOverflowed()) {
+ mErrorMask |= ERROR_OVERFLOW;
+ }
+ if (mNumElements > MAX_NUM_ELEMENTS) {
+ mErrorMask |= ERROR_TOO_MANY_FIELDS;
+ }
+
+ if (0 == mErrorMask) {
+ mBuffer.putByte(POS_NUM_ELEMENTS, (byte) mNumElements);
+ } else {
+ // Write atom id and error mask. Overwrite any annotations for atom Id.
+ mPos = POS_ATOM_ID;
+ mPos += mBuffer.putByte(mPos, TYPE_INT);
+ mPos += mBuffer.putInt(mPos, mAtomId);
+ mPos += mBuffer.putByte(mPos, TYPE_ERRORS);
+ mPos += mBuffer.putInt(mPos, mErrorMask);
+ mBuffer.putByte(POS_NUM_ELEMENTS, (byte) 3);
+ }
+
+ final int size = mPos;
+
+ if (mUsePooledBuffer) {
+ return new StatsEvent(mAtomId, mBuffer, mBuffer.getBytes(), size);
+ } else {
+ // Create a copy of the buffer with the required number of bytes.
+ final byte[] payload = new byte[size];
+ System.arraycopy(mBuffer.getBytes(), 0, payload, 0, size);
+
+ // Return Buffer instance to the pool.
+ mBuffer.release();
+
+ return new StatsEvent(mAtomId, null, payload, size);
+ }
+ }
+
+ private void writeTypeId(final byte typeId) {
+ mPosLastField = mPos;
+ mLastType = typeId;
+ mCurrentAnnotationCount = 0;
+ final byte encodedId = (byte) (typeId & 0x0F);
+ mPos += mBuffer.putByte(mPos, encodedId);
+ }
+
+ private void writeAnnotationCount() {
+ // Use first 4 bits for annotation count and last 4 bits for typeId.
+ final byte encodedId = (byte) ((mCurrentAnnotationCount << 4) | (mLastType & 0x0F));
+ mBuffer.putByte(mPosLastField, encodedId);
+ }
+
+ @NonNull
+ private static byte[] stringToBytes(@Nullable final String value) {
+ return (null == value ? "" : value).getBytes(UTF_8);
+ }
+ }
+
+ private static final class Buffer {
+ private static Object sLock = new Object();
+
+ @GuardedBy("sLock")
+ private static Buffer sPool;
+
+ private byte[] mBytes = new byte[MAX_PUSH_PAYLOAD_SIZE];
+ private boolean mOverflow = false;
+ private int mMaxSize = MAX_PULL_PAYLOAD_SIZE;
+
+ @NonNull
+ private static Buffer obtain() {
+ final Buffer buffer;
+ synchronized (sLock) {
+ buffer = null == sPool ? new Buffer() : sPool;
+ sPool = null;
+ }
+ buffer.reset();
+ return buffer;
+ }
+
+ private Buffer() {
+ }
+
+ @NonNull
+ private byte[] getBytes() {
+ return mBytes;
+ }
+
+ private void release() {
+ // Recycle this Buffer if its size is MAX_PUSH_PAYLOAD_SIZE or under.
+ if (mBytes.length <= MAX_PUSH_PAYLOAD_SIZE) {
+ synchronized (sLock) {
+ if (null == sPool) {
+ sPool = this;
+ }
+ }
+ }
+ }
+
+ private void reset() {
+ mOverflow = false;
+ mMaxSize = MAX_PULL_PAYLOAD_SIZE;
+ }
+
+ private void setMaxSize(final int maxSize, final int numBytesWritten) {
+ mMaxSize = maxSize;
+ if (numBytesWritten > maxSize) {
+ mOverflow = true;
+ }
+ }
+
+ private boolean hasOverflowed() {
+ return mOverflow;
+ }
+
+ /**
+ * Checks for available space in the byte array.
+ *
+ * @param index starting position in the buffer to start the check.
+ * @param numBytes number of bytes to check from index.
+ * @return true if space is available, false otherwise.
+ **/
+ private boolean hasEnoughSpace(final int index, final int numBytes) {
+ final int totalBytesNeeded = index + numBytes;
+
+ if (totalBytesNeeded > mMaxSize) {
+ mOverflow = true;
+ return false;
+ }
+
+ // Expand buffer if needed.
+ if (mBytes.length < mMaxSize && totalBytesNeeded > mBytes.length) {
+ int newSize = mBytes.length;
+ do {
+ newSize *= 2;
+ } while (newSize <= totalBytesNeeded);
+
+ if (newSize > mMaxSize) {
+ newSize = mMaxSize;
+ }
+
+ mBytes = Arrays.copyOf(mBytes, newSize);
+ }
+
+ return true;
+ }
+
+ /**
+ * Writes a byte into the buffer.
+ *
+ * @param index position in the buffer where the byte is written.
+ * @param value the byte to write.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putByte(final int index, final byte value) {
+ if (hasEnoughSpace(index, Byte.BYTES)) {
+ mBytes[index] = (byte) (value);
+ return Byte.BYTES;
+ }
+ return 0;
+ }
+
+ /**
+ * Writes a boolean into the buffer.
+ *
+ * @param index position in the buffer where the boolean is written.
+ * @param value the boolean to write.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putBoolean(final int index, final boolean value) {
+ return putByte(index, (byte) (value ? 1 : 0));
+ }
+
+ /**
+ * Writes an integer into the buffer.
+ *
+ * @param index position in the buffer where the integer is written.
+ * @param value the integer to write.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putInt(final int index, final int value) {
+ if (hasEnoughSpace(index, Integer.BYTES)) {
+ // Use little endian byte order.
+ mBytes[index] = (byte) (value);
+ mBytes[index + 1] = (byte) (value >> 8);
+ mBytes[index + 2] = (byte) (value >> 16);
+ mBytes[index + 3] = (byte) (value >> 24);
+ return Integer.BYTES;
+ }
+ return 0;
+ }
+
+ /**
+ * Writes a long into the buffer.
+ *
+ * @param index position in the buffer where the long is written.
+ * @param value the long to write.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putLong(final int index, final long value) {
+ if (hasEnoughSpace(index, Long.BYTES)) {
+ // Use little endian byte order.
+ mBytes[index] = (byte) (value);
+ mBytes[index + 1] = (byte) (value >> 8);
+ mBytes[index + 2] = (byte) (value >> 16);
+ mBytes[index + 3] = (byte) (value >> 24);
+ mBytes[index + 4] = (byte) (value >> 32);
+ mBytes[index + 5] = (byte) (value >> 40);
+ mBytes[index + 6] = (byte) (value >> 48);
+ mBytes[index + 7] = (byte) (value >> 56);
+ return Long.BYTES;
+ }
+ return 0;
+ }
+
+ /**
+ * Writes a float into the buffer.
+ *
+ * @param index position in the buffer where the float is written.
+ * @param value the float to write.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putFloat(final int index, final float value) {
+ return putInt(index, Float.floatToIntBits(value));
+ }
+
+ /**
+ * Copies a byte array into the buffer.
+ *
+ * @param index position in the buffer where the byte array is copied.
+ * @param value the byte array to copy.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putByteArray(final int index, @NonNull final byte[] value) {
+ final int numBytes = value.length;
+ if (hasEnoughSpace(index, numBytes)) {
+ System.arraycopy(value, 0, mBytes, index, numBytes);
+ return numBytes;
+ }
+ return 0;
+ }
+ }
+}
diff --git a/apex/statsd/framework/java/android/util/StatsLog.java b/apex/statsd/framework/java/android/util/StatsLog.java
new file mode 100644
index 000000000000..0a9f4ebabdf0
--- /dev/null
+++ b/apex/statsd/framework/java/android/util/StatsLog.java
@@ -0,0 +1,185 @@
+/*
+ * 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.util;
+
+import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.PACKAGE_USAGE_STATS;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.os.IStatsd;
+import android.os.Process;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.statsd.StatsdStatsLog;
+
+/**
+ * StatsLog provides an API for developers to send events to statsd. The events can be used to
+ * define custom metrics inside statsd.
+ */
+public final class StatsLog {
+
+ // Load JNI library
+ static {
+ System.loadLibrary("stats_jni");
+ }
+ private static final String TAG = "StatsLog";
+ private static final boolean DEBUG = false;
+ private static final int EXPERIMENT_IDS_FIELD_ID = 1;
+
+ private StatsLog() {
+ }
+
+ /**
+ * Logs a start event.
+ *
+ * @param label developer-chosen label.
+ * @return True if the log request was sent to statsd.
+ */
+ public static boolean logStart(int label) {
+ int callingUid = Process.myUid();
+ StatsdStatsLog.write(
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED,
+ callingUid,
+ label,
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__START);
+ return true;
+ }
+
+ /**
+ * Logs a stop event.
+ *
+ * @param label developer-chosen label.
+ * @return True if the log request was sent to statsd.
+ */
+ public static boolean logStop(int label) {
+ int callingUid = Process.myUid();
+ StatsdStatsLog.write(
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED,
+ callingUid,
+ label,
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__STOP);
+ return true;
+ }
+
+ /**
+ * Logs an event that does not represent a start or stop boundary.
+ *
+ * @param label developer-chosen label.
+ * @return True if the log request was sent to statsd.
+ */
+ public static boolean logEvent(int label) {
+ int callingUid = Process.myUid();
+ StatsdStatsLog.write(
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED,
+ callingUid,
+ label,
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__UNSPECIFIED);
+ return true;
+ }
+
+ /**
+ * Logs an event for binary push for module updates.
+ *
+ * @param trainName name of install train.
+ * @param trainVersionCode version code of the train.
+ * @param options optional flags about this install.
+ * The last 3 bits indicate options:
+ * 0x01: FLAG_REQUIRE_STAGING
+ * 0x02: FLAG_ROLLBACK_ENABLED
+ * 0x04: FLAG_REQUIRE_LOW_LATENCY_MONITOR
+ * @param state current install state. Defined as State enums in
+ * BinaryPushStateChanged atom in
+ * frameworks/base/cmds/statsd/src/atoms.proto
+ * @param experimentIds experiment ids.
+ * @return True if the log request was sent to statsd.
+ */
+ @RequiresPermission(allOf = {DUMP, PACKAGE_USAGE_STATS})
+ public static boolean logBinaryPushStateChanged(@NonNull String trainName,
+ long trainVersionCode, int options, int state,
+ @NonNull long[] experimentIds) {
+ ProtoOutputStream proto = new ProtoOutputStream();
+ for (long id : experimentIds) {
+ proto.write(
+ ProtoOutputStream.FIELD_TYPE_INT64
+ | ProtoOutputStream.FIELD_COUNT_REPEATED
+ | EXPERIMENT_IDS_FIELD_ID,
+ id);
+ }
+ StatsdStatsLog.write(StatsdStatsLog.BINARY_PUSH_STATE_CHANGED,
+ trainName,
+ trainVersionCode,
+ (options & IStatsd.FLAG_REQUIRE_STAGING) > 0,
+ (options & IStatsd.FLAG_ROLLBACK_ENABLED) > 0,
+ (options & IStatsd.FLAG_REQUIRE_LOW_LATENCY_MONITOR) > 0,
+ state,
+ proto.getBytes(),
+ 0,
+ 0,
+ false);
+ return true;
+ }
+
+ /**
+ * Write an event to stats log using the raw format.
+ *
+ * @param buffer The encoded buffer of data to write.
+ * @param size The number of bytes from the buffer to write.
+ * @hide
+ */
+ // TODO(b/144935988): Mark deprecated.
+ @SystemApi
+ public static void writeRaw(@NonNull byte[] buffer, int size) {
+ // TODO(b/144935988): make this no-op once clients have migrated to StatsEvent.
+ writeImpl(buffer, size, 0);
+ }
+
+ /**
+ * Write an event to stats log using the raw format.
+ *
+ * @param buffer The encoded buffer of data to write.
+ * @param size The number of bytes from the buffer to write.
+ * @param atomId The id of the atom to which the event belongs.
+ */
+ private static native void writeImpl(@NonNull byte[] buffer, int size, int atomId);
+
+ /**
+ * Write an event to stats log using the raw format encapsulated in StatsEvent.
+ * After writing to stats log, release() is called on the StatsEvent object.
+ * No further action should be taken on the StatsEvent object following this call.
+ *
+ * @param statsEvent The StatsEvent object containing the encoded buffer of data to write.
+ * @hide
+ */
+ @SystemApi
+ public static void write(@NonNull final StatsEvent statsEvent) {
+ writeImpl(statsEvent.getBytes(), statsEvent.getNumBytes(), statsEvent.getAtomId());
+ statsEvent.release();
+ }
+
+ private static void enforceDumpCallingPermission(Context context) {
+ context.enforceCallingPermission(android.Manifest.permission.DUMP, "Need DUMP permission.");
+ }
+
+ private static void enforcesageStatsCallingPermission(Context context) {
+ context.enforceCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS,
+ "Need PACKAGE_USAGE_STATS permission.");
+ }
+}
diff --git a/apex/statsd/framework/test/Android.bp b/apex/statsd/framework/test/Android.bp
new file mode 100644
index 000000000000..b113d595b57c
--- /dev/null
+++ b/apex/statsd/framework/test/Android.bp
@@ -0,0 +1,36 @@
+// Copyright (C) 2020 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.
+
+android_test {
+ name: "FrameworkStatsdTest",
+ platform_apis: true,
+ srcs: [
+ // TODO(b/147705194): Use framework-statsd as a lib dependency instead.
+ ":framework-statsd-sources",
+ "**/*.java",
+ ],
+ manifest: "AndroidManifest.xml",
+ static_libs: [
+ "androidx.test.rules",
+ "truth-prebuilt",
+ ],
+ libs: [
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ ],
+ test_suites: [
+ "device-tests",
+ "mts",
+ ],
+} \ No newline at end of file
diff --git a/apex/statsd/framework/test/AndroidManifest.xml b/apex/statsd/framework/test/AndroidManifest.xml
new file mode 100644
index 000000000000..8f89d2332b12
--- /dev/null
+++ b/apex/statsd/framework/test/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.os.statsd.framework.test"
+ >
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.os.statsd.framework.test"
+ android:label="Framework Statsd Tests" />
+
+</manifest>
diff --git a/apex/statsd/framework/test/AndroidTest.xml b/apex/statsd/framework/test/AndroidTest.xml
new file mode 100644
index 000000000000..fb519150ecd5
--- /dev/null
+++ b/apex/statsd/framework/test/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<configuration description="Runs Tests for Statsd.">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="FrameworkStatsdTest.apk" />
+ <option name="install-arg" value="-g" />
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="mts" />
+ <option name="test-tag" value="FrameworkStatsdTest" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.os.statsd.framework.test" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.os.statsd" />
+ </object>
+</configuration> \ No newline at end of file
diff --git a/apex/statsd/framework/test/src/android/app/PullAtomMetadataTest.java b/apex/statsd/framework/test/src/android/app/PullAtomMetadataTest.java
new file mode 100644
index 000000000000..fd386bd8e32e
--- /dev/null
+++ b/apex/statsd/framework/test/src/android/app/PullAtomMetadataTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.StatsManager.PullAtomMetadata;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class PullAtomMetadataTest {
+
+ @Test
+ public void testEmpty() {
+ PullAtomMetadata metadata = new PullAtomMetadata.Builder().build();
+ assertThat(metadata.getTimeoutMillis()).isEqualTo(StatsManager.DEFAULT_TIMEOUT_MILLIS);
+ assertThat(metadata.getCoolDownMillis()).isEqualTo(StatsManager.DEFAULT_COOL_DOWN_MILLIS);
+ assertThat(metadata.getAdditiveFields()).isNull();
+ }
+
+ @Test
+ public void testSetTimeoutMillis() {
+ long timeoutMillis = 500L;
+ PullAtomMetadata metadata =
+ new PullAtomMetadata.Builder().setTimeoutMillis(timeoutMillis).build();
+ assertThat(metadata.getTimeoutMillis()).isEqualTo(timeoutMillis);
+ assertThat(metadata.getCoolDownMillis()).isEqualTo(StatsManager.DEFAULT_COOL_DOWN_MILLIS);
+ assertThat(metadata.getAdditiveFields()).isNull();
+ }
+
+ @Test
+ public void testSetCoolDownMillis() {
+ long coolDownMillis = 10_000L;
+ PullAtomMetadata metadata =
+ new PullAtomMetadata.Builder().setCoolDownMillis(coolDownMillis).build();
+ assertThat(metadata.getTimeoutMillis()).isEqualTo(StatsManager.DEFAULT_TIMEOUT_MILLIS);
+ assertThat(metadata.getCoolDownMillis()).isEqualTo(coolDownMillis);
+ assertThat(metadata.getAdditiveFields()).isNull();
+ }
+
+ @Test
+ public void testSetAdditiveFields() {
+ int[] fields = {2, 4, 6};
+ PullAtomMetadata metadata =
+ new PullAtomMetadata.Builder().setAdditiveFields(fields).build();
+ assertThat(metadata.getTimeoutMillis()).isEqualTo(StatsManager.DEFAULT_TIMEOUT_MILLIS);
+ assertThat(metadata.getCoolDownMillis()).isEqualTo(StatsManager.DEFAULT_COOL_DOWN_MILLIS);
+ assertThat(metadata.getAdditiveFields()).isEqualTo(fields);
+ }
+
+ @Test
+ public void testSetAllElements() {
+ long timeoutMillis = 300L;
+ long coolDownMillis = 9572L;
+ int[] fields = {3, 2};
+ PullAtomMetadata metadata = new PullAtomMetadata.Builder()
+ .setTimeoutMillis(timeoutMillis)
+ .setCoolDownMillis(coolDownMillis)
+ .setAdditiveFields(fields)
+ .build();
+ assertThat(metadata.getTimeoutMillis()).isEqualTo(timeoutMillis);
+ assertThat(metadata.getCoolDownMillis()).isEqualTo(coolDownMillis);
+ assertThat(metadata.getAdditiveFields()).isEqualTo(fields);
+ }
+}
diff --git a/apex/statsd/framework/test/src/android/os/StatsDimensionsValueTest.java b/apex/statsd/framework/test/src/android/os/StatsDimensionsValueTest.java
new file mode 100644
index 000000000000..db25911e6eee
--- /dev/null
+++ b/apex/statsd/framework/test/src/android/os/StatsDimensionsValueTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.os;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public final class StatsDimensionsValueTest {
+
+ @Test
+ public void testConversionFromStructuredParcel() {
+ int tupleField = 100; // atom id
+ String stringValue = "Hello";
+ int intValue = 123;
+ long longValue = 123456789L;
+ float floatValue = 1.1f;
+ boolean boolValue = true;
+
+ // Construct structured parcel
+ StatsDimensionsValueParcel sdvp = new StatsDimensionsValueParcel();
+ sdvp.field = tupleField;
+ sdvp.valueType = StatsDimensionsValue.TUPLE_VALUE_TYPE;
+ sdvp.tupleValue = new StatsDimensionsValueParcel[5];
+
+ for (int i = 0; i < 5; i++) {
+ sdvp.tupleValue[i] = new StatsDimensionsValueParcel();
+ sdvp.tupleValue[i].field = i + 1;
+ }
+
+ sdvp.tupleValue[0].valueType = StatsDimensionsValue.STRING_VALUE_TYPE;
+ sdvp.tupleValue[1].valueType = StatsDimensionsValue.INT_VALUE_TYPE;
+ sdvp.tupleValue[2].valueType = StatsDimensionsValue.LONG_VALUE_TYPE;
+ sdvp.tupleValue[3].valueType = StatsDimensionsValue.FLOAT_VALUE_TYPE;
+ sdvp.tupleValue[4].valueType = StatsDimensionsValue.BOOLEAN_VALUE_TYPE;
+
+ sdvp.tupleValue[0].stringValue = stringValue;
+ sdvp.tupleValue[1].intValue = intValue;
+ sdvp.tupleValue[2].longValue = longValue;
+ sdvp.tupleValue[3].floatValue = floatValue;
+ sdvp.tupleValue[4].boolValue = boolValue;
+
+ // Convert to StatsDimensionsValue and check result
+ StatsDimensionsValue sdv = new StatsDimensionsValue(sdvp);
+
+ assertThat(sdv.getField()).isEqualTo(tupleField);
+ assertThat(sdv.getValueType()).isEqualTo(StatsDimensionsValue.TUPLE_VALUE_TYPE);
+ List<StatsDimensionsValue> sdvChildren = sdv.getTupleValueList();
+ assertThat(sdvChildren.size()).isEqualTo(5);
+
+ for (int i = 0; i < 5; i++) {
+ assertThat(sdvChildren.get(i).getField()).isEqualTo(i + 1);
+ }
+
+ assertThat(sdvChildren.get(0).getValueType())
+ .isEqualTo(StatsDimensionsValue.STRING_VALUE_TYPE);
+ assertThat(sdvChildren.get(1).getValueType())
+ .isEqualTo(StatsDimensionsValue.INT_VALUE_TYPE);
+ assertThat(sdvChildren.get(2).getValueType())
+ .isEqualTo(StatsDimensionsValue.LONG_VALUE_TYPE);
+ assertThat(sdvChildren.get(3).getValueType())
+ .isEqualTo(StatsDimensionsValue.FLOAT_VALUE_TYPE);
+ assertThat(sdvChildren.get(4).getValueType())
+ .isEqualTo(StatsDimensionsValue.BOOLEAN_VALUE_TYPE);
+
+ assertThat(sdvChildren.get(0).getStringValue()).isEqualTo(stringValue);
+ assertThat(sdvChildren.get(1).getIntValue()).isEqualTo(intValue);
+ assertThat(sdvChildren.get(2).getLongValue()).isEqualTo(longValue);
+ assertThat(sdvChildren.get(3).getFloatValue()).isEqualTo(floatValue);
+ assertThat(sdvChildren.get(4).getBooleanValue()).isEqualTo(boolValue);
+
+ // Ensure that StatsDimensionsValue and StatsDimensionsValueParcel are
+ // parceled equivalently
+ Parcel sdvpParcel = Parcel.obtain();
+ Parcel sdvParcel = Parcel.obtain();
+ sdvp.writeToParcel(sdvpParcel, 0);
+ sdv.writeToParcel(sdvParcel, 0);
+ assertThat(sdvpParcel.dataSize()).isEqualTo(sdvParcel.dataSize());
+ }
+
+ @Test
+ public void testNullTupleArray() {
+ int tupleField = 100; // atom id
+
+ StatsDimensionsValueParcel parcel = new StatsDimensionsValueParcel();
+ parcel.field = tupleField;
+ parcel.valueType = StatsDimensionsValue.TUPLE_VALUE_TYPE;
+ parcel.tupleValue = null;
+
+ StatsDimensionsValue sdv = new StatsDimensionsValue(parcel);
+ assertThat(sdv.getField()).isEqualTo(tupleField);
+ assertThat(sdv.getValueType()).isEqualTo(StatsDimensionsValue.TUPLE_VALUE_TYPE);
+ List<StatsDimensionsValue> sdvChildren = sdv.getTupleValueList();
+ assertThat(sdvChildren.size()).isEqualTo(0);
+ }
+}
diff --git a/apex/statsd/framework/test/src/android/util/StatsEventTest.java b/apex/statsd/framework/test/src/android/util/StatsEventTest.java
new file mode 100644
index 000000000000..8d263699d9c8
--- /dev/null
+++ b/apex/statsd/framework/test/src/android/util/StatsEventTest.java
@@ -0,0 +1,818 @@
+/*
+ * 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.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.os.SystemClock;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.collect.Range;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Random;
+
+/**
+ * Internal tests for {@link StatsEvent}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class StatsEventTest {
+
+ @Test
+ public void testNoFields() {
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder().usePooledBuffer().build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ final int expectedAtomId = 0;
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("Third element is not errors type")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_ERRORS);
+
+ final int errorMask = buffer.getInt();
+
+ assertWithMessage("ERROR_NO_ATOM_ID should be the only error in the error mask")
+ .that(errorMask).isEqualTo(StatsEvent.ERROR_NO_ATOM_ID);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testOnlyAtomId() {
+ final int expectedAtomId = 109;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(2);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testIntBooleanIntInt() {
+ final int expectedAtomId = 109;
+ final int field1 = 1;
+ final boolean field2 = true;
+ final int field3 = 3;
+ final int field4 = 4;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeInt(field1)
+ .writeBoolean(field2)
+ .writeInt(field3)
+ .writeInt(field4)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(6);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("First field is not Int")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect field 1")
+ .that(buffer.getInt()).isEqualTo(field1);
+
+ assertWithMessage("Second field is not Boolean")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BOOLEAN);
+
+ assertWithMessage("Incorrect field 2")
+ .that(buffer.get()).isEqualTo(1);
+
+ assertWithMessage("Third field is not Int")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect field 3")
+ .that(buffer.getInt()).isEqualTo(field3);
+
+ assertWithMessage("Fourth field is not Int")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect field 4")
+ .that(buffer.getInt()).isEqualTo(field4);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testStringFloatByteArray() {
+ final int expectedAtomId = 109;
+ final String field1 = "Str 1";
+ final float field2 = 9.334f;
+ final byte[] field3 = new byte[] { 56, 23, 89, -120 };
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeString(field1)
+ .writeFloat(field2)
+ .writeByteArray(field3)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(5);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("First field is not String")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_STRING);
+
+ final String field1Actual = getStringFromByteBuffer(buffer);
+ assertWithMessage("Incorrect field 1")
+ .that(field1Actual).isEqualTo(field1);
+
+ assertWithMessage("Second field is not Float")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_FLOAT);
+
+ assertWithMessage("Incorrect field 2")
+ .that(buffer.getFloat()).isEqualTo(field2);
+
+ assertWithMessage("Third field is not byte array")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BYTE_ARRAY);
+
+ final byte[] field3Actual = getByteArrayFromByteBuffer(buffer);
+ assertWithMessage("Incorrect field 3")
+ .that(field3Actual).isEqualTo(field3);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testAttributionChainLong() {
+ final int expectedAtomId = 109;
+ final int[] uids = new int[] { 1, 2, 3, 4, 5 };
+ final String[] tags = new String[] { "1", "2", "3", "4", "5" };
+ final long field2 = -230909823L;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeAttributionChain(uids, tags)
+ .writeLong(field2)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(4);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("First field is not Attribution Chain")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_ATTRIBUTION_CHAIN);
+
+ assertWithMessage("Incorrect number of attribution nodes")
+ .that(buffer.get()).isEqualTo((byte) uids.length);
+
+ for (int i = 0; i < tags.length; i++) {
+ assertWithMessage("Incorrect uid in Attribution Chain")
+ .that(buffer.getInt()).isEqualTo(uids[i]);
+
+ final String tag = getStringFromByteBuffer(buffer);
+ assertWithMessage("Incorrect tag in Attribution Chain")
+ .that(tag).isEqualTo(tags[i]);
+ }
+
+ assertWithMessage("Second field is not Long")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect field 2")
+ .that(buffer.getLong()).isEqualTo(field2);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testKeyValuePairs() {
+ final int expectedAtomId = 109;
+ final SparseIntArray intMap = new SparseIntArray();
+ final SparseLongArray longMap = new SparseLongArray();
+ final SparseArray<String> stringMap = new SparseArray<>();
+ final SparseArray<Float> floatMap = new SparseArray<>();
+ intMap.put(1, -1);
+ intMap.put(2, -2);
+ stringMap.put(3, "abc");
+ stringMap.put(4, "2h");
+ floatMap.put(9, -234.344f);
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeKeyValuePairs(intMap, longMap, stringMap, floatMap)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("First field is not KeyValuePairs")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_KEY_VALUE_PAIRS);
+
+ assertWithMessage("Incorrect number of key value pairs")
+ .that(buffer.get()).isEqualTo(
+ (byte) (intMap.size() + longMap.size() + stringMap.size()
+ + floatMap.size()));
+
+ for (int i = 0; i < intMap.size(); i++) {
+ assertWithMessage("Incorrect key in intMap")
+ .that(buffer.getInt()).isEqualTo(intMap.keyAt(i));
+ assertWithMessage("The type id of the value should be TYPE_INT in intMap")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("Incorrect value in intMap")
+ .that(buffer.getInt()).isEqualTo(intMap.valueAt(i));
+ }
+
+ for (int i = 0; i < longMap.size(); i++) {
+ assertWithMessage("Incorrect key in longMap")
+ .that(buffer.getInt()).isEqualTo(longMap.keyAt(i));
+ assertWithMessage("The type id of the value should be TYPE_LONG in longMap")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+ assertWithMessage("Incorrect value in longMap")
+ .that(buffer.getLong()).isEqualTo(longMap.valueAt(i));
+ }
+
+ for (int i = 0; i < stringMap.size(); i++) {
+ assertWithMessage("Incorrect key in stringMap")
+ .that(buffer.getInt()).isEqualTo(stringMap.keyAt(i));
+ assertWithMessage("The type id of the value should be TYPE_STRING in stringMap")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_STRING);
+ final String value = getStringFromByteBuffer(buffer);
+ assertWithMessage("Incorrect value in stringMap")
+ .that(value).isEqualTo(stringMap.valueAt(i));
+ }
+
+ for (int i = 0; i < floatMap.size(); i++) {
+ assertWithMessage("Incorrect key in floatMap")
+ .that(buffer.getInt()).isEqualTo(floatMap.keyAt(i));
+ assertWithMessage("The type id of the value should be TYPE_FLOAT in floatMap")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_FLOAT);
+ assertWithMessage("Incorrect value in floatMap")
+ .that(buffer.getFloat()).isEqualTo(floatMap.valueAt(i));
+ }
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testSingleAnnotations() {
+ final int expectedAtomId = 109;
+ final int field1 = 1;
+ final byte field1AnnotationId = 45;
+ final boolean field1AnnotationValue = false;
+ final boolean field2 = true;
+ final byte field2AnnotationId = 1;
+ final int field2AnnotationValue = 23;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeInt(field1)
+ .addBooleanAnnotation(field1AnnotationId, field1AnnotationValue)
+ .writeBoolean(field2)
+ .addIntAnnotation(field2AnnotationId, field2AnnotationValue)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(4);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ final byte field1Header = buffer.get();
+ final int field1AnnotationValueCount = field1Header >> 4;
+ final byte field1Type = (byte) (field1Header & 0x0F);
+ assertWithMessage("First field is not Int")
+ .that(field1Type).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("First field annotation count is wrong")
+ .that(field1AnnotationValueCount).isEqualTo(1);
+ assertWithMessage("Incorrect field 1")
+ .that(buffer.getInt()).isEqualTo(field1);
+ assertWithMessage("First field's annotation id is wrong")
+ .that(buffer.get()).isEqualTo(field1AnnotationId);
+ assertWithMessage("First field's annotation type is wrong")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BOOLEAN);
+ assertWithMessage("First field's annotation value is wrong")
+ .that(buffer.get()).isEqualTo(field1AnnotationValue ? 1 : 0);
+
+ final byte field2Header = buffer.get();
+ final int field2AnnotationValueCount = field2Header >> 4;
+ final byte field2Type = (byte) (field2Header & 0x0F);
+ assertWithMessage("Second field is not boolean")
+ .that(field2Type).isEqualTo(StatsEvent.TYPE_BOOLEAN);
+ assertWithMessage("Second field annotation count is wrong")
+ .that(field2AnnotationValueCount).isEqualTo(1);
+ assertWithMessage("Incorrect field 2")
+ .that(buffer.get()).isEqualTo(field2 ? 1 : 0);
+ assertWithMessage("Second field's annotation id is wrong")
+ .that(buffer.get()).isEqualTo(field2AnnotationId);
+ assertWithMessage("Second field's annotation type is wrong")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("Second field's annotation value is wrong")
+ .that(buffer.getInt()).isEqualTo(field2AnnotationValue);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testAtomIdAnnotations() {
+ final int expectedAtomId = 109;
+ final byte atomAnnotationId = 84;
+ final int atomAnnotationValue = 9;
+ final int field1 = 1;
+ final byte field1AnnotationId = 45;
+ final boolean field1AnnotationValue = false;
+ final boolean field2 = true;
+ final byte field2AnnotationId = 1;
+ final int field2AnnotationValue = 23;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .addIntAnnotation(atomAnnotationId, atomAnnotationValue)
+ .writeInt(field1)
+ .addBooleanAnnotation(field1AnnotationId, field1AnnotationValue)
+ .writeBoolean(field2)
+ .addIntAnnotation(field2AnnotationId, field2AnnotationValue)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(4);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ final byte atomIdHeader = buffer.get();
+ final int atomIdAnnotationValueCount = atomIdHeader >> 4;
+ final byte atomIdValueType = (byte) (atomIdHeader & 0x0F);
+ assertWithMessage("Second element is not atom id")
+ .that(atomIdValueType).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("Atom id annotation count is wrong")
+ .that(atomIdAnnotationValueCount).isEqualTo(1);
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+ assertWithMessage("Atom id's annotation id is wrong")
+ .that(buffer.get()).isEqualTo(atomAnnotationId);
+ assertWithMessage("Atom id's annotation type is wrong")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("Atom id's annotation value is wrong")
+ .that(buffer.getInt()).isEqualTo(atomAnnotationValue);
+
+ final byte field1Header = buffer.get();
+ final int field1AnnotationValueCount = field1Header >> 4;
+ final byte field1Type = (byte) (field1Header & 0x0F);
+ assertWithMessage("First field is not Int")
+ .that(field1Type).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("First field annotation count is wrong")
+ .that(field1AnnotationValueCount).isEqualTo(1);
+ assertWithMessage("Incorrect field 1")
+ .that(buffer.getInt()).isEqualTo(field1);
+ assertWithMessage("First field's annotation id is wrong")
+ .that(buffer.get()).isEqualTo(field1AnnotationId);
+ assertWithMessage("First field's annotation type is wrong")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BOOLEAN);
+ assertWithMessage("First field's annotation value is wrong")
+ .that(buffer.get()).isEqualTo(field1AnnotationValue ? 1 : 0);
+
+ final byte field2Header = buffer.get();
+ final int field2AnnotationValueCount = field2Header >> 4;
+ final byte field2Type = (byte) (field2Header & 0x0F);
+ assertWithMessage("Second field is not boolean")
+ .that(field2Type).isEqualTo(StatsEvent.TYPE_BOOLEAN);
+ assertWithMessage("Second field annotation count is wrong")
+ .that(field2AnnotationValueCount).isEqualTo(1);
+ assertWithMessage("Incorrect field 2")
+ .that(buffer.get()).isEqualTo(field2 ? 1 : 0);
+ assertWithMessage("Second field's annotation id is wrong")
+ .that(buffer.get()).isEqualTo(field2AnnotationId);
+ assertWithMessage("Second field's annotation type is wrong")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("Second field's annotation value is wrong")
+ .that(buffer.getInt()).isEqualTo(field2AnnotationValue);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testSetAtomIdNotCalledImmediately() {
+ final int expectedAtomId = 109;
+ final int field1 = 25;
+ final boolean field2 = true;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .writeInt(field1)
+ .setAtomId(expectedAtomId)
+ .writeBoolean(field2)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("Third element is not errors type")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_ERRORS);
+
+ final int errorMask = buffer.getInt();
+
+ assertWithMessage("ERROR_ATOM_ID_INVALID_POSITION should be the only error in the mask")
+ .that(errorMask).isEqualTo(StatsEvent.ERROR_ATOM_ID_INVALID_POSITION);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testLargePulledEvent() {
+ final int expectedAtomId = 10_020;
+ byte[] field1 = new byte[10 * 1024];
+ new Random().nextBytes(field1);
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent =
+ StatsEvent.newBuilder().setAtomId(expectedAtomId).writeByteArray(field1).build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get())
+ .isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong())
+ .isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("Third element is not byte array")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_BYTE_ARRAY);
+
+ final byte[] field1Actual = getByteArrayFromByteBuffer(buffer);
+ assertWithMessage("Incorrect field 1").that(field1Actual).isEqualTo(field1);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testPulledEventOverflow() {
+ final int expectedAtomId = 10_020;
+ byte[] field1 = new byte[50 * 1024];
+ new Random().nextBytes(field1);
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent =
+ StatsEvent.newBuilder().setAtomId(expectedAtomId).writeByteArray(field1).build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get())
+ .isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong())
+ .isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("Third element is not errors type")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_ERRORS);
+
+ final int errorMask = buffer.getInt();
+
+ assertWithMessage("ERROR_OVERFLOW should be the only error in the error mask")
+ .that(errorMask)
+ .isEqualTo(StatsEvent.ERROR_OVERFLOW);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testPushedEventOverflow() {
+ final int expectedAtomId = 10_020;
+ byte[] field1 = new byte[10 * 1024];
+ new Random().nextBytes(field1);
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeByteArray(field1)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get())
+ .isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong())
+ .isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("Third element is not errors type")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_ERRORS);
+
+ final int errorMask = buffer.getInt();
+
+ assertWithMessage("ERROR_OVERFLOW should be the only error in the error mask")
+ .that(errorMask)
+ .isEqualTo(StatsEvent.ERROR_OVERFLOW);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ private static byte[] getByteArrayFromByteBuffer(final ByteBuffer buffer) {
+ final int numBytes = buffer.getInt();
+ byte[] bytes = new byte[numBytes];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ private static String getStringFromByteBuffer(final ByteBuffer buffer) {
+ final byte[] bytes = getByteArrayFromByteBuffer(buffer);
+ return new String(bytes, UTF_8);
+ }
+}
diff --git a/apex/statsd/jni/android_util_StatsLog.cpp b/apex/statsd/jni/android_util_StatsLog.cpp
new file mode 100644
index 000000000000..71ce94923c8d
--- /dev/null
+++ b/apex/statsd/jni/android_util_StatsLog.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+#define LOG_NAMESPACE "StatsLog.tag."
+#define LOG_TAG "StatsLog_println"
+
+#include <jni.h>
+#include <log/log.h>
+#include <nativehelper/scoped_local_ref.h>
+#include "stats_buffer_writer.h"
+
+namespace android {
+
+static void android_util_StatsLog_write(JNIEnv* env, jobject clazz, jbyteArray buf, jint size,
+ jint atomId) {
+ if (buf == NULL) {
+ return;
+ }
+ jint actualSize = env->GetArrayLength(buf);
+ if (actualSize < size) {
+ return;
+ }
+
+ jbyte* bufferArray = env->GetByteArrayElements(buf, NULL);
+ if (bufferArray == NULL) {
+ return;
+ }
+
+ write_buffer_to_statsd((void*) bufferArray, size, atomId);
+
+ env->ReleaseByteArrayElements(buf, bufferArray, 0);
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ { "writeImpl", "([BII)V", (void*) android_util_StatsLog_write },
+};
+
+int register_android_util_StatsLog(JNIEnv* env)
+{
+ static const char* kStatsLogClass = "android/util/StatsLog";
+
+ ScopedLocalRef<jclass> cls(env, env->FindClass(kStatsLogClass));
+ if (cls.get() == nullptr) {
+ ALOGE("jni statsd registration failure, class not found '%s'", kStatsLogClass);
+ return JNI_ERR;
+ }
+
+ const jint count = sizeof(gMethods) / sizeof(gMethods[0]);
+ int status = env->RegisterNatives(cls.get(), gMethods, count);
+ if (status < 0) {
+ ALOGE("jni statsd registration failure, status: %d", status);
+ return JNI_ERR;
+ }
+ return JNI_VERSION_1_4;
+}
+
+}; // namespace android
+
+/*
+ * JNI Initialization
+ */
+jint JNI_OnLoad(JavaVM* jvm, void* reserved) {
+ JNIEnv* e;
+
+ ALOGV("statsd : loading JNI\n");
+ // Check JNI version
+ if (jvm->GetEnv((void**)&e, JNI_VERSION_1_4)) {
+ ALOGE("JNI version mismatch error");
+ return JNI_ERR;
+ }
+
+ return android::register_android_util_StatsLog(e);
+}
diff --git a/apex/statsd/service/Android.bp b/apex/statsd/service/Android.bp
new file mode 100644
index 000000000000..df0ccfc54d64
--- /dev/null
+++ b/apex/statsd/service/Android.bp
@@ -0,0 +1,35 @@
+// Copyright (C) 2020 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: "service-statsd-sources",
+ srcs: [
+ "java/**/*.java",
+ ],
+}
+
+java_library {
+ name: "service-statsd",
+ srcs: [ ":service-statsd-sources" ],
+ sdk_version: "system_server_current",
+ libs: [
+ "framework-annotations-lib",
+ "framework-statsd",
+ ],
+ plugins: ["java_api_finder"],
+ apex_available: [
+ "com.android.os.statsd",
+ "test_com.android.os.statsd",
+ ],
+}
diff --git a/apex/statsd/service/java/com/android/server/stats/StatsCompanion.java b/apex/statsd/service/java/com/android/server/stats/StatsCompanion.java
new file mode 100644
index 000000000000..dc477a5590ea
--- /dev/null
+++ b/apex/statsd/service/java/com/android/server/stats/StatsCompanion.java
@@ -0,0 +1,188 @@
+/*
+ * 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.stats;
+
+import android.app.PendingIntent;
+import android.app.StatsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IPendingIntentRef;
+import android.os.Process;
+import android.os.StatsDimensionsValue;
+import android.os.StatsDimensionsValueParcel;
+import android.util.Log;
+
+import com.android.server.SystemService;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * @hide
+ */
+public class StatsCompanion {
+ private static final String TAG = "StatsCompanion";
+ private static final boolean DEBUG = false;
+
+ private static final int AID_STATSD = 1066;
+
+ private static final String STATS_COMPANION_SERVICE = "statscompanion";
+ private static final String STATS_MANAGER_SERVICE = "statsmanager";
+
+ static void enforceStatsdCallingUid() {
+ if (Binder.getCallingPid() == Process.myPid()) {
+ return;
+ }
+ if (Binder.getCallingUid() != AID_STATSD) {
+ throw new SecurityException("Not allowed to access StatsCompanion");
+ }
+ }
+
+ /**
+ * Lifecycle class for both {@link StatsCompanionService} and {@link StatsManagerService}.
+ */
+ public static final class Lifecycle extends SystemService {
+ private StatsCompanionService mStatsCompanionService;
+ private StatsManagerService mStatsManagerService;
+
+ public Lifecycle(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ mStatsCompanionService = new StatsCompanionService(getContext());
+ mStatsManagerService = new StatsManagerService(getContext());
+ mStatsCompanionService.setStatsManagerService(mStatsManagerService);
+ mStatsManagerService.setStatsCompanionService(mStatsCompanionService);
+
+ try {
+ publishBinderService(STATS_COMPANION_SERVICE, mStatsCompanionService);
+ if (DEBUG) Log.d(TAG, "Published " + STATS_COMPANION_SERVICE);
+ publishBinderService(STATS_MANAGER_SERVICE, mStatsManagerService);
+ if (DEBUG) Log.d(TAG, "Published " + STATS_MANAGER_SERVICE);
+ } catch (Exception e) {
+ Log.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();
+ }
+ if (phase == PHASE_BOOT_COMPLETED) {
+ mStatsCompanionService.bootCompleted();
+ }
+ }
+ }
+
+ /**
+ * Wrapper for {@link PendingIntent}. Allows Statsd to send PendingIntents.
+ */
+ public static class PendingIntentRef extends IPendingIntentRef.Stub {
+
+ private static final String TAG = "PendingIntentRef";
+
+ /**
+ * 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.
+ */
+ private static final String EXTRA_LAST_REPORT_TIME = "android.app.extra.LAST_REPORT_TIME";
+ private static final int CODE_DATA_BROADCAST = 1;
+ private static final int CODE_ACTIVE_CONFIGS_BROADCAST = 1;
+ private static final int CODE_SUBSCRIBER_BROADCAST = 1;
+
+ private final PendingIntent mPendingIntent;
+ private final Context mContext;
+
+ public PendingIntentRef(PendingIntent pendingIntent, Context context) {
+ mPendingIntent = pendingIntent;
+ mContext = context;
+ }
+
+ @Override
+ public void sendDataBroadcast(long lastReportTimeNs) {
+ enforceStatsdCallingUid();
+ Intent intent = new Intent();
+ intent.putExtra(EXTRA_LAST_REPORT_TIME, lastReportTimeNs);
+ try {
+ mPendingIntent.send(mContext, CODE_DATA_BROADCAST, intent, null, null);
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "Unable to send PendingIntent");
+ }
+ }
+
+ @Override
+ public void sendActiveConfigsChangedBroadcast(long[] configIds) {
+ enforceStatsdCallingUid();
+ Intent intent = new Intent();
+ intent.putExtra(StatsManager.EXTRA_STATS_ACTIVE_CONFIG_KEYS, configIds);
+ try {
+ mPendingIntent.send(mContext, CODE_ACTIVE_CONFIGS_BROADCAST, intent, null, null);
+ if (DEBUG) {
+ Log.d(TAG, "Sent broadcast with config ids " + Arrays.toString(configIds));
+ }
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "Unable to send active configs changed broadcast using PendingIntent");
+ }
+ }
+
+ @Override
+ public void sendSubscriberBroadcast(long configUid, long configId, long subscriptionId,
+ long subscriptionRuleId, String[] cookies,
+ StatsDimensionsValueParcel dimensionsValueParcel) {
+ enforceStatsdCallingUid();
+ StatsDimensionsValue dimensionsValue = new StatsDimensionsValue(dimensionsValueParcel);
+ Intent intent =
+ new Intent()
+ .putExtra(StatsManager.EXTRA_STATS_CONFIG_UID, configUid)
+ .putExtra(StatsManager.EXTRA_STATS_CONFIG_KEY, configId)
+ .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);
+ cookieList.addAll(Arrays.asList(cookies));
+ intent.putStringArrayListExtra(
+ StatsManager.EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES, cookieList);
+
+ if (DEBUG) {
+ Log.d(TAG,
+ String.format(
+ "Statsd sendSubscriberBroadcast with params {%d %d %d %d %s %s}",
+ configUid, configId, subscriptionId, subscriptionRuleId,
+ Arrays.toString(cookies),
+ dimensionsValue));
+ }
+ try {
+ mPendingIntent.send(mContext, CODE_SUBSCRIBER_BROADCAST, intent, null, null);
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG,
+ "Unable to send using PendingIntent from uid " + configUid
+ + "; presumably it had been cancelled.");
+ }
+ }
+ }
+}
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..cbc8ed636ff2
--- /dev/null
+++ b/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java
@@ -0,0 +1,817 @@
+/*
+ * 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.os.Process.THREAD_PRIORITY_BACKGROUND;
+
+import android.app.AlarmManager;
+import android.app.AlarmManager.OnAlarmListener;
+import android.app.StatsManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.IStatsCompanionService;
+import android.os.IStatsd;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.StatsFrameworkInitializer;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 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 {
+
+ 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 DEATH_THRESHOLD = 10;
+
+ static final class CompanionHandler extends Handler {
+ CompanionHandler(Looper looper) {
+ super(looper);
+ }
+ }
+
+ private final Context mContext;
+ private final AlarmManager mAlarmManager;
+ @GuardedBy("sStatsdLock")
+ private static IStatsd sStatsd;
+ private static final Object sStatsdLock = new Object();
+
+ private final OnAlarmListener mAnomalyAlarmListener;
+ private final OnAlarmListener mPullingAlarmListener;
+ private final OnAlarmListener mPeriodicAlarmListener;
+
+ private StatsManagerService mStatsManagerService;
+
+ @GuardedBy("sStatsdLock")
+ private final HashSet<Long> mDeathTimeMillis = new HashSet<>();
+ @GuardedBy("sStatsdLock")
+ private final HashMap<Long, String> mDeletedFiles = new HashMap<>();
+ private final CompanionHandler mHandler;
+
+ // Flag that is set when PHASE_BOOT_COMPLETED is triggered in the StatsCompanion lifecycle.
+ private AtomicBoolean mBootCompleted = new AtomicBoolean(false);
+
+ public StatsCompanionService(Context context) {
+ super();
+ mContext = context;
+ mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ if (DEBUG) Log.d(TAG, "Registered receiver for ACTION_PACKAGE_REPLACED and ADDED.");
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mHandler = new CompanionHandler(handlerThread.getLooper());
+
+ mAnomalyAlarmListener = new AnomalyAlarmListener(context);
+ mPullingAlarmListener = new PullingAlarmListener(context);
+ mPeriodicAlarmListener = new PeriodicAlarmListener(context);
+ }
+
+ 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;
+ }
+
+ /**
+ * Non-blocking call to retrieve a reference to statsd
+ *
+ * @return IStatsd object if statsd is ready, null otherwise.
+ */
+ private static IStatsd getStatsdNonblocking() {
+ synchronized (sStatsdLock) {
+ return sStatsd;
+ }
+ }
+
+ private static void informAllUids(Context context) {
+ ParcelFileDescriptor[] fds;
+ try {
+ fds = ParcelFileDescriptor.createPipe();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to create a pipe to send uid map data.", e);
+ return;
+ }
+ HandlerThread backgroundThread = new HandlerThread(
+ "statsCompanionService.bg", THREAD_PRIORITY_BACKGROUND);
+ backgroundThread.start();
+ Handler handler = new Handler(backgroundThread.getLooper());
+ handler.post(() -> {
+ UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
+ PackageManager pm = context.getPackageManager();
+ final List<UserHandle> users = um.getUserHandles(true);
+ if (DEBUG) {
+ Log.d(TAG, "Iterating over " + users.size() + " userHandles.");
+ }
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd == null) {
+ return;
+ }
+ try {
+ statsd.informAllUidData(fds[0]);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send uid map to statsd");
+ }
+ try {
+ fds[0].close();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to close the read side of the pipe.", e);
+ }
+ final ParcelFileDescriptor writeFd = fds[1];
+ 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 (UserHandle userHandle : users) {
+ List<PackageInfo> pi =
+ pm.getInstalledPackagesAsUser(PackageManager.MATCH_UNINSTALLED_PACKAGES
+ | PackageManager.MATCH_ANY_USER
+ | PackageManager.MATCH_APEX,
+ userHandle.getIdentifier());
+ 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(ProtoOutputStream.FIELD_TYPE_MESSAGE
+ | ProtoOutputStream.FIELD_COUNT_REPEATED
+ | APPLICATION_INFO_FIELD_ID);
+ output.write(ProtoOutputStream.FIELD_TYPE_INT32
+ | ProtoOutputStream.FIELD_COUNT_SINGLE | UID_FIELD_ID,
+ pi.get(j).applicationInfo.uid);
+ output.write(ProtoOutputStream.FIELD_TYPE_INT64
+ | ProtoOutputStream.FIELD_COUNT_SINGLE
+ | VERSION_FIELD_ID, pi.get(j).getLongVersionCode());
+ output.write(ProtoOutputStream.FIELD_TYPE_STRING
+ | ProtoOutputStream.FIELD_COUNT_SINGLE
+ | VERSION_STRING_FIELD_ID,
+ pi.get(j).versionName);
+ output.write(ProtoOutputStream.FIELD_TYPE_STRING
+ | ProtoOutputStream.FIELD_COUNT_SINGLE
+ | PACKAGE_NAME_FIELD_ID, pi.get(j).packageName);
+ output.write(ProtoOutputStream.FIELD_TYPE_STRING
+ | ProtoOutputStream.FIELD_COUNT_SINGLE
+ | INSTALLER_FIELD_ID,
+ installer == null ? "" : installer);
+ numRecords++;
+ output.end(applicationInfoToken);
+ }
+ }
+ }
+ output.flush();
+ if (DEBUG) {
+ Log.d(TAG, "Sent data for " + numRecords + " apps");
+ }
+ } finally {
+ FileUtils.closeQuietly(fout);
+ backgroundThread.quit();
+ backgroundThread.interrupt();
+ }
+ });
+ }
+
+ private static class WakelockThread extends Thread {
+ private final PowerManager.WakeLock mWl;
+ private final Runnable mRunnable;
+
+ WakelockThread(Context context, String wakelockName, Runnable runnable) {
+ PowerManager powerManager = (PowerManager)
+ context.getSystemService(Context.POWER_SERVICE);
+ mWl = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, wakelockName);
+ mRunnable = runnable;
+ }
+ @Override
+ public void run() {
+ try {
+ mRunnable.run();
+ } finally {
+ mWl.release();
+ }
+ }
+ @Override
+ public void start() {
+ mWl.acquire();
+ super.start();
+ }
+ }
+
+ 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) Log.d(TAG, "StatsCompanionService noticed an app was updated.");
+ synchronized (sStatsdLock) {
+ if (sStatsd == null) {
+ Log.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.
+ 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) {
+ Log.w(TAG, "Failed to inform statsd of an app update", e);
+ }
+ }
+ }
+ }
+
+ private static final class UserUpdateReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Pull the latest state of UID->app name, version mapping.
+ // Needed since the new user basically has a version of every app.
+ informAllUids(context);
+ }
+ }
+
+ public static final class AnomalyAlarmListener implements OnAlarmListener {
+ private final Context mContext;
+
+ AnomalyAlarmListener(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void onAlarm() {
+ if (DEBUG) {
+ Log.i(TAG, "StatsCompanionService believes an anomaly has occurred at time "
+ + System.currentTimeMillis() + "ms.");
+ }
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd == null) {
+ Log.w(TAG, "Could not access statsd to inform it of anomaly alarm firing");
+ return;
+ }
+
+ // Wakelock needs to be retained while calling statsd.
+ Thread thread = new WakelockThread(mContext,
+ AnomalyAlarmListener.class.getCanonicalName(), new Runnable() {
+ @Override
+ public void run() {
+ try {
+ statsd.informAnomalyAlarmFired();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to inform statsd of anomaly alarm firing", e);
+ }
+ }
+ });
+ thread.start();
+ }
+ }
+
+ public final static class PullingAlarmListener implements OnAlarmListener {
+ private final Context mContext;
+
+ PullingAlarmListener(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void onAlarm() {
+ if (DEBUG) {
+ Log.d(TAG, "Time to poll something.");
+ }
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd == null) {
+ Log.w(TAG, "Could not access statsd to inform it of pulling alarm firing.");
+ return;
+ }
+
+ // Wakelock needs to be retained while calling statsd.
+ Thread thread = new WakelockThread(mContext,
+ PullingAlarmListener.class.getCanonicalName(), new Runnable() {
+ @Override
+ public void run() {
+ try {
+ statsd.informPollAlarmFired();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to inform statsd of pulling alarm firing.", e);
+ }
+ }
+ });
+ thread.start();
+ }
+ }
+
+ public final static class PeriodicAlarmListener implements OnAlarmListener {
+ private final Context mContext;
+
+ PeriodicAlarmListener(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void onAlarm() {
+ if (DEBUG) {
+ Log.d(TAG, "Time to trigger periodic alarm.");
+ }
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd == null) {
+ Log.w(TAG, "Could not access statsd to inform it of periodic alarm firing.");
+ return;
+ }
+
+ // Wakelock needs to be retained while calling statsd.
+ Thread thread = new WakelockThread(mContext,
+ PeriodicAlarmListener.class.getCanonicalName(), new Runnable() {
+ @Override
+ public void run() {
+ try {
+ statsd.informAlarmForSubscriberTriggeringFired();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to inform statsd of periodic alarm firing.", e);
+ }
+ }
+ });
+ thread.start();
+ }
+ }
+
+ 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;
+ }
+
+ if (DEBUG) {
+ Log.i(TAG, "StatsCompanionService noticed a shutdown.");
+ }
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd == null) {
+ Log.w(TAG, "Could not access statsd to inform it of a shutdown event.");
+ return;
+ }
+ try {
+ // two way binder call
+ statsd.informDeviceShutdown();
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to inform statsd of a shutdown event.", e);
+ }
+ }
+ }
+
+ @Override // Binder call
+ public void setAnomalyAlarm(long timestampMs) {
+ StatsCompanion.enforceStatsdCallingUid();
+ if (DEBUG) Log.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() {
+ StatsCompanion.enforceStatsdCallingUid();
+ if (DEBUG) Log.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) {
+ StatsCompanion.enforceStatsdCallingUid();
+ if (DEBUG) {
+ Log.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() {
+ StatsCompanion.enforceStatsdCallingUid();
+ if (DEBUG) {
+ Log.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) {
+ StatsCompanion.enforceStatsdCallingUid();
+ if (DEBUG) {
+ Log.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() {
+ StatsCompanion.enforceStatsdCallingUid();
+ if (DEBUG) {
+ Log.d(TAG, "Cancelling pulling alarm");
+ }
+ final long callingToken = Binder.clearCallingIdentity();
+ try {
+ mAlarmManager.cancel(mPullingAlarmListener);
+ } finally {
+ Binder.restoreCallingIdentity(callingToken);
+ }
+ }
+
+ @Override // Binder call
+ public void statsdReady() {
+ StatsCompanion.enforceStatsdCallingUid();
+ if (DEBUG) {
+ Log.d(TAG, "learned that statsdReady");
+ }
+ sayHiToStatsd(); // tell statsd that we're ready too and link to it
+
+ final Intent intent = new Intent(StatsManager.ACTION_STATSD_STARTED);
+ // Retrieve list of broadcast receivers for this broadcast & send them directed broadcasts
+ // to wake them up (if they're in background).
+ List<ResolveInfo> resolveInfos =
+ mContext.getPackageManager().queryBroadcastReceiversAsUser(
+ intent, 0, UserHandle.SYSTEM);
+ if (resolveInfos == null || resolveInfos.isEmpty()) {
+ return; // No need to send broadcast.
+ }
+
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ Intent intentToSend = new Intent(intent);
+ intentToSend.setComponent(new ComponentName(
+ resolveInfo.activityInfo.applicationInfo.packageName,
+ resolveInfo.activityInfo.name));
+ mContext.sendBroadcastAsUser(intentToSend, UserHandle.SYSTEM,
+ android.Manifest.permission.DUMP);
+ }
+ }
+
+ @Override // Binder call
+ public boolean checkPermission(String permission, int pid, int uid) {
+ StatsCompanion.enforceStatsdCallingUid();
+ return mContext.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ // Statsd related code
+
+ /**
+ * Fetches the statsd IBinder service. This is a blocking call that always refetches statsd
+ * instead of returning the cached sStatsd.
+ * Note: This should only be called from {@link #sayHiToStatsd()}. All other clients should use
+ * the cached sStatsd via {@link #getStatsdNonblocking()}.
+ */
+ private IStatsd fetchStatsdServiceLocked() {
+ sStatsd = IStatsd.Stub.asInterface(StatsFrameworkInitializer
+ .getStatsServiceManager()
+ .getStatsdServiceRegisterer()
+ .get());
+ return sStatsd;
+ }
+
+ private void registerStatsdDeathRecipient(IStatsd statsd, List<BroadcastReceiver> receivers) {
+ StatsdDeathRecipient deathRecipient = new StatsdDeathRecipient(statsd, receivers);
+
+ try {
+ statsd.asBinder().linkToDeath(deathRecipient, /*flags=*/0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "linkToDeath (StatsdDeathRecipient) failed");
+ // Statsd has already died. Unregister receivers ourselves.
+ for (BroadcastReceiver receiver : receivers) {
+ mContext.unregisterReceiver(receiver);
+ }
+ synchronized (sStatsdLock) {
+ if (statsd == sStatsd) {
+ statsdNotReadyLocked();
+ }
+ }
+ }
+ }
+
+ /**
+ * Now that the android system is ready, StatsCompanion is ready too, so inform statsd.
+ */
+ void systemReady() {
+ if (DEBUG) Log.d(TAG, "Learned that systemReady");
+ sayHiToStatsd();
+ }
+
+ void setStatsManagerService(StatsManagerService statsManagerService) {
+ mStatsManagerService = statsManagerService;
+ }
+
+ /**
+ * Tells statsd that statscompanion is ready. If the binder call returns, link to
+ * statsd.
+ */
+ private void sayHiToStatsd() {
+ IStatsd statsd;
+ synchronized (sStatsdLock) {
+ if (sStatsd != null && sStatsd.asBinder().isBinderAlive()) {
+ Log.e(TAG, "statsd has already been fetched before",
+ new IllegalStateException("IStatsd object should be null or dead"));
+ return;
+ }
+ statsd = fetchStatsdServiceLocked();
+ }
+
+ if (statsd == null) {
+ Log.i(TAG, "Could not yet find statsd to tell it that StatsCompanion is alive.");
+ return;
+ }
+
+ // Cleann up from previous statsd - cancel any alarms that had been set. Do this here
+ // instead of in binder death because statsd can come back and set different alarms, or not
+ // want to set an alarm when it had been set. This guarantees that when we get a new statsd,
+ // we cancel any alarms before it is able to set them.
+ cancelAnomalyAlarm();
+ cancelPullingAlarm();
+ cancelAlarmForSubscriberTriggering();
+
+ if (DEBUG) Log.d(TAG, "Saying hi to statsd");
+ mStatsManagerService.statsdReady(statsd);
+ try {
+ statsd.statsCompanionReady();
+
+ BroadcastReceiver appUpdateReceiver = new AppUpdateReceiver();
+ BroadcastReceiver userUpdateReceiver = new UserUpdateReceiver();
+ BroadcastReceiver shutdownEventReceiver = new ShutdownEventReceiver();
+
+ // 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.registerReceiverForAllUsers(appUpdateReceiver, 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.registerReceiverForAllUsers(userUpdateReceiver, filter, null, null);
+
+ // Setup receiver for device reboots or shutdowns.
+ filter = new IntentFilter(Intent.ACTION_REBOOT);
+ filter.addAction(Intent.ACTION_SHUTDOWN);
+ mContext.registerReceiverForAllUsers(shutdownEventReceiver, filter, null, null);
+
+ // Register death recipient.
+ List<BroadcastReceiver> broadcastReceivers =
+ List.of(appUpdateReceiver, userUpdateReceiver, shutdownEventReceiver);
+ registerStatsdDeathRecipient(statsd, broadcastReceivers);
+
+ // Tell statsd that boot has completed. The signal may have already been sent, but since
+ // the signal-receiving function is idempotent, that's ok.
+ if (mBootCompleted.get()) {
+ statsd.bootCompleted();
+ }
+
+ // Pull the latest state of UID->app name, version mapping when statsd starts.
+ informAllUids(mContext);
+
+ Log.i(TAG, "Told statsd that StatsCompanionService is alive.");
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to inform statsd that statscompanion is ready", e);
+ }
+ }
+
+ private class StatsdDeathRecipient implements IBinder.DeathRecipient {
+
+ private final IStatsd mStatsd;
+ private final List<BroadcastReceiver> mReceiversToUnregister;
+
+ StatsdDeathRecipient(IStatsd statsd, List<BroadcastReceiver> receivers) {
+ mStatsd = statsd;
+ mReceiversToUnregister = receivers;
+ }
+
+ // It is possible for binderDied to be called after a restarted statsd calls statsdReady,
+ // but that's alright because the code does not assume an ordering of the two calls.
+ @Override
+ public void binderDied() {
+ Log.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 = new File(CONFIG_DIR).listFiles();
+ if (configs != null && configs.length > 0) {
+ String fileName = configs[0].getName();
+ if (configs[0].delete()) {
+ mDeletedFiles.put(now, fileName);
+ }
+ }
+ }
+
+ // Unregister receivers on death because receivers can only be unregistered once.
+ // Otherwise, an IllegalArgumentException is thrown.
+ for (BroadcastReceiver receiver: mReceiversToUnregister) {
+ mContext.unregisterReceiver(receiver);
+ }
+
+ // It's possible for statsd to have restarted and called statsdReady, causing a new
+ // sStatsd binder object to be fetched, before the binderDied callback runs. Only
+ // call #statsdNotReadyLocked if that hasn't happened yet.
+ if (mStatsd == sStatsd) {
+ statsdNotReadyLocked();
+ }
+ }
+ }
+ }
+
+ private void statsdNotReadyLocked() {
+ sStatsd = null;
+ mStatsManagerService.statsdNotReady();
+ }
+
+ void bootCompleted() {
+ mBootCompleted.set(true);
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd == null) {
+ // Statsd is not yet ready.
+ // Delay the boot completed ping to {@link #sayHiToStatsd()}
+ return;
+ }
+ try {
+ statsd.bootCompleted();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to notify statsd that boot completed");
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+ 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));
+ }
+ }
+ }
+}
diff --git a/apex/statsd/service/java/com/android/server/stats/StatsManagerService.java b/apex/statsd/service/java/com/android/server/stats/StatsManagerService.java
new file mode 100644
index 000000000000..97846f2397a5
--- /dev/null
+++ b/apex/statsd/service/java/com/android/server/stats/StatsManagerService.java
@@ -0,0 +1,661 @@
+/*
+ * 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.stats;
+
+import static com.android.server.stats.StatsCompanion.PendingIntentRef;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IPullAtomCallback;
+import android.os.IStatsManagerService;
+import android.os.IStatsd;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Service for {@link android.app.StatsManager}.
+ *
+ * @hide
+ */
+public class StatsManagerService extends IStatsManagerService.Stub {
+
+ private static final String TAG = "StatsManagerService";
+ private static final boolean DEBUG = false;
+
+ private static final int STATSD_TIMEOUT_MILLIS = 5000;
+
+ private static final String USAGE_STATS_PERMISSION_OPS = "android:get_usage_stats";
+
+ @GuardedBy("mLock")
+ private IStatsd mStatsd;
+ private final Object mLock = new Object();
+
+ private StatsCompanionService mStatsCompanionService;
+ private Context mContext;
+
+ @GuardedBy("mLock")
+ private ArrayMap<ConfigKey, PendingIntentRef> mDataFetchPirMap = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private ArrayMap<Integer, PendingIntentRef> mActiveConfigsPirMap = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private ArrayMap<ConfigKey, ArrayMap<Long, PendingIntentRef>> mBroadcastSubscriberPirMap =
+ new ArrayMap<>();
+
+ public StatsManagerService(Context context) {
+ super();
+ mContext = context;
+ }
+
+ private static class ConfigKey {
+ private final int mUid;
+ private final long mConfigId;
+
+ ConfigKey(int uid, long configId) {
+ mUid = uid;
+ mConfigId = configId;
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ public long getConfigId() {
+ return mConfigId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mUid, mConfigId);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ConfigKey) {
+ ConfigKey other = (ConfigKey) obj;
+ return this.mUid == other.getUid() && this.mConfigId == other.getConfigId();
+ }
+ return false;
+ }
+ }
+
+ private static 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 class PullerValue {
+ private final long mCoolDownMillis;
+ private final long mTimeoutMillis;
+ private final int[] mAdditiveFields;
+ private final IPullAtomCallback mCallback;
+
+ PullerValue(long coolDownMillis, long timeoutMillis, int[] additiveFields,
+ IPullAtomCallback callback) {
+ mCoolDownMillis = coolDownMillis;
+ mTimeoutMillis = timeoutMillis;
+ mAdditiveFields = additiveFields;
+ mCallback = callback;
+ }
+
+ public long getCoolDownMillis() {
+ return mCoolDownMillis;
+ }
+
+ public long getTimeoutMillis() {
+ return mTimeoutMillis;
+ }
+
+ public int[] getAdditiveFields() {
+ return mAdditiveFields;
+ }
+
+ public IPullAtomCallback getCallback() {
+ return mCallback;
+ }
+ }
+
+ private final ArrayMap<PullerKey, PullerValue> mPullers = new ArrayMap<>();
+
+ @Override
+ public void registerPullAtomCallback(int atomTag, long coolDownMillis, long timeoutMillis,
+ int[] additiveFields, IPullAtomCallback pullerCallback) {
+ enforceRegisterStatsPullAtomPermission();
+ if (pullerCallback == null) {
+ Log.w(TAG, "Puller callback is null for atom " + atomTag);
+ return;
+ }
+ int callingUid = Binder.getCallingUid();
+ PullerKey key = new PullerKey(callingUid, atomTag);
+ PullerValue val =
+ new PullerValue(coolDownMillis, timeoutMillis, additiveFields, pullerCallback);
+
+ // Always cache the puller in StatsManagerService. If statsd is down, we will register the
+ // puller when statsd comes back up.
+ synchronized (mLock) {
+ mPullers.put(key, val);
+ }
+
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd == null) {
+ return;
+ }
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ statsd.registerPullAtomCallback(callingUid, atomTag, coolDownMillis, timeoutMillis,
+ additiveFields, pullerCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to access statsd to register puller for atom " + atomTag);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void unregisterPullAtomCallback(int atomTag) {
+ enforceRegisterStatsPullAtomPermission();
+ int callingUid = Binder.getCallingUid();
+ PullerKey key = new PullerKey(callingUid, atomTag);
+
+ // Always remove the puller from StatsManagerService even if statsd is down. When statsd
+ // comes back up, we will not re-register the removed puller.
+ synchronized (mLock) {
+ mPullers.remove(key);
+ }
+
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd == null) {
+ return;
+ }
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ statsd.unregisterPullAtomCallback(callingUid, atomTag);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to access statsd to unregister puller for atom " + atomTag);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void setDataFetchOperation(long configId, PendingIntent pendingIntent,
+ String packageName) {
+ enforceDumpAndUsageStatsPermission(packageName);
+ int callingUid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ PendingIntentRef pir = new PendingIntentRef(pendingIntent, mContext);
+ ConfigKey key = new ConfigKey(callingUid, configId);
+ // We add the PIR to a map so we can reregister if statsd is unavailable.
+ synchronized (mLock) {
+ mDataFetchPirMap.put(key, pir);
+ }
+ try {
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd != null) {
+ statsd.setDataFetchOperation(configId, pir, callingUid);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to setDataFetchOperation with statsd");
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void removeDataFetchOperation(long configId, String packageName) {
+ enforceDumpAndUsageStatsPermission(packageName);
+ int callingUid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ ConfigKey key = new ConfigKey(callingUid, configId);
+ synchronized (mLock) {
+ mDataFetchPirMap.remove(key);
+ }
+ try {
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd != null) {
+ statsd.removeDataFetchOperation(configId, callingUid);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to removeDataFetchOperation with statsd");
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public long[] setActiveConfigsChangedOperation(PendingIntent pendingIntent,
+ String packageName) {
+ enforceDumpAndUsageStatsPermission(packageName);
+ int callingUid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ PendingIntentRef pir = new PendingIntentRef(pendingIntent, mContext);
+ // We add the PIR to a map so we can reregister if statsd is unavailable.
+ synchronized (mLock) {
+ mActiveConfigsPirMap.put(callingUid, pir);
+ }
+ try {
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd != null) {
+ return statsd.setActiveConfigsChangedOperation(pir, callingUid);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to setActiveConfigsChangedOperation with statsd");
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ return new long[] {};
+ }
+
+ @Override
+ public void removeActiveConfigsChangedOperation(String packageName) {
+ enforceDumpAndUsageStatsPermission(packageName);
+ int callingUid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ synchronized (mLock) {
+ mActiveConfigsPirMap.remove(callingUid);
+ }
+ try {
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd != null) {
+ statsd.removeActiveConfigsChangedOperation(callingUid);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to removeActiveConfigsChangedOperation with statsd");
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void setBroadcastSubscriber(long configId, long subscriberId,
+ PendingIntent pendingIntent, String packageName) {
+ enforceDumpAndUsageStatsPermission(packageName);
+ int callingUid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ PendingIntentRef pir = new PendingIntentRef(pendingIntent, mContext);
+ ConfigKey key = new ConfigKey(callingUid, configId);
+ // We add the PIR to a map so we can reregister if statsd is unavailable.
+ synchronized (mLock) {
+ ArrayMap<Long, PendingIntentRef> innerMap = mBroadcastSubscriberPirMap
+ .getOrDefault(key, new ArrayMap<>());
+ innerMap.put(subscriberId, pir);
+ mBroadcastSubscriberPirMap.put(key, innerMap);
+ }
+ try {
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd != null) {
+ statsd.setBroadcastSubscriber(
+ configId, subscriberId, pir, callingUid);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to setBroadcastSubscriber with statsd");
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void unsetBroadcastSubscriber(long configId, long subscriberId, String packageName) {
+ enforceDumpAndUsageStatsPermission(packageName);
+ int callingUid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ ConfigKey key = new ConfigKey(callingUid, configId);
+ synchronized (mLock) {
+ ArrayMap<Long, PendingIntentRef> innerMap = mBroadcastSubscriberPirMap
+ .getOrDefault(key, new ArrayMap<>());
+ innerMap.remove(subscriberId);
+ if (innerMap.isEmpty()) {
+ mBroadcastSubscriberPirMap.remove(key);
+ }
+ }
+ try {
+ IStatsd statsd = getStatsdNonblocking();
+ if (statsd != null) {
+ statsd.unsetBroadcastSubscriber(configId, subscriberId, callingUid);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to unsetBroadcastSubscriber with statsd");
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public long[] getRegisteredExperimentIds() throws IllegalStateException {
+ enforceDumpAndUsageStatsPermission(null);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ IStatsd statsd = waitForStatsd();
+ if (statsd != null) {
+ return statsd.getRegisteredExperimentIds();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to getRegisteredExperimentIds with statsd");
+ throw new IllegalStateException(e.getMessage(), e);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ throw new IllegalStateException("Failed to connect to statsd to registerExperimentIds");
+ }
+
+ @Override
+ public byte[] getMetadata(String packageName) throws IllegalStateException {
+ enforceDumpAndUsageStatsPermission(packageName);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ IStatsd statsd = waitForStatsd();
+ if (statsd != null) {
+ return statsd.getMetadata();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to getMetadata with statsd");
+ throw new IllegalStateException(e.getMessage(), e);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ throw new IllegalStateException("Failed to connect to statsd to getMetadata");
+ }
+
+ @Override
+ public byte[] getData(long key, String packageName) throws IllegalStateException {
+ enforceDumpAndUsageStatsPermission(packageName);
+ int callingUid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ IStatsd statsd = waitForStatsd();
+ if (statsd != null) {
+ return statsd.getData(key, callingUid);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to getData with statsd");
+ throw new IllegalStateException(e.getMessage(), e);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ throw new IllegalStateException("Failed to connect to statsd to getData");
+ }
+
+ @Override
+ public void addConfiguration(long configId, byte[] config, String packageName)
+ throws IllegalStateException {
+ enforceDumpAndUsageStatsPermission(packageName);
+ int callingUid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ IStatsd statsd = waitForStatsd();
+ if (statsd != null) {
+ statsd.addConfiguration(configId, config, callingUid);
+ return;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to addConfiguration with statsd");
+ throw new IllegalStateException(e.getMessage(), e);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ throw new IllegalStateException("Failed to connect to statsd to addConfig");
+ }
+
+ @Override
+ public void removeConfiguration(long configId, String packageName)
+ throws IllegalStateException {
+ enforceDumpAndUsageStatsPermission(packageName);
+ int callingUid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ IStatsd statsd = waitForStatsd();
+ if (statsd != null) {
+ statsd.removeConfiguration(configId, callingUid);
+ return;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to removeConfiguration with statsd");
+ throw new IllegalStateException(e.getMessage(), e);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ throw new IllegalStateException("Failed to connect to statsd to removeConfig");
+ }
+
+ void setStatsCompanionService(StatsCompanionService statsCompanionService) {
+ mStatsCompanionService = statsCompanionService;
+ }
+
+ /**
+ * Checks that the caller has both DUMP and PACKAGE_USAGE_STATS permissions. Also checks that
+ * the caller has USAGE_STATS_PERMISSION_OPS for the specified packageName if it is not null.
+ *
+ * @param packageName The packageName to check USAGE_STATS_PERMISSION_OPS.
+ */
+ private void enforceDumpAndUsageStatsPermission(@Nullable String packageName) {
+ int callingUid = Binder.getCallingUid();
+ int callingPid = Binder.getCallingPid();
+
+ if (callingPid == Process.myPid()) {
+ return;
+ }
+
+ mContext.enforceCallingPermission(Manifest.permission.DUMP, null);
+ mContext.enforceCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS, null);
+
+ if (packageName == null) {
+ return;
+ }
+ AppOpsManager appOpsManager = (AppOpsManager) mContext
+ .getSystemService(Context.APP_OPS_SERVICE);
+ switch (appOpsManager.noteOp(USAGE_STATS_PERMISSION_OPS,
+ Binder.getCallingUid(), packageName, null, null)) {
+ case AppOpsManager.MODE_ALLOWED:
+ case AppOpsManager.MODE_DEFAULT:
+ break;
+ default:
+ throw new SecurityException(
+ String.format("UID %d / PID %d lacks app-op %s",
+ callingUid, callingPid, USAGE_STATS_PERMISSION_OPS)
+ );
+ }
+ }
+
+ private void enforceRegisterStatsPullAtomPermission() {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.REGISTER_STATS_PULL_ATOM,
+ "Need REGISTER_STATS_PULL_ATOM permission.");
+ }
+
+
+ /**
+ * Clients should call this if blocking until statsd to be ready is desired
+ *
+ * @return IStatsd object if statsd becomes ready within the timeout, null otherwise.
+ */
+ private IStatsd waitForStatsd() {
+ synchronized (mLock) {
+ if (mStatsd == null) {
+ try {
+ mLock.wait(STATSD_TIMEOUT_MILLIS);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "wait for statsd interrupted");
+ }
+ }
+ return mStatsd;
+ }
+ }
+
+ /**
+ * Clients should call this to receive a reference to statsd.
+ *
+ * @return IStatsd object if statsd is ready, null otherwise.
+ */
+ private IStatsd getStatsdNonblocking() {
+ synchronized (mLock) {
+ return mStatsd;
+ }
+ }
+
+ /**
+ * Called from {@link StatsCompanionService}.
+ *
+ * Tells StatsManagerService that Statsd is ready and updates
+ * Statsd with the contents of our local cache.
+ */
+ void statsdReady(IStatsd statsd) {
+ synchronized (mLock) {
+ mStatsd = statsd;
+ mLock.notify();
+ }
+ sayHiToStatsd(statsd);
+ }
+
+ /**
+ * Called from {@link StatsCompanionService}.
+ *
+ * Tells StatsManagerService that Statsd is no longer ready
+ * and we should no longer make binder calls with statsd.
+ */
+ void statsdNotReady() {
+ synchronized (mLock) {
+ mStatsd = null;
+ }
+ }
+
+ private void sayHiToStatsd(IStatsd statsd) {
+ if (statsd == null) {
+ return;
+ }
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ registerAllPullers(statsd);
+ registerAllDataFetchOperations(statsd);
+ registerAllActiveConfigsChangedOperations(statsd);
+ registerAllBroadcastSubscribers(statsd);
+ } catch (RemoteException e) {
+ Log.e(TAG, "StatsManager failed to (re-)register data with statsd");
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ // Pre-condition: the Binder calling identity has already been cleared
+ private void registerAllPullers(IStatsd statsd) throws RemoteException {
+ // Since we do not want to make an IPC with the lock held, we first create a copy of the
+ // data with the lock held before iterating through the map.
+ ArrayMap<PullerKey, PullerValue> pullersCopy;
+ synchronized (mLock) {
+ pullersCopy = new ArrayMap<>(mPullers);
+ }
+
+ for (Map.Entry<PullerKey, PullerValue> entry : pullersCopy.entrySet()) {
+ PullerKey key = entry.getKey();
+ PullerValue value = entry.getValue();
+ statsd.registerPullAtomCallback(key.getUid(), key.getAtom(), value.getCoolDownMillis(),
+ value.getTimeoutMillis(), value.getAdditiveFields(), value.getCallback());
+ }
+ statsd.allPullersFromBootRegistered();
+ }
+
+ // Pre-condition: the Binder calling identity has already been cleared
+ private void registerAllDataFetchOperations(IStatsd statsd) throws RemoteException {
+ // Since we do not want to make an IPC with the lock held, we first create a copy of the
+ // data with the lock held before iterating through the map.
+ ArrayMap<ConfigKey, PendingIntentRef> dataFetchCopy;
+ synchronized (mLock) {
+ dataFetchCopy = new ArrayMap<>(mDataFetchPirMap);
+ }
+
+ for (Map.Entry<ConfigKey, PendingIntentRef> entry : dataFetchCopy.entrySet()) {
+ ConfigKey key = entry.getKey();
+ statsd.setDataFetchOperation(key.getConfigId(), entry.getValue(), key.getUid());
+ }
+ }
+
+ // Pre-condition: the Binder calling identity has already been cleared
+ private void registerAllActiveConfigsChangedOperations(IStatsd statsd) throws RemoteException {
+ // Since we do not want to make an IPC with the lock held, we first create a copy of the
+ // data with the lock held before iterating through the map.
+ ArrayMap<Integer, PendingIntentRef> activeConfigsChangedCopy;
+ synchronized (mLock) {
+ activeConfigsChangedCopy = new ArrayMap<>(mActiveConfigsPirMap);
+ }
+
+ for (Map.Entry<Integer, PendingIntentRef> entry : activeConfigsChangedCopy.entrySet()) {
+ statsd.setActiveConfigsChangedOperation(entry.getValue(), entry.getKey());
+ }
+ }
+
+ // Pre-condition: the Binder calling identity has already been cleared
+ private void registerAllBroadcastSubscribers(IStatsd statsd) throws RemoteException {
+ // Since we do not want to make an IPC with the lock held, we first create a deep copy of
+ // the data with the lock held before iterating through the map.
+ ArrayMap<ConfigKey, ArrayMap<Long, PendingIntentRef>> broadcastSubscriberCopy =
+ new ArrayMap<>();
+ synchronized (mLock) {
+ for (Map.Entry<ConfigKey, ArrayMap<Long, PendingIntentRef>> entry :
+ mBroadcastSubscriberPirMap.entrySet()) {
+ broadcastSubscriberCopy.put(entry.getKey(), new ArrayMap(entry.getValue()));
+ }
+ }
+
+ for (Map.Entry<ConfigKey, ArrayMap<Long, PendingIntentRef>> entry :
+ mBroadcastSubscriberPirMap.entrySet()) {
+ ConfigKey configKey = entry.getKey();
+ for (Map.Entry<Long, PendingIntentRef> subscriberEntry : entry.getValue().entrySet()) {
+ statsd.setBroadcastSubscriber(configKey.getConfigId(), subscriberEntry.getKey(),
+ subscriberEntry.getValue(), configKey.getUid());
+ }
+ }
+ }
+}
diff --git a/apex/statsd/statsd.rc b/apex/statsd/statsd.rc
new file mode 100644
index 000000000000..605da2af0c19
--- /dev/null
+++ b/apex/statsd/statsd.rc
@@ -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.
+
+service statsd /apex/com.android.os.statsd/bin/statsd
+ class main
+ socket statsdw dgram+passcred 0222 statsd statsd
+ user statsd
+ group statsd log
+ writepid /dev/cpuset/system-background/tasks
diff --git a/apex/statsd/testing/Android.bp b/apex/statsd/testing/Android.bp
new file mode 100644
index 000000000000..a9cd0ccb53e8
--- /dev/null
+++ b/apex/statsd/testing/Android.bp
@@ -0,0 +1,25 @@
+// 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_test {
+ name: "test_com.android.os.statsd",
+ visibility: [
+ "//system/apex/tests",
+ ],
+ defaults: ["com.android.os.statsd-defaults"],
+ manifest: "test_manifest.json",
+ file_contexts: ":com.android.os.statsd-file_contexts",
+ // Test APEX, should never be installed
+ installable: false,
+}
diff --git a/apex/statsd/testing/test_manifest.json b/apex/statsd/testing/test_manifest.json
new file mode 100644
index 000000000000..57343d3e6ae5
--- /dev/null
+++ b/apex/statsd/testing/test_manifest.json
@@ -0,0 +1,4 @@
+{
+ "name": "com.android.os.statsd",
+ "version": 2147483647
+}
diff --git a/apex/statsd/tests/libstatspull/Android.bp b/apex/statsd/tests/libstatspull/Android.bp
new file mode 100644
index 000000000000..05b3e049ac39
--- /dev/null
+++ b/apex/statsd/tests/libstatspull/Android.bp
@@ -0,0 +1,61 @@
+// 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.
+
+android_test {
+ name: "LibStatsPullTests",
+ static_libs: [
+ "androidx.test.rules",
+ "platformprotoslite",
+ "statsdprotolite",
+ "truth-prebuilt",
+ ],
+ libs: [
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ ],
+ jni_libs: [
+ "libstatspull_testhelper",
+ ],
+ srcs: [
+ "src/**/*.java",
+ "protos/**/*.proto",
+ ],
+ test_suites: [
+ "device-tests",
+ "mts",
+ ],
+ platform_apis: true,
+ privileged: true,
+ certificate: "platform",
+ compile_multilib: "both",
+}
+
+cc_library_shared {
+ name: "libstatspull_testhelper",
+ srcs: ["jni/stats_pull_helper.cpp"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ shared_libs: [
+ "libbinder_ndk",
+ "statsd-aidl-ndk_platform",
+ ],
+ static_libs: [
+ "libstatspull_private",
+ "libstatssocket_private",
+ "libutils",
+ "libcutils",
+ ],
+}
diff --git a/apex/statsd/tests/libstatspull/AndroidManifest.xml b/apex/statsd/tests/libstatspull/AndroidManifest.xml
new file mode 100644
index 000000000000..0c669b051c86
--- /dev/null
+++ b/apex/statsd/tests/libstatspull/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.internal.os.statsd.libstats" >
+
+
+ <uses-permission android:name="android.permission.DUMP" />
+ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+ <uses-permission android:name="android.permission.REGISTER_STATS_PULL_ATOM" />
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.internal.os.statsd.libstats"
+ android:label="Tests for libstatspull">
+ </instrumentation>
+</manifest>
+
diff --git a/apex/statsd/tests/libstatspull/jni/stats_pull_helper.cpp b/apex/statsd/tests/libstatspull/jni/stats_pull_helper.cpp
new file mode 100644
index 000000000000..166592d35151
--- /dev/null
+++ b/apex/statsd/tests/libstatspull/jni/stats_pull_helper.cpp
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+#include <jni.h>
+#include <log/log.h>
+#include <stats_event.h>
+#include <stats_pull_atom_callback.h>
+
+#include <chrono>
+#include <thread>
+
+using std::this_thread::sleep_for;
+
+namespace {
+static int32_t sAtomTag;
+static int32_t sPullReturnVal;
+static int64_t sLatencyMillis;
+static int32_t sAtomsPerPull;
+static int32_t sNumPulls = 0;
+
+static AStatsManager_PullAtomCallbackReturn pullAtomCallback(int32_t atomTag, AStatsEventList* data,
+ void* /*cookie*/) {
+ sNumPulls++;
+ sleep_for(std::chrono::milliseconds(sLatencyMillis));
+ for (int i = 0; i < sAtomsPerPull; i++) {
+ AStatsEvent* event = AStatsEventList_addStatsEvent(data);
+ AStatsEvent_setAtomId(event, atomTag);
+ AStatsEvent_writeInt64(event, (int64_t) sNumPulls);
+ AStatsEvent_build(event);
+ }
+ return sPullReturnVal;
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_android_internal_os_statsd_libstats_LibStatsPullTests_setStatsPuller(
+ JNIEnv* /*env*/, jobject /* this */, jint atomTag, jlong timeoutMillis,
+ jlong coolDownMillis, jint pullRetVal, jlong latencyMillis, int atomsPerPull) {
+ sAtomTag = atomTag;
+ sPullReturnVal = pullRetVal;
+ sLatencyMillis = latencyMillis;
+ sAtomsPerPull = atomsPerPull;
+ sNumPulls = 0;
+ AStatsManager_PullAtomMetadata* metadata = AStatsManager_PullAtomMetadata_obtain();
+ AStatsManager_PullAtomMetadata_setCoolDownMillis(metadata, coolDownMillis);
+ AStatsManager_PullAtomMetadata_setTimeoutMillis(metadata, timeoutMillis);
+
+ AStatsManager_setPullAtomCallback(sAtomTag, metadata, &pullAtomCallback, nullptr);
+ AStatsManager_PullAtomMetadata_release(metadata);
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_android_internal_os_statsd_libstats_LibStatsPullTests_clearStatsPuller(JNIEnv* /*env*/,
+ jobject /* this */,
+ jint /*atomTag*/) {
+ AStatsManager_clearPullAtomCallback(sAtomTag);
+}
+} // namespace
diff --git a/apex/statsd/tests/libstatspull/protos/test_atoms.proto b/apex/statsd/tests/libstatspull/protos/test_atoms.proto
new file mode 100644
index 000000000000..56c1b534a7ce
--- /dev/null
+++ b/apex/statsd/tests/libstatspull/protos/test_atoms.proto
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+syntax = "proto2";
+
+package com.android.internal.os.statsd.protos;
+
+option java_package = "com.android.internal.os.statsd.protos";
+option java_outer_classname = "TestAtoms";
+
+message PullCallbackAtomWrapper {
+ optional PullCallbackAtom pull_callback_atom = 150030;
+}
+
+message PullCallbackAtom {
+ optional int64 long_val = 1;
+}
+
+
+
diff --git a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java
new file mode 100644
index 000000000000..6108a324e15e
--- /dev/null
+++ b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java
@@ -0,0 +1,287 @@
+/*
+ * 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.internal.os.statsd.libstats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.StatsManager;
+import android.content.Context;
+import android.util.Log;
+import android.util.StatsLog;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.StatsdConfigProto.AtomMatcher;
+import com.android.internal.os.StatsdConfigProto.FieldFilter;
+import com.android.internal.os.StatsdConfigProto.GaugeMetric;
+import com.android.internal.os.StatsdConfigProto.PullAtomPackages;
+import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.internal.os.StatsdConfigProto.TimeUnit;
+import com.android.internal.os.statsd.protos.TestAtoms;
+import com.android.os.AtomsProto.Atom;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/**
+ * Test puller registration.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class LibStatsPullTests {
+ private static final String LOG_TAG = LibStatsPullTests.class.getSimpleName();
+ private static final int SHORT_SLEEP_MILLIS = 250;
+ private static final int LONG_SLEEP_MILLIS = 1_000;
+ private Context mContext;
+ private static final int PULL_ATOM_TAG = 150030;
+ private static final int APP_BREADCRUMB_LABEL = 3;
+ private static int sPullReturnValue;
+ private static long sConfigId;
+ private static long sPullLatencyMillis;
+ private static long sPullTimeoutMillis;
+ private static long sCoolDownMillis;
+ private static int sAtomsPerPull;
+
+ static {
+ System.loadLibrary("statspull_testhelper");
+ }
+
+ /**
+ * Setup the tests. Initialize shared data.
+ */
+ @Before
+ public void setup() {
+ mContext = InstrumentationRegistry.getTargetContext();
+ assertThat(InstrumentationRegistry.getInstrumentation()).isNotNull();
+ sPullReturnValue = StatsManager.PULL_SUCCESS;
+ sPullLatencyMillis = 0;
+ sPullTimeoutMillis = 10_000L;
+ sCoolDownMillis = 1_000L;
+ sAtomsPerPull = 1;
+ }
+
+ /**
+ * Teardown the tests.
+ */
+ @After
+ public void tearDown() throws Exception {
+ clearStatsPuller(PULL_ATOM_TAG);
+ StatsManager statsManager = (StatsManager) mContext.getSystemService(
+ Context.STATS_MANAGER);
+ statsManager.removeConfig(sConfigId);
+ }
+
+ /**
+ * Tests adding a puller callback and that pulls complete successfully.
+ */
+ @Test
+ public void testPullAtomCallbackRegistration() throws Exception {
+ StatsManager statsManager = (StatsManager) mContext.getSystemService(
+ Context.STATS_MANAGER);
+ // Upload a config that captures that pulled atom.
+ createAndAddConfigToStatsd(statsManager);
+
+ // Add the puller.
+ setStatsPuller(PULL_ATOM_TAG, sPullTimeoutMillis, sCoolDownMillis, sPullReturnValue,
+ sPullLatencyMillis, sAtomsPerPull);
+ Thread.sleep(SHORT_SLEEP_MILLIS);
+ StatsLog.logStart(APP_BREADCRUMB_LABEL);
+ // Let the current bucket finish.
+ Thread.sleep(LONG_SLEEP_MILLIS);
+ List<Atom> data = StatsConfigUtils.getGaugeMetricDataList(statsManager, sConfigId);
+ clearStatsPuller(PULL_ATOM_TAG);
+ assertThat(data.size()).isEqualTo(1);
+ TestAtoms.PullCallbackAtomWrapper atomWrapper = null;
+ try {
+ atomWrapper = TestAtoms.PullCallbackAtomWrapper.parser()
+ .parseFrom(data.get(0).toByteArray());
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Failed to parse primitive atoms");
+ }
+ assertThat(atomWrapper).isNotNull();
+ assertThat(atomWrapper.hasPullCallbackAtom()).isTrue();
+ TestAtoms.PullCallbackAtom atom =
+ atomWrapper.getPullCallbackAtom();
+ assertThat(atom.getLongVal()).isEqualTo(1);
+ }
+
+ /**
+ * Tests that a failed pull is skipped.
+ */
+ @Test
+ public void testPullAtomCallbackFailure() throws Exception {
+ StatsManager statsManager = (StatsManager) mContext.getSystemService(
+ Context.STATS_MANAGER);
+ createAndAddConfigToStatsd(statsManager);
+ sPullReturnValue = StatsManager.PULL_SKIP;
+ // Add the puller.
+ setStatsPuller(PULL_ATOM_TAG, sPullTimeoutMillis, sCoolDownMillis, sPullReturnValue,
+ sPullLatencyMillis, sAtomsPerPull);
+ Thread.sleep(SHORT_SLEEP_MILLIS);
+ StatsLog.logStart(APP_BREADCRUMB_LABEL);
+ // Let the current bucket finish.
+ Thread.sleep(LONG_SLEEP_MILLIS);
+ List<Atom> data = StatsConfigUtils.getGaugeMetricDataList(statsManager, sConfigId);
+ clearStatsPuller(PULL_ATOM_TAG);
+ assertThat(data.size()).isEqualTo(0);
+ }
+
+ /**
+ * Tests that a pull that times out is skipped.
+ */
+ @Test
+ public void testPullAtomCallbackTimeout() throws Exception {
+ StatsManager statsManager = (StatsManager) mContext.getSystemService(
+ Context.STATS_MANAGER);
+ createAndAddConfigToStatsd(statsManager);
+ // The puller will sleep for 1.5 sec.
+ sPullLatencyMillis = 1_500;
+ // 1 second timeout
+ sPullTimeoutMillis = 1_000;
+
+ // Add the puller.
+ setStatsPuller(PULL_ATOM_TAG, sPullTimeoutMillis, sCoolDownMillis, sPullReturnValue,
+ sPullLatencyMillis, sAtomsPerPull);
+ Thread.sleep(SHORT_SLEEP_MILLIS);
+ StatsLog.logStart(APP_BREADCRUMB_LABEL);
+ // Let the current bucket finish and the pull timeout.
+ Thread.sleep(sPullLatencyMillis * 2);
+ List<Atom> data = StatsConfigUtils.getGaugeMetricDataList(statsManager, sConfigId);
+ clearStatsPuller(PULL_ATOM_TAG);
+ assertThat(data.size()).isEqualTo(0);
+ }
+
+ /**
+ * Tests that 2 pulls in quick succession use the cache instead of pulling again.
+ */
+ @Test
+ public void testPullAtomCallbackCache() throws Exception {
+ StatsManager statsManager = (StatsManager) mContext.getSystemService(
+ Context.STATS_MANAGER);
+ createAndAddConfigToStatsd(statsManager);
+
+ // Set the cooldown to 10 seconds
+ sCoolDownMillis = 10_000L;
+ // Add the puller.
+ setStatsPuller(PULL_ATOM_TAG, sPullTimeoutMillis, sCoolDownMillis, sPullReturnValue,
+ sPullLatencyMillis, sAtomsPerPull);
+
+ Thread.sleep(SHORT_SLEEP_MILLIS);
+ StatsLog.logStart(APP_BREADCRUMB_LABEL);
+ // Pull from cache.
+ StatsLog.logStart(APP_BREADCRUMB_LABEL);
+ Thread.sleep(LONG_SLEEP_MILLIS);
+ List<Atom> data = StatsConfigUtils.getGaugeMetricDataList(statsManager, sConfigId);
+ clearStatsPuller(PULL_ATOM_TAG);
+ assertThat(data.size()).isEqualTo(2);
+ for (int i = 0; i < data.size(); i++) {
+ TestAtoms.PullCallbackAtomWrapper atomWrapper = null;
+ try {
+ atomWrapper = TestAtoms.PullCallbackAtomWrapper.parser()
+ .parseFrom(data.get(i).toByteArray());
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Failed to parse primitive atoms");
+ }
+ assertThat(atomWrapper).isNotNull();
+ assertThat(atomWrapper.hasPullCallbackAtom()).isTrue();
+ TestAtoms.PullCallbackAtom atom =
+ atomWrapper.getPullCallbackAtom();
+ assertThat(atom.getLongVal()).isEqualTo(1);
+ }
+ }
+
+ /**
+ * Tests that a pull that returns 1000 stats events works properly.
+ */
+ @Test
+ public void testPullAtomCallbackStress() throws Exception {
+ StatsManager statsManager = (StatsManager) mContext.getSystemService(
+ Context.STATS_MANAGER);
+ // Upload a config that captures that pulled atom.
+ createAndAddConfigToStatsd(statsManager);
+ sAtomsPerPull = 1000;
+ // Add the puller.
+ setStatsPuller(PULL_ATOM_TAG, sPullTimeoutMillis, sCoolDownMillis, sPullReturnValue,
+ sPullLatencyMillis, sAtomsPerPull);
+
+ Thread.sleep(SHORT_SLEEP_MILLIS);
+ StatsLog.logStart(APP_BREADCRUMB_LABEL);
+ // Let the current bucket finish.
+ Thread.sleep(LONG_SLEEP_MILLIS);
+ List<Atom> data = StatsConfigUtils.getGaugeMetricDataList(statsManager, sConfigId);
+ clearStatsPuller(PULL_ATOM_TAG);
+ assertThat(data.size()).isEqualTo(sAtomsPerPull);
+
+ for (int i = 0; i < data.size(); i++) {
+ TestAtoms.PullCallbackAtomWrapper atomWrapper = null;
+ try {
+ atomWrapper = TestAtoms.PullCallbackAtomWrapper.parser()
+ .parseFrom(data.get(i).toByteArray());
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Failed to parse primitive atoms");
+ }
+ assertThat(atomWrapper).isNotNull();
+ assertThat(atomWrapper.hasPullCallbackAtom()).isTrue();
+ TestAtoms.PullCallbackAtom atom =
+ atomWrapper.getPullCallbackAtom();
+ assertThat(atom.getLongVal()).isEqualTo(1);
+ }
+ }
+
+ private void createAndAddConfigToStatsd(StatsManager statsManager) throws Exception {
+ sConfigId = System.currentTimeMillis();
+ long triggerMatcherId = sConfigId + 10;
+ long pullerMatcherId = sConfigId + 11;
+ long metricId = sConfigId + 100;
+ StatsdConfig config = StatsConfigUtils.getSimpleTestConfig(sConfigId)
+ .addAtomMatcher(
+ StatsConfigUtils.getAppBreadcrumbMatcher(triggerMatcherId,
+ APP_BREADCRUMB_LABEL))
+ .addAtomMatcher(AtomMatcher.newBuilder()
+ .setId(pullerMatcherId)
+ .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder()
+ .setAtomId(PULL_ATOM_TAG))
+ )
+ .addGaugeMetric(GaugeMetric.newBuilder()
+ .setId(metricId)
+ .setWhat(pullerMatcherId)
+ .setTriggerEvent(triggerMatcherId)
+ .setGaugeFieldsFilter(FieldFilter.newBuilder().setIncludeAll(true))
+ .setBucket(TimeUnit.CTS)
+ .setSamplingType(GaugeMetric.SamplingType.FIRST_N_SAMPLES)
+ .setMaxNumGaugeAtomsPerBucket(1000)
+ )
+ .addPullAtomPackages(PullAtomPackages.newBuilder()
+ .setAtomId(PULL_ATOM_TAG)
+ .addPackages(LibStatsPullTests.class.getPackage().getName()))
+ .build();
+ statsManager.addConfig(sConfigId, config.toByteArray());
+ assertThat(StatsConfigUtils.verifyValidConfigExists(statsManager, sConfigId)).isTrue();
+ }
+
+ private native void setStatsPuller(int atomTag, long timeoutMillis, long coolDownMillis,
+ int pullReturnVal, long latencyMillis, int atomPerPull);
+
+ private native void clearStatsPuller(int atomTag);
+}
+
diff --git a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java
new file mode 100644
index 000000000000..b5afb94886de
--- /dev/null
+++ b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java
@@ -0,0 +1,124 @@
+/*
+ * 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.internal.os.statsd.libstats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.StatsManager;
+import android.util.Log;
+
+import com.android.internal.os.StatsdConfigProto.AtomMatcher;
+import com.android.internal.os.StatsdConfigProto.FieldValueMatcher;
+import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.os.AtomsProto.AppBreadcrumbReported;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.ConfigMetricsReport;
+import com.android.os.StatsLog.ConfigMetricsReportList;
+import com.android.os.StatsLog.GaugeBucketInfo;
+import com.android.os.StatsLog.GaugeMetricData;
+import com.android.os.StatsLog.StatsLogReport;
+import com.android.os.StatsLog.StatsdStatsReport;
+import com.android.os.StatsLog.StatsdStatsReport.ConfigStats;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Util class for constructing statsd configs.
+ */
+public class StatsConfigUtils {
+ public static final String TAG = "statsd.StatsConfigUtils";
+ public static final int SHORT_WAIT = 2_000; // 2 seconds.
+
+ /**
+ * @return An empty StatsdConfig in serialized proto format.
+ */
+ public static StatsdConfig.Builder getSimpleTestConfig(long configId) {
+ return StatsdConfig.newBuilder().setId(configId)
+ .addAllowedLogSource(StatsConfigUtils.class.getPackage().getName());
+ }
+
+
+ public static boolean verifyValidConfigExists(StatsManager statsManager, long configId) {
+ StatsdStatsReport report = null;
+ try {
+ report = StatsdStatsReport.parser().parseFrom(statsManager.getStatsMetadata());
+ } catch (Exception e) {
+ Log.e(TAG, "getMetadata failed", e);
+ }
+ assertThat(report).isNotNull();
+ boolean foundConfig = false;
+ for (ConfigStats configStats : report.getConfigStatsList()) {
+ if (configStats.getId() == configId && configStats.getIsValid()
+ && configStats.getDeletionTimeSec() == 0) {
+ foundConfig = true;
+ }
+ }
+ return foundConfig;
+ }
+
+ public static AtomMatcher getAppBreadcrumbMatcher(long id, int label) {
+ return AtomMatcher.newBuilder()
+ .setId(id)
+ .setSimpleAtomMatcher(
+ SimpleAtomMatcher.newBuilder()
+ .setAtomId(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER)
+ .addFieldValueMatcher(FieldValueMatcher.newBuilder()
+ .setField(AppBreadcrumbReported.LABEL_FIELD_NUMBER)
+ .setEqInt(label)
+ )
+ )
+ .build();
+ }
+
+ public static ConfigMetricsReport getConfigMetricsReport(StatsManager statsManager,
+ long configId) {
+ ConfigMetricsReportList reportList = null;
+ try {
+ reportList = ConfigMetricsReportList.parser()
+ .parseFrom(statsManager.getReports(configId));
+ } catch (Exception e) {
+ Log.e(TAG, "getData failed", e);
+ }
+ assertThat(reportList).isNotNull();
+ assertThat(reportList.getReportsCount()).isEqualTo(1);
+ ConfigMetricsReport report = reportList.getReports(0);
+ assertThat(report.getDumpReportReason())
+ .isEqualTo(ConfigMetricsReport.DumpReportReason.GET_DATA_CALLED);
+ return report;
+
+ }
+ public static List<Atom> getGaugeMetricDataList(ConfigMetricsReport report) {
+ List<Atom> data = new ArrayList<>();
+ for (StatsLogReport metric : report.getMetricsList()) {
+ for (GaugeMetricData gaugeMetricData : metric.getGaugeMetrics().getDataList()) {
+ for (GaugeBucketInfo bucketInfo : gaugeMetricData.getBucketInfoList()) {
+ for (Atom atom : bucketInfo.getAtomList()) {
+ data.add(atom);
+ }
+ }
+ }
+ }
+ return data;
+ }
+
+ public static List<Atom> getGaugeMetricDataList(StatsManager statsManager, long configId) {
+ ConfigMetricsReport report = getConfigMetricsReport(statsManager, configId);
+ return getGaugeMetricDataList(report);
+ }
+}
+