summaryrefslogtreecommitdiff
path: root/apex
diff options
context:
space:
mode:
Diffstat (limited to 'apex')
-rw-r--r--apex/appsearch/Android.bp37
-rw-r--r--apex/appsearch/apex_manifest.json4
-rw-r--r--apex/appsearch/com.android.appsearch.avbpubkeybin0 -> 1032 bytes
-rw-r--r--apex/appsearch/com.android.appsearch.pem51
-rw-r--r--apex/appsearch/com.android.appsearch.pk8bin0 -> 2373 bytes
-rw-r--r--apex/appsearch/com.android.appsearch.x509.pem35
-rw-r--r--apex/appsearch/framework/Android.bp80
-rw-r--r--apex/appsearch/framework/jarjar-rules.txt2
-rw-r--r--apex/appsearch/framework/java/android/app/TEST_MAPPING7
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/AppSearchBatchResult.java163
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java694
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java256
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java405
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java43
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/AppSearchResult.java215
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java359
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl112
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/IllegalSchemaException.java36
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/IllegalSearchSpecException.java36
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/MatchInfo.java182
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/SearchResults.java128
-rw-r--r--apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java260
-rw-r--r--apex/appsearch/service/Android.bp26
-rw-r--r--apex/appsearch/service/jarjar-rules.txt2
-rw-r--r--apex/appsearch/service/java/com/android/server/appsearch/AppSearchException.java57
-rw-r--r--apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java287
-rw-r--r--apex/appsearch/service/java/com/android/server/appsearch/TEST_MAPPING23
-rw-r--r--apex/appsearch/service/java/com/android/server/appsearch/impl/AppSearchImpl.java332
-rw-r--r--apex/appsearch/service/java/com/android/server/appsearch/impl/FakeIcing.java216
-rw-r--r--apex/appsearch/service/java/com/android/server/appsearch/impl/ImplInstanceManager.java56
-rw-r--r--apex/jobscheduler/framework/Android.bp5
-rw-r--r--apex/jobscheduler/framework/java/android/app/AlarmManager.aidl19
-rw-r--r--apex/jobscheduler/framework/java/android/app/AlarmManager.java1152
-rw-r--r--apex/jobscheduler/framework/java/android/app/IAlarmCompleteListener.aidl27
-rw-r--r--apex/jobscheduler/framework/java/android/app/IAlarmListener.aidl29
-rw-r--r--apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl44
-rw-r--r--apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java23
-rw-r--r--apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java3
-rw-r--r--apex/jobscheduler/service/Android.bp6
-rw-r--r--apex/jobscheduler/service/jarjar-rules.txt18
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java4888
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/TEST_MAPPING24
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java26
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobStore.java9
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java48
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java4
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java103
-rw-r--r--apex/media/OWNERS3
60 files changed, 10461 insertions, 98 deletions
diff --git a/apex/appsearch/Android.bp b/apex/appsearch/Android.bp
new file mode 100644
index 000000000000..b014fdcb3df3
--- /dev/null
+++ b/apex/appsearch/Android.bp
@@ -0,0 +1,37 @@
+// 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.appsearch",
+ manifest: "apex_manifest.json",
+ java_libs: [
+ "framework-appsearch",
+ "service-appsearch",
+ ],
+ key: "com.android.appsearch.key",
+ certificate: ":com.android.appsearch.certificate",
+}
+
+apex_key {
+ name: "com.android.appsearch.key",
+ public_key: "com.android.appsearch.avbpubkey",
+ private_key: "com.android.appsearch.pem",
+}
+
+android_app_certificate {
+ name: "com.android.appsearch.certificate",
+ // This will use com.android.appsearch.x509.pem (the cert) and
+ // com.android.appsearch.pk8 (the private key)
+ certificate: "com.android.appsearch",
+}
diff --git a/apex/appsearch/apex_manifest.json b/apex/appsearch/apex_manifest.json
new file mode 100644
index 000000000000..39a2d38fa642
--- /dev/null
+++ b/apex/appsearch/apex_manifest.json
@@ -0,0 +1,4 @@
+{
+ "name": "com.android.appsearch",
+ "version": 300000000
+}
diff --git a/apex/appsearch/com.android.appsearch.avbpubkey b/apex/appsearch/com.android.appsearch.avbpubkey
new file mode 100644
index 000000000000..4e5acae9c1e4
--- /dev/null
+++ b/apex/appsearch/com.android.appsearch.avbpubkey
Binary files differ
diff --git a/apex/appsearch/com.android.appsearch.pem b/apex/appsearch/com.android.appsearch.pem
new file mode 100644
index 000000000000..4ed5945acc86
--- /dev/null
+++ b/apex/appsearch/com.android.appsearch.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKQIBAAKCAgEAro9f/jvoIsj6ywuRmuUQS8UtprhohJitrovDMfm/T2R/WQvy
+AvUxgetyF4XvBPCDRqCsGxXCJMQOn1furrAeTmWbGHPhA0PI1Ys/qtfNMbh9THyn
+70I2c4X70CUQ+8/Y8BJ8CAB4iER/s9QtD28QLvM2BBUzRoKUSBGUYNMlYobjgRdK
+57V7yg48LkvUIg1fzIW3M5gCgOXa0u1xOadKX3m7tzCboHcXp5anfWX5PH1+okRu
+jzdI8OjtUq23qhoRw5Skz0Vbf4a+8t3kT3slF/Q7O8LoRPwpZsvIcvTyCGAqlra7
+2L2LN4H1p+u2ko3r/QmRbJn2eXW07elkyrggXMyn2rTxibQgk53wYfSavMyNd/E7
++de/uJ60l2aPa+5KUaR8eYwchXEELdqQ+zRgSZ2711xCaY4glEj7DT6VlEEdr26x
+akX0ra7e2sVGv1um/dvSyVO5aFKKjVvo4LqhWKWO8yvDMxmDDTNatvWhY2Bhd3RA
+0hilYpWQFb9Tv5f4E0tZmfvlddgux7sw++Y/RIimBFoSyf5AezAUIFYYoYvEzytB
+muq1/ecNHr+Z2tZMxN88sJVhzRzD9tKUyXhvxOV2Lg9TIeVTWGwQqgSnHWtIe+1p
+cw8inPfYEhP4Q+3W/RlPvNdu75x8Nj2aG7bxZnhoQDRDw5ddgma27I+a8esCAwEA
+AQKCAgBsNh9I6HRAVBz8kCBkSEnw3rwtFTZdtJQ+lw+bRHpvShqT5g7R/JQDOSTS
+JkoE4uBOgT4P0E45Inz6FLW2/yDacqxR3UwJDRVMI/WFACCJCRhLuR8V+BLvTIjN
+AJ1lrPSL5rmS8E/IEcakgQyp+6ypnkXHBCl0NXCcuKEl4N7VFE+mb/0UZPHnUSnH
+fWR085uGmwH17u7mXxdnGKDPH8DALSPMLUrcj9dPIdqUpwl5kUZWa1uqVphWF98/
+GMe5oE2Q0+3TO+i7xplKz3lAOFPHZLTvmCUK1tMHkZ6ifOwpewwLwB30/5N1BpB1
+126nrWk0xKCtFUixBOHzdnLwJHKSbi7chQU5q39oAJoTfxdmAJlaG0zQHUQZ44MQ
+gemzSA7uJbtoAOAZVF1K14xbIpnfidqTB7N3RCmiJE+/Hpkq6PxgPfu5rqocPbPC
+t0FgJ4NXNmKOAuJllSlrrHATcUOhF4g5pX7tvOc8X4y7bvfwOmtw5ez3INKMF0q6
+/y0vVCi6N1Z7CTa9eY8feZ1PImk/Fkq4NInSPyx7ZE3pLYmsvuJjliFrWo9TRVae
+Dt5vvBKBOpAfhDiHkeXbX7Raj2B6c6adF4no/3SAVlAjIq1iBVjfQWyHAGUoEW1O
+u3LdHTIb6gSTLJ4AfryEKrOE+1VMlYt92GwX692KKXMaJjytSQKCAQEA3pYbl8HD
+Y++UyEN5VzWAQedT3//GDwpDfgdERe2E4smYrkVNJ2WAG2SqY1A35DIl8be3eHvl
+soaL38j48ailfDYY9tI+IlapNh+VOLej+HiOytaPlLhcv2FpSC2qZT4EiU6IBXLo
++l6FrmD/VQXTjvoktzsDB/n1t4Dfa3Ogf+lLf1Jxr94YpEnDh18V5ofj78SplVLm
+NrzsHxAafE4Ni2a7dyWjcDYIuL7FTShT+0K4W45tRr+CGxThxu7LEe7zw4Z1IagU
+jJNtXjvDD/Zw4UTqI6RwWGZsu6UjPS6LHhOqnWqflWmFRIfMbDkuWvnGZTM9DkVg
+kk1+BNi1PECZXwKCAQEAyMOjbVo6XV3lFN0X8TpHyg/z9ar00/SE7WEJHqPSuzYT
+rSfU4vDDlaPAwkYvGi9ZKi9VM+R3CyBNxnK9Yq6NurHhhrYcAwdS/hGLT1K2o0Y8
+Pgv7gZCFb+SIwLBhlUG9otGULcBzLneqgVUqyMG6IoCjuC2LRyB71Xc2UMyg6n/f
+XpV2RTMb8f+26cgm6nj0SDAfgpr8HV6uNV80c6l1A8gq86nUWwiVAEUdmExSDe7J
+shsfWAj8RSErqDXf1BtEdPLJUSIPX5VXkzAXOXIkengwVno0vv0dBN8uraS8iQSG
+0JsJLLcw9b5kvnh6FEbE7POsIqKyCZV9VADwO6YW9QKCAQBYQsdwNqoGv6KMgozj
+8tgHyfWtVduwbQ50M+dznwpZbzz2pY5Bd/MDabhSpyVyfBwlrAa5ZM+hKc7fDu7/
+zDLKfR0LCjUPIrP4PS/LjK4dQZjFf6zxeOV2EedQcqMlgCEGXTh8iKMvXDm/+sBk
+c2n/QNs8OM8r44b2m8h78B6NefGw6/0ekn/M7V72F9M0VWAh3Cauim+09tbePmFy
+NvUR+MuPJEKZpSNyNltADCS49izqSSC1tAygNniMjHXDh6/rMS7TCLYVRARTIHlp
+o/wAp3X8aiEOPJcTFRlTElihtYSq5POgqHXqxbpek5H5CyALUvT76rCvcsDspQ3A
+dZEbAoIBAQCoLEmP5o8Rev/UdEgECB/uwWJIngYsLp3TAv/SrMRvkiL1X3JTD/+m
+L9/eXVBDjPoR/khPCcg2h77ex2qhaTrL8wnKAG6CkvYQYb3impTnPIRmLT9nDxrX
+2gY78wQrNUCXTRvlH1rcx90KLb+DH9S95ig+tdf/otRYwl27XU5GYQtJfcXuvZth
+IiWku8btjpiCh909WHpsV81yY+faI08j9d8U8WQzRYMbEMpzsyrhBO/rxBCDfDNl
+7R1W8JooYRb9KAs/bVqXZNBROW2a72RjOp6zMfdRLVHLrPC7AE32MNaFk/khfesD
+T5OwgdcxeP6oxo2hDcw5fwHXBlo2fTCpAoIBAQChgjv5AfQ50spqvHy6MNem4tV0
+L0IsxmNLsi8X2a6s4kStwUzOxDA8c/e54XabxQNZ0ERU1q+bgbG7PWC4twDMPR8i
+2DO6rgqSK4MjGOTgAoeDuy3mElFQmCLRs04Wf4jh8kPi217WFlYBynh2HmBKbh42
+JmIrLetbKEK13FXRvMkgZcX4OIDrT5TOvev4VZArU8PTRlWv3sqsKAVXjX0clGHf
+I0/2kSsr2qq1UY7JrYWZsZ9uqz2ZH0pF19a6O/Cq4uqTYoL+sYzFTSeFmChRjV1g
+ancTvTn9lcBqECDMgq5DE/p96Oxg/t8elalR6WDUlysafphVz3nTuyMTh7ka
+-----END RSA PRIVATE KEY-----
diff --git a/apex/appsearch/com.android.appsearch.pk8 b/apex/appsearch/com.android.appsearch.pk8
new file mode 100644
index 000000000000..77e98b20877b
--- /dev/null
+++ b/apex/appsearch/com.android.appsearch.pk8
Binary files differ
diff --git a/apex/appsearch/com.android.appsearch.x509.pem b/apex/appsearch/com.android.appsearch.x509.pem
new file mode 100644
index 000000000000..e37c4b9fcead
--- /dev/null
+++ b/apex/appsearch/com.android.appsearch.x509.pem
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIGETCCA/mgAwIBAgIUGl+MIvpEi0+TWzj0ieh6WtjXmP8wDQYJKoZIhvcNAQEL
+BQAwgZYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy
+b2lkMRIwEAYDVQQDDAlhcHBzZWFyY2gxIjAgBgkqhkiG9w0BCQEWE2FuZHJvaWRA
+YW5kcm9pZC5jb20wIBcNMTkxMTIwMjMxNTM1WhgPNDc1NzEwMTYyMzE1MzVaMIGW
+MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
+bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDES
+MBAGA1UEAwwJYXBwc2VhcmNoMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJv
+aWQuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsyPlp3q3P9Xg
+W1WhIwQiF9em9oqaGQ/3dbIxickAy591qcRbpHb4lDTZusRECfqlV215mV+lv5x4
+EhOnId3uPKBAJ/YDtL7zUW6TWL7to7zEnUqSIKTcoQzNF2EiCeGuRhrtgYvAD3HQ
+dwr4xrbSADbDArF04A49voLpsmq1fyNgl86VISiMRqoSLJnA6eghlduuOt+nf252
+6WgxDs/JrO/eK70q0+RwmWzVJ/tVr+36a65N4EHhfL4t2hdV0k0XFob7hBn7XWzC
+QrSR3jCvE3yAfAr3tq5c19/WWBA7V45nEHzXyAvBUHWubYvDi+vm/yzqU2rQwScC
+bzp4zK4CnhBHqb4gHoy0+kfFIwJ1A3GT2pl3ba/NsIYgliMtPQfkDV5PE5RTNcwH
+21ewH7vm2+spQv5Z/2TEV2lEHlp2vuAliyn2AT4u1ginr6vtBRFLmpPeziFcfB0y
+7h04GctZpX8odz+XI7aMDe47RNu9XyJX0vulntxmlDF76k8Z9DIXg02hY+yc/i7+
+2ztnj1eXL51p+HyhK5VbvJWbKkVaMQijlbuIMYNzMA6L0WHWRc2Cux9UDODMGoiC
+w09JpqudCS/95I/F1xaWJ/Kh3vKeQshHAz0hrL7v7wpjmfeXf6NGsWJGy+giCwZj
+ABtn9nFQoesgi7M1LeazD5Q/4v4AMaUCAwEAAaNTMFEwHQYDVR0OBBYEFJpHCy2Y
+3qaL6cLpE9fe53L61KEEMB8GA1UdIwQYMBaAFJpHCy2Y3qaL6cLpE9fe53L61KEE
+MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAGDYAcOhbOYcDB2K
+WDZka+FCORFFvz4nLQGE7Z9TAn1g7XusM2KbXlb2drIN6CWOFlnKQrUsNsAHrc+s
+tl+A1vC3/NfYKKBVuizPx/kHUgz3k/UIJzbzEu/uCJd86idcJoUTqC/qEJAeeQqM
+XpsNP1Yg7oyzZT8sFlUAKeDeXJ7fIDXR6nduUQ6uJXkee/5JF3VedHdgHAUsC19/
+KHhyVU3MLDUNBdAmM79+DsdVYi2Pw31jojMu95Zz1MYTRBcgQAiEw5nncr38k6ac
+Gy+JffgJR68FzI4QLBSxnDRFD2zXJ09lpP6Sjb1FVcDzk7Bi/EQDLBkrkbeLsk5F
+a0xz9VoJ3kM7Cc4R9MXN4ZWuePjdJwgasnHmllsXn45R9odgJgmfzuUwtgNw/XKQ
+QcQl7Q9QUrBCqIoHijxscUZCBSmIHVNBBDckRAmSXHeWMRlO3uBR4IA/Jfrt//4f
+uc7CNUp+LQ6EzBXJOVFrXRtau6Oj+jM1+fzxKo1uV2+T+GdVEE5jeF/6nB3qna6h
+2NmyLqbqeqp2QxgzBWSGy8Ugs6zg4wItJBqOoRLKKFxTJu5OAzJ4fUA+g7WFXNhR
+kG56SJ863LZoORKHWE72oXYeIW98Tq0qKLH3NzH5L4tfX8DeBTq+APezHetH1ljA
+D0avPy62g0i643bbpwZgezBgRIKL
+-----END CERTIFICATE-----
diff --git a/apex/appsearch/framework/Android.bp b/apex/appsearch/framework/Android.bp
new file mode 100644
index 000000000000..321f471c3c8b
--- /dev/null
+++ b/apex/appsearch/framework/Android.bp
@@ -0,0 +1,80 @@
+// 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-appsearch-sources",
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.aidl",
+ ],
+ path: "java",
+}
+
+java_library {
+ name: "framework-appsearch",
+ installable: true,
+ sdk_version: "core_platform", // TODO(b/146218515) should be module_current
+ srcs: [":framework-appsearch-sources"],
+ hostdex: true, // for hiddenapi check
+ libs: [
+ "framework-minus-apex", // TODO(b/146218515) should be removed
+ ],
+ static_libs: ["icing-java-proto-lite"],
+ visibility: [
+ // TODO(b/146218515) remove this when framework is built with the stub of appsearch
+ "//frameworks/base",
+ "//frameworks/base/apex/appsearch:__subpackages__",
+ ],
+ jarjar_rules: "jarjar-rules.txt",
+ permitted_packages: ["android.app.appsearch"],
+ apex_available: ["com.android.appsearch"],
+}
+
+metalava_appsearch_docs_args =
+ "--hide-package com.android.server " +
+ "--error UnhiddenSystemApi " +
+ "--hide RequiresPermission " +
+ "--hide MissingPermission " +
+ "--hide BroadcastBehavior " +
+ "--hide HiddenSuperclass " +
+ "--hide DeprecationMismatch " +
+ "--hide UnavailableSymbol " +
+ "--hide SdkConstant " +
+ "--hide HiddenTypeParameter " +
+ "--hide Todo --hide Typo " +
+ "--hide HiddenTypedefConstant " +
+ "--show-annotation android.annotation.SystemApi "
+
+droidstubs {
+ name: "framework-appsearch-stubs-srcs",
+ srcs: [":framework-appsearch-sources"],
+ libs: ["framework-annotations-lib"],
+ aidl: {
+ include_dirs: ["frameworks/base/core/java"],
+ },
+ args: metalava_appsearch_docs_args,
+ sdk_version: "module_current",
+}
+
+java_library {
+ name: "framework-appsearch-stubs",
+ srcs: [":framework-appsearch-stubs-srcs"],
+ aidl: {
+ export_include_dirs: [
+ "java",
+ ],
+ },
+ sdk_version: "module_current",
+ installable: false,
+}
diff --git a/apex/appsearch/framework/jarjar-rules.txt b/apex/appsearch/framework/jarjar-rules.txt
new file mode 100644
index 000000000000..acf759a58d56
--- /dev/null
+++ b/apex/appsearch/framework/jarjar-rules.txt
@@ -0,0 +1,2 @@
+rule com.google.protobuf.** android.app.appsearch.protobuf.@1
+rule com.google.android.icing.proto.** android.app.appsearch.proto.@1
diff --git a/apex/appsearch/framework/java/android/app/TEST_MAPPING b/apex/appsearch/framework/java/android/app/TEST_MAPPING
new file mode 100644
index 000000000000..12188f83a29f
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "imports": [
+ {
+ "path": "frameworks/base/apex/appsearch/service/java/com/android/server/appsearch"
+ }
+ ]
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchBatchResult.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchBatchResult.java
new file mode 100644
index 000000000000..dc758253f1a4
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchBatchResult.java
@@ -0,0 +1,163 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Provides access to multiple {@link AppSearchResult}s from a batch operation accepting multiple
+ * inputs.
+ *
+ * @param <KeyType> The type of the keys for {@link #getSuccesses} and {@link #getFailures}.
+ * @param <ValueType> The type of result objects associated with the keys.
+ * @hide
+ */
+public class AppSearchBatchResult<KeyType, ValueType> implements Parcelable {
+ @NonNull private final Map<KeyType, ValueType> mSuccesses;
+ @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mFailures;
+
+ private AppSearchBatchResult(
+ @NonNull Map<KeyType, ValueType> successes,
+ @NonNull Map<KeyType, AppSearchResult<ValueType>> failures) {
+ mSuccesses = successes;
+ mFailures = failures;
+ }
+
+ private AppSearchBatchResult(@NonNull Parcel in) {
+ mSuccesses = Collections.unmodifiableMap(in.readHashMap(/*loader=*/ null));
+ mFailures = Collections.unmodifiableMap(in.readHashMap(/*loader=*/ null));
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeMap(mSuccesses);
+ dest.writeMap(mFailures);
+ }
+
+ /** Returns {@code true} if this {@link AppSearchBatchResult} has no failures. */
+ public boolean isSuccess() {
+ return mFailures.isEmpty();
+ }
+
+ /**
+ * Returns a {@link Map} of all successful keys mapped to the successful {@link ValueType}
+ * values they produced.
+ *
+ * <p>The values of the {@link Map} will not be {@code null}.
+ */
+ @NonNull
+ public Map<KeyType, ValueType> getSuccesses() {
+ return mSuccesses;
+ }
+
+ /**
+ * Returns a {@link Map} of all failed keys mapped to the failed {@link AppSearchResult}s they
+ * produced.
+ *
+ * <p>The values of the {@link Map} will not be {@code null}.
+ */
+ @NonNull
+ public Map<KeyType, AppSearchResult<ValueType>> getFailures() {
+ return mFailures;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<AppSearchBatchResult> CREATOR =
+ new Creator<AppSearchBatchResult>() {
+ @NonNull
+ @Override
+ public AppSearchBatchResult createFromParcel(@NonNull Parcel in) {
+ return new AppSearchBatchResult(in);
+ }
+
+ @NonNull
+ @Override
+ public AppSearchBatchResult[] newArray(int size) {
+ return new AppSearchBatchResult[size];
+ }
+ };
+
+ /**
+ * Builder for {@link AppSearchBatchResult} objects.
+ *
+ * @param <KeyType> The type of keys.
+ * @param <ValueType> The type of result objects associated with the keys.
+ * @hide
+ */
+ public static final class Builder<KeyType, ValueType> {
+ private final Map<KeyType, ValueType> mSuccesses = new ArrayMap<>();
+ private final Map<KeyType, AppSearchResult<ValueType>> mFailures = new ArrayMap<>();
+
+ /** Creates a new {@link Builder} for this {@link AppSearchBatchResult}. */
+ public Builder() {}
+
+ /**
+ * Associates the {@code key} with the given successful return value.
+ *
+ * <p>Any previous mapping for a key, whether success or failure, is deleted.
+ */
+ public Builder setSuccess(@NonNull KeyType key, @Nullable ValueType result) {
+ return setResult(key, AppSearchResult.newSuccessfulResult(result));
+ }
+
+ /**
+ * Associates the {@code key} with the given failure code and error message.
+ *
+ * <p>Any previous mapping for a key, whether success or failure, is deleted.
+ */
+ public Builder setFailure(
+ @NonNull KeyType key,
+ @AppSearchResult.ResultCode int resultCode,
+ @Nullable String errorMessage) {
+ return setResult(key, AppSearchResult.newFailedResult(resultCode, errorMessage));
+ }
+
+ /**
+ * Associates the {@code key} with the given {@code result}.
+ *
+ * <p>Any previous mapping for a key, whether success or failure, is deleted.
+ */
+ @NonNull
+ public Builder setResult(@NonNull KeyType key, @NonNull AppSearchResult<ValueType> result) {
+ if (result.isSuccess()) {
+ mSuccesses.put(key, result.getResultValue());
+ mFailures.remove(key);
+ } else {
+ mFailures.put(key, result);
+ mSuccesses.remove(key);
+ }
+ return this;
+ }
+
+ /** Builds an {@link AppSearchBatchResult} from the contents of this {@link Builder}. */
+ @NonNull
+ public AppSearchBatchResult<KeyType, ValueType> build() {
+ return new AppSearchBatchResult<>(mSuccesses, mFailures);
+ }
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java
new file mode 100644
index 000000000000..9afa19475bef
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java
@@ -0,0 +1,694 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.DurationMillisLong;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.PropertyProto;
+import com.google.protobuf.ByteString;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Represents a document unit.
+ *
+ * <p>Documents are constructed via {@link AppSearchDocument.Builder}.
+ * @hide
+ */
+public class AppSearchDocument {
+ private static final String TAG = "AppSearchDocument";
+
+ /**
+ * The maximum number of elements in a repeatable field. Will reject the request if exceed
+ * this limit.
+ */
+ private static final int MAX_REPEATED_PROPERTY_LENGTH = 100;
+
+ /**
+ * The maximum {@link String#length} of a {@link String} field. Will reject the request if
+ * {@link String}s longer than this.
+ */
+ private static final int MAX_STRING_LENGTH = 20_000;
+
+ /**
+ * Contains {@link AppSearchDocument} basic information (uri, schemaType etc) and properties
+ * ordered by keys.
+ */
+ @NonNull
+ private final DocumentProto mProto;
+
+ /** Contains all properties in {@link #mProto} to support getting properties via keys. */
+ @NonNull
+ private final Map<String, Object> mProperties;
+
+ /**
+ * Creates a new {@link AppSearchDocument}.
+ * @param proto Contains {@link AppSearchDocument} basic information (uri, schemaType etc) and
+ * properties ordered by keys.
+ * @param propertiesMap Contains all properties in {@link #mProto} to support get properties
+ * via keys.
+ */
+ private AppSearchDocument(@NonNull DocumentProto proto,
+ @NonNull Map<String, Object> propertiesMap) {
+ mProto = proto;
+ mProperties = propertiesMap;
+ }
+
+ /**
+ * Creates a new {@link AppSearchDocument} from an existing instance.
+ *
+ * <p>This method should be only used by constructor of a subclass.
+ */
+ protected AppSearchDocument(@NonNull AppSearchDocument document) {
+ this(document.mProto, document.mProperties);
+ }
+
+ /** @hide */
+ AppSearchDocument(@NonNull DocumentProto documentProto) {
+ this(documentProto, new ArrayMap<>());
+ for (int i = 0; i < documentProto.getPropertiesCount(); i++) {
+ PropertyProto property = documentProto.getProperties(i);
+ String name = property.getName();
+ if (property.getStringValuesCount() > 0) {
+ String[] values = new String[property.getStringValuesCount()];
+ for (int j = 0; j < values.length; j++) {
+ values[j] = property.getStringValues(j);
+ }
+ mProperties.put(name, values);
+ } else if (property.getInt64ValuesCount() > 0) {
+ long[] values = new long[property.getInt64ValuesCount()];
+ for (int j = 0; j < values.length; j++) {
+ values[j] = property.getInt64Values(j);
+ }
+ mProperties.put(property.getName(), values);
+ } else if (property.getDoubleValuesCount() > 0) {
+ double[] values = new double[property.getDoubleValuesCount()];
+ for (int j = 0; j < values.length; j++) {
+ values[j] = property.getDoubleValues(j);
+ }
+ mProperties.put(property.getName(), values);
+ } else if (property.getBooleanValuesCount() > 0) {
+ boolean[] values = new boolean[property.getBooleanValuesCount()];
+ for (int j = 0; j < values.length; j++) {
+ values[j] = property.getBooleanValues(j);
+ }
+ mProperties.put(property.getName(), values);
+ } else if (property.getBytesValuesCount() > 0) {
+ byte[][] values = new byte[property.getBytesValuesCount()][];
+ for (int j = 0; j < values.length; j++) {
+ values[j] = property.getBytesValues(j).toByteArray();
+ }
+ mProperties.put(name, values);
+ } else if (property.getDocumentValuesCount() > 0) {
+ AppSearchDocument[] values =
+ new AppSearchDocument[property.getDocumentValuesCount()];
+ for (int j = 0; j < values.length; j++) {
+ values[j] = new AppSearchDocument(property.getDocumentValues(j));
+ }
+ mProperties.put(name, values);
+ } else {
+ throw new IllegalStateException("Unknown type of value: " + name);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link DocumentProto} of the {@link AppSearchDocument}.
+ *
+ * <p>The {@link DocumentProto} contains {@link AppSearchDocument}'s basic information and all
+ * properties ordered by keys.
+ * @hide
+ */
+ @NonNull
+ @VisibleForTesting
+ public DocumentProto getProto() {
+ return mProto;
+ }
+
+ /** Returns the URI of the {@link AppSearchDocument}. */
+ @NonNull
+ public String getUri() {
+ return mProto.getUri();
+ }
+
+ /** Returns the schema type of the {@link AppSearchDocument}. */
+ @NonNull
+ public String getSchemaType() {
+ return mProto.getSchema();
+ }
+
+ /**
+ * Returns the creation timestamp in milliseconds of the {@link AppSearchDocument}. Value will
+ * be in the {@link System#currentTimeMillis()} time base.
+ */
+ @CurrentTimeMillisLong
+ public long getCreationTimestampMillis() {
+ return mProto.getCreationTimestampMs();
+ }
+
+ /**
+ * Returns the TTL (Time To Live) of the {@link AppSearchDocument}, in milliseconds.
+ *
+ * <p>The default value is 0, which means the document is permanent and won't be auto-deleted
+ * until the app is uninstalled.
+ */
+ @DurationMillisLong
+ public long getTtlMillis() {
+ return mProto.getTtlMs();
+ }
+
+ /**
+ * Returns the score of the {@link AppSearchDocument}.
+ *
+ * <p>The score is a query-independent measure of the document's quality, relative to other
+ * {@link AppSearchDocument}s of the same type.
+ *
+ * <p>The default value is 0.
+ */
+ public int getScore() {
+ return mProto.getScore();
+ }
+
+ /**
+ * Retrieve a {@link String} value by key.
+ *
+ * @param key The key to look for.
+ * @return The first {@link String} associated with the given key or {@code null} if there
+ * is no such key or the value is of a different type.
+ */
+ @Nullable
+ public String getPropertyString(@NonNull String key) {
+ String[] propertyArray = getPropertyStringArray(key);
+ if (ArrayUtils.isEmpty(propertyArray)) {
+ return null;
+ }
+ warnIfSinglePropertyTooLong("String", key, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /**
+ * Retrieve a {@code long} value by key.
+ *
+ * @param key The key to look for.
+ * @return The first {@code long} associated with the given key or default value {@code 0} if
+ * there is no such key or the value is of a different type.
+ */
+ public long getPropertyLong(@NonNull String key) {
+ long[] propertyArray = getPropertyLongArray(key);
+ if (ArrayUtils.isEmpty(propertyArray)) {
+ return 0;
+ }
+ warnIfSinglePropertyTooLong("Long", key, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /**
+ * Retrieve a {@code double} value by key.
+ *
+ * @param key The key to look for.
+ * @return The first {@code double} associated with the given key or default value {@code 0.0}
+ * if there is no such key or the value is of a different type.
+ */
+ public double getPropertyDouble(@NonNull String key) {
+ double[] propertyArray = getPropertyDoubleArray(key);
+ // TODO(tytytyww): Add support double array to ArraysUtils.isEmpty().
+ if (propertyArray == null || propertyArray.length == 0) {
+ return 0.0;
+ }
+ warnIfSinglePropertyTooLong("Double", key, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /**
+ * Retrieve a {@code boolean} value by key.
+ *
+ * @param key The key to look for.
+ * @return The first {@code boolean} associated with the given key or default value
+ * {@code false} if there is no such key or the value is of a different type.
+ */
+ public boolean getPropertyBoolean(@NonNull String key) {
+ boolean[] propertyArray = getPropertyBooleanArray(key);
+ if (ArrayUtils.isEmpty(propertyArray)) {
+ return false;
+ }
+ warnIfSinglePropertyTooLong("Boolean", key, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /**
+ * Retrieve a {@code byte[]} value by key.
+ *
+ * @param key The key to look for.
+ * @return The first {@code byte[]} associated with the given key or {@code null} if there
+ * is no such key or the value is of a different type.
+ */
+ @Nullable
+ public byte[] getPropertyBytes(@NonNull String key) {
+ byte[][] propertyArray = getPropertyBytesArray(key);
+ if (ArrayUtils.isEmpty(propertyArray)) {
+ return null;
+ }
+ warnIfSinglePropertyTooLong("ByteArray", key, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /**
+ * Retrieve a {@link AppSearchDocument} value by key.
+ *
+ * @param key The key to look for.
+ * @return The first {@link AppSearchDocument} associated with the given key or {@code null} if
+ * there is no such key or the value is of a different type.
+ */
+ @Nullable
+ public AppSearchDocument getPropertyDocument(@NonNull String key) {
+ AppSearchDocument[] propertyArray = getPropertyDocumentArray(key);
+ if (ArrayUtils.isEmpty(propertyArray)) {
+ return null;
+ }
+ warnIfSinglePropertyTooLong("Document", key, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /** Prints a warning to logcat if the given propertyLength is greater than 1. */
+ private static void warnIfSinglePropertyTooLong(
+ @NonNull String propertyType, @NonNull String key, int propertyLength) {
+ if (propertyLength > 1) {
+ Log.w(TAG, "The value for \"" + key + "\" contains " + propertyLength
+ + " elements. Only the first one will be returned from "
+ + "getProperty" + propertyType + "(). Try getProperty" + propertyType
+ + "Array().");
+ }
+ }
+
+ /**
+ * Retrieve a repeated {@link String} property by key.
+ *
+ * @param key The key to look for.
+ * @return The {@code String[]} associated with the given key, or {@code null} if no value
+ * is set or the value is of a different type.
+ */
+ @Nullable
+ public String[] getPropertyStringArray(@NonNull String key) {
+ return getAndCastPropertyArray(key, String[].class);
+ }
+
+ /**
+ * Retrieve a repeated {@code long} property by key.
+ *
+ * @param key The key to look for.
+ * @return The {@code long[]} associated with the given key, or {@code null} if no value is
+ * set or the value is of a different type.
+ */
+ @Nullable
+ public long[] getPropertyLongArray(@NonNull String key) {
+ return getAndCastPropertyArray(key, long[].class);
+ }
+
+ /**
+ * Retrieve a repeated {@code double} property by key.
+ *
+ * @param key The key to look for.
+ * @return The {@code double[]} associated with the given key, or {@code null} if no value
+ * is set or the value is of a different type.
+ */
+ @Nullable
+ public double[] getPropertyDoubleArray(@NonNull String key) {
+ return getAndCastPropertyArray(key, double[].class);
+ }
+
+ /**
+ * Retrieve a repeated {@code boolean} property by key.
+ *
+ * @param key The key to look for.
+ * @return The {@code boolean[]} associated with the given key, or {@code null} if no value
+ * is set or the value is of a different type.
+ */
+ @Nullable
+ public boolean[] getPropertyBooleanArray(@NonNull String key) {
+ return getAndCastPropertyArray(key, boolean[].class);
+ }
+
+ /**
+ * Retrieve a {@code byte[][]} property by key.
+ *
+ * @param key The key to look for.
+ * @return The {@code byte[][]} associated with the given key, or {@code null} if no value
+ * is set or the value is of a different type.
+ */
+ @Nullable
+ public byte[][] getPropertyBytesArray(@NonNull String key) {
+ return getAndCastPropertyArray(key, byte[][].class);
+ }
+
+ /**
+ * Retrieve a repeated {@link AppSearchDocument} property by key.
+ *
+ * @param key The key to look for.
+ * @return The {@link AppSearchDocument[]} associated with the given key, or {@code null} if no
+ * value is set or the value is of a different type.
+ */
+ @Nullable
+ public AppSearchDocument[] getPropertyDocumentArray(@NonNull String key) {
+ return getAndCastPropertyArray(key, AppSearchDocument[].class);
+ }
+
+ /**
+ * Gets a repeated property of the given key, and casts it to the given class type, which
+ * must be an array class type.
+ */
+ @Nullable
+ private <T> T getAndCastPropertyArray(@NonNull String key, @NonNull Class<T> tClass) {
+ Object value = mProperties.get(key);
+ if (value == null) {
+ return null;
+ }
+ try {
+ return tClass.cast(value);
+ } catch (ClassCastException e) {
+ Log.w(TAG, "Error casting to requested type for key \"" + key + "\"", e);
+ return null;
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ // Check only proto's equality is sufficient here since all properties in
+ // mProperties are ordered by keys and stored in proto.
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof AppSearchDocument)) {
+ return false;
+ }
+ AppSearchDocument otherDocument = (AppSearchDocument) other;
+ return this.mProto.equals(otherDocument.mProto);
+ }
+
+ @Override
+ public int hashCode() {
+ // Hash only proto is sufficient here since all properties in mProperties are ordered by
+ // keys and stored in proto.
+ return mProto.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return mProto.toString();
+ }
+
+ /**
+ * The builder class for {@link AppSearchDocument}.
+ *
+ * @param <BuilderType> Type of subclass who extend this.
+ */
+ public static class Builder<BuilderType extends Builder> {
+
+ private final Map<String, Object> mProperties = new ArrayMap<>();
+ private final DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
+ private final BuilderType mBuilderTypeInstance;
+
+ /**
+ * Creates a new {@link AppSearchDocument.Builder}.
+ *
+ * <p>The URI is a unique string opaque to AppSearch.
+ *
+ * @param uri The uri of {@link AppSearchDocument}.
+ * @param schemaType The schema type of the {@link AppSearchDocument}. The passed-in
+ * {@code schemaType} must be defined using {@link AppSearchManager#setSchema} prior
+ * to inserting a document of this {@code schemaType} into the AppSearch index using
+ * {@link AppSearchManager#putDocuments(List)}. Otherwise, the document will be
+ * rejected by {@link AppSearchManager#putDocuments(List)}.
+ */
+ public Builder(@NonNull String uri, @NonNull String schemaType) {
+ mBuilderTypeInstance = (BuilderType) this;
+ mProtoBuilder.setUri(uri).setSchema(schemaType);
+ // Set current timestamp for creation timestamp by default.
+ setCreationTimestampMillis(System.currentTimeMillis());
+ }
+
+ /**
+ * Sets the score of the {@link AppSearchDocument}.
+ *
+ * <p>The score is a query-independent measure of the document's quality, relative to
+ * other {@link AppSearchDocument}s of the same type.
+ *
+ * @throws IllegalArgumentException If the provided value is negative.
+ */
+ @NonNull
+ public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) {
+ if (score < 0) {
+ throw new IllegalArgumentException("Document score cannot be negative.");
+ }
+ mProtoBuilder.setScore(score);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Set the creation timestamp in milliseconds of the {@link AppSearchDocument}. Should be
+ * set using a value obtained from the {@link System#currentTimeMillis()} time base.
+ */
+ @NonNull
+ public BuilderType setCreationTimestampMillis(
+ @CurrentTimeMillisLong long creationTimestampMillis) {
+ mProtoBuilder.setCreationTimestampMs(creationTimestampMillis);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Set the TTL (Time To Live) of the {@link AppSearchDocument}, in milliseconds.
+ *
+ * <p>After this many milliseconds since the {@link #setCreationTimestampMillis(long)}
+ * creation timestamp}, the document is deleted.
+ *
+ * @param ttlMillis A non-negative duration in milliseconds.
+ * @throws IllegalArgumentException If the provided value is negative.
+ */
+ @NonNull
+ public BuilderType setTtlMillis(@DurationMillisLong long ttlMillis) {
+ Preconditions.checkArgumentNonNegative(
+ ttlMillis, "Document ttlMillis cannot be negative.");
+ mProtoBuilder.setTtlMs(ttlMillis);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@code String} values for a property, replacing its previous
+ * values.
+ *
+ * @param key The key associated with the {@code values}.
+ * @param values The {@code String} values of the property.
+ */
+ @NonNull
+ public BuilderType setProperty(@NonNull String key, @NonNull String... values) {
+ putInPropertyMap(key, values);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@code boolean} values for a property, replacing its previous
+ * values.
+ *
+ * @param key The key associated with the {@code values}.
+ * @param values The {@code boolean} values of the property.
+ */
+ @NonNull
+ public BuilderType setProperty(@NonNull String key, @NonNull boolean... values) {
+ putInPropertyMap(key, values);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@code long} values for a property, replacing its previous
+ * values.
+ *
+ * @param key The key associated with the {@code values}.
+ * @param values The {@code long} values of the property.
+ */
+ @NonNull
+ public BuilderType setProperty(@NonNull String key, @NonNull long... values) {
+ putInPropertyMap(key, values);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@code double} values for a property, replacing its previous
+ * values.
+ *
+ * @param key The key associated with the {@code values}.
+ * @param values The {@code double} values of the property.
+ */
+ @NonNull
+ public BuilderType setProperty(@NonNull String key, @NonNull double... values) {
+ putInPropertyMap(key, values);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@code byte[]} for a property, replacing its previous values.
+ *
+ * @param key The key associated with the {@code values}.
+ * @param values The {@code byte[]} of the property.
+ */
+ @NonNull
+ public BuilderType setProperty(@NonNull String key, @NonNull byte[]... values) {
+ putInPropertyMap(key, values);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@link AppSearchDocument} values for a property, replacing its
+ * previous values.
+ *
+ * @param key The key associated with the {@code values}.
+ * @param values The {@link AppSearchDocument} values of the property.
+ */
+ @NonNull
+ public BuilderType setProperty(@NonNull String key, @NonNull AppSearchDocument... values) {
+ putInPropertyMap(key, values);
+ return mBuilderTypeInstance;
+ }
+
+ private void putInPropertyMap(@NonNull String key, @NonNull String[] values)
+ throws IllegalArgumentException {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(values);
+ validateRepeatedPropertyLength(key, values.length);
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] == null) {
+ throw new IllegalArgumentException("The String at " + i + " is null.");
+ } else if (values[i].length() > MAX_STRING_LENGTH) {
+ throw new IllegalArgumentException("The String at " + i + " length is: "
+ + values[i].length() + ", which exceeds length limit: "
+ + MAX_STRING_LENGTH + ".");
+ }
+ }
+ mProperties.put(key, values);
+ }
+
+ private void putInPropertyMap(@NonNull String key, @NonNull boolean[] values) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(values);
+ validateRepeatedPropertyLength(key, values.length);
+ mProperties.put(key, values);
+ }
+
+ private void putInPropertyMap(@NonNull String key, @NonNull double[] values) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(values);
+ validateRepeatedPropertyLength(key, values.length);
+ mProperties.put(key, values);
+ }
+
+ private void putInPropertyMap(@NonNull String key, @NonNull long[] values) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(values);
+ validateRepeatedPropertyLength(key, values.length);
+ mProperties.put(key, values);
+ }
+
+ private void putInPropertyMap(@NonNull String key, @NonNull byte[][] values) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(values);
+ validateRepeatedPropertyLength(key, values.length);
+ mProperties.put(key, values);
+ }
+
+ private void putInPropertyMap(@NonNull String key, @NonNull AppSearchDocument[] values) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(values);
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] == null) {
+ throw new IllegalArgumentException("The document at " + i + " is null.");
+ }
+ }
+ validateRepeatedPropertyLength(key, values.length);
+ mProperties.put(key, values);
+ }
+
+ private static void validateRepeatedPropertyLength(@NonNull String key, int length) {
+ if (length == 0) {
+ throw new IllegalArgumentException("The input array is empty.");
+ } else if (length > MAX_REPEATED_PROPERTY_LENGTH) {
+ throw new IllegalArgumentException(
+ "Repeated property \"" + key + "\" has length " + length
+ + ", which exceeds the limit of "
+ + MAX_REPEATED_PROPERTY_LENGTH);
+ }
+ }
+
+ /** Builds the {@link AppSearchDocument} object. */
+ @NonNull
+ public AppSearchDocument build() {
+ // Build proto by sorting the keys in mProperties to exclude the influence of
+ // order. Therefore documents will generate same proto as long as the contents are
+ // same. Note that the order of repeated fields is still preserved.
+ ArrayList<String> keys = new ArrayList<>(mProperties.keySet());
+ Collections.sort(keys);
+ for (int i = 0; i < keys.size(); i++) {
+ String name = keys.get(i);
+ Object values = mProperties.get(name);
+ PropertyProto.Builder propertyProto = PropertyProto.newBuilder().setName(name);
+ if (values instanceof boolean[]) {
+ for (boolean value : (boolean[]) values) {
+ propertyProto.addBooleanValues(value);
+ }
+ } else if (values instanceof long[]) {
+ for (long value : (long[]) values) {
+ propertyProto.addInt64Values(value);
+ }
+ } else if (values instanceof double[]) {
+ for (double value : (double[]) values) {
+ propertyProto.addDoubleValues(value);
+ }
+ } else if (values instanceof String[]) {
+ for (String value : (String[]) values) {
+ propertyProto.addStringValues(value);
+ }
+ } else if (values instanceof AppSearchDocument[]) {
+ for (AppSearchDocument value : (AppSearchDocument[]) values) {
+ propertyProto.addDocumentValues(value.getProto());
+ }
+ } else if (values instanceof byte[][]) {
+ for (byte[] value : (byte[][]) values) {
+ propertyProto.addBytesValues(ByteString.copyFrom(value));
+ }
+ } else {
+ throw new IllegalStateException(
+ "Property \"" + name + "\" has unsupported value type \""
+ + values.getClass().getSimpleName() + "\"");
+ }
+ mProtoBuilder.addProperties(propertyProto);
+ }
+ return new AppSearchDocument(mProtoBuilder.build(), mProperties);
+ }
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java
new file mode 100644
index 000000000000..b13dd9f48c8d
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java
@@ -0,0 +1,256 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchSchema.PropertyConfig;
+
+/**
+ * Encapsulates a {@link AppSearchDocument} that represent an email.
+ *
+ * <p>This class is a higher level implement of {@link AppSearchDocument}.
+ *
+ * <p>This class will eventually migrate to Jetpack, where it will become public API.
+ *
+ * @hide
+ */
+public class AppSearchEmail extends AppSearchDocument {
+ private static final String KEY_FROM = "from";
+ private static final String KEY_TO = "to";
+ private static final String KEY_CC = "cc";
+ private static final String KEY_BCC = "bcc";
+ private static final String KEY_SUBJECT = "subject";
+ private static final String KEY_BODY = "body";
+
+ /** The name of the schema type for {@link AppSearchEmail} documents.*/
+ public static final String SCHEMA_TYPE = "builtin:Email";
+
+ public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
+ .addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_FROM)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_TO)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_CC)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_BCC)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_SUBJECT)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_BODY)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build()
+
+ ).build();
+
+ /**
+ * Creates a new {@link AppSearchEmail} from the contents of an existing
+ * {@link AppSearchDocument}.
+ *
+ * @param document The {@link AppSearchDocument} containing the email content.
+ * @hide
+ */
+ public AppSearchEmail(@NonNull AppSearchDocument document) {
+ super(document);
+ }
+
+ /**
+ * Get the from address of {@link AppSearchEmail}.
+ *
+ * @return Returns the subject of {@link AppSearchEmail} or {@code null} if it's not been set
+ * yet.
+ * @hide
+ */
+ @Nullable
+ public String getFrom() {
+ return getPropertyString(KEY_FROM);
+ }
+
+ /**
+ * Get the destination addresses of {@link AppSearchEmail}.
+ *
+ * @return Returns the destination addresses of {@link AppSearchEmail} or {@code null} if it's
+ * not been set yet.
+ * @hide
+ */
+ @Nullable
+ public String[] getTo() {
+ return getPropertyStringArray(KEY_TO);
+ }
+
+ /**
+ * Get the CC list of {@link AppSearchEmail}.
+ *
+ * @return Returns the CC list of {@link AppSearchEmail} or {@code null} if it's not been set
+ * yet.
+ * @hide
+ */
+ @Nullable
+ public String[] getCc() {
+ return getPropertyStringArray(KEY_CC);
+ }
+
+ /**
+ * Get the BCC list of {@link AppSearchEmail}.
+ *
+ * @return Returns the BCC list of {@link AppSearchEmail} or {@code null} if it's not been set
+ * yet.
+ * @hide
+ */
+ @Nullable
+ public String[] getBcc() {
+ return getPropertyStringArray(KEY_BCC);
+ }
+
+ /**
+ * Get the subject of {@link AppSearchEmail}.
+ *
+ * @return Returns the value subject of {@link AppSearchEmail} or {@code null} if it's not been
+ * set yet.
+ * @hide
+ */
+ @Nullable
+ public String getSubject() {
+ return getPropertyString(KEY_SUBJECT);
+ }
+
+ /**
+ * Get the body of {@link AppSearchEmail}.
+ *
+ * @return Returns the body of {@link AppSearchEmail} or {@code null} if it's not been set yet.
+ * @hide
+ */
+ @Nullable
+ public String getBody() {
+ return getPropertyString(KEY_BODY);
+ }
+
+ /**
+ * The builder class for {@link AppSearchEmail}.
+ * @hide
+ */
+ public static class Builder extends AppSearchDocument.Builder<AppSearchEmail.Builder> {
+
+ /**
+ * Create a new {@link AppSearchEmail.Builder}
+ * @param uri The Uri of the Email.
+ * @hide
+ */
+ public Builder(@NonNull String uri) {
+ super(uri, SCHEMA_TYPE);
+ }
+
+ /**
+ * Set the from address of {@link AppSearchEmail}
+ * @hide
+ */
+ @NonNull
+ public AppSearchEmail.Builder setFrom(@NonNull String from) {
+ setProperty(KEY_FROM, from);
+ return this;
+ }
+
+ /**
+ * Set the destination address of {@link AppSearchEmail}
+ * @hide
+ */
+ @NonNull
+ public AppSearchEmail.Builder setTo(@NonNull String... to) {
+ setProperty(KEY_TO, to);
+ return this;
+ }
+
+ /**
+ * Set the CC list of {@link AppSearchEmail}
+ * @hide
+ */
+ @NonNull
+ public AppSearchEmail.Builder setCc(@NonNull String... cc) {
+ setProperty(KEY_CC, cc);
+ return this;
+ }
+
+ /**
+ * Set the BCC list of {@link AppSearchEmail}
+ * @hide
+ */
+ @NonNull
+ public AppSearchEmail.Builder setBcc(@NonNull String... bcc) {
+ setProperty(KEY_BCC, bcc);
+ return this;
+ }
+
+ /**
+ * Set the subject of {@link AppSearchEmail}
+ * @hide
+ */
+ @NonNull
+ public AppSearchEmail.Builder setSubject(@NonNull String subject) {
+ setProperty(KEY_SUBJECT, subject);
+ return this;
+ }
+
+ /**
+ * Set the body of {@link AppSearchEmail}
+ * @hide
+ */
+ @NonNull
+ public AppSearchEmail.Builder setBody(@NonNull String body) {
+ setProperty(KEY_BODY, body);
+ return this;
+ }
+
+ /**
+ * Builds the {@link AppSearchEmail} object.
+ *
+ * @hide
+ */
+ @NonNull
+ @Override
+ public AppSearchEmail build() {
+ return new AppSearchEmail(super.build());
+ }
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java
new file mode 100644
index 000000000000..64264c03f79e
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java
@@ -0,0 +1,405 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+
+import com.android.internal.infra.AndroidFuture;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.SchemaProto;
+import com.google.android.icing.proto.SearchResultProto;
+import com.google.android.icing.proto.SearchSpecProto;
+import com.google.android.icing.proto.StatusProto;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * This class provides access to the centralized AppSearch index maintained by the system.
+ *
+ * <p>Apps can index structured text documents with AppSearch, which can then be retrieved through
+ * the query API.
+ *
+ * @hide
+ */
+// TODO(b/148046169): This class header needs a detailed example/tutorial.
+@SystemService(Context.APP_SEARCH_SERVICE)
+public class AppSearchManager {
+ private final IAppSearchManager mService;
+
+ /** @hide */
+ public AppSearchManager(@NonNull IAppSearchManager service) {
+ mService = service;
+ }
+
+ /**
+ * Sets the schema being used by documents provided to the {@link #putDocuments} method.
+ *
+ * <p>The schema provided here is compared to the stored copy of the schema previously supplied
+ * to {@link #setSchema}, if any, to determine how to treat existing documents. The following
+ * types of schema modifications are always safe and are made without deleting any existing
+ * documents:
+ * <ul>
+ * <li>Addition of new types
+ * <li>Addition of new
+ * {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL
+ * OPTIONAL} or
+ * {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_REPEATED
+ * REPEATED} properties to a type
+ * <li>Changing the cardinality of a data type to be less restrictive (e.g. changing an
+ * {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL
+ * OPTIONAL} property into a
+ * {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_REPEATED
+ * REPEATED} property.
+ * </ul>
+ *
+ * <p>The following types of schema changes are not backwards-compatible:
+ * <ul>
+ * <li>Removal of an existing type
+ * <li>Removal of a property from a type
+ * <li>Changing the data type ({@code boolean}, {@code long}, etc.) of an existing property
+ * <li>For properties of {@code AppSearchDocument} type, changing the schema type of
+ * {@code AppSearchDocument}s of that property
+ * <li>Changing the cardinality of a data type to be more restrictive (e.g. changing an
+ * {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL
+ * OPTIONAL} property into a
+ * {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_REQUIRED
+ * REQUIRED} property).
+ * <li>Adding a
+ * {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_REQUIRED
+ * REQUIRED} property.
+ * </ul>
+ * <p>Supplying a schema with such changes will result in this call returning an
+ * {@link AppSearchResult} with a code of {@link AppSearchResult#RESULT_INVALID_SCHEMA} and an
+ * error message describing the incompatibility. In this case the previously set schema will
+ * remain active.
+ *
+ * <p>If you need to make non-backwards-compatible changes as described above, instead use the
+ * {@link #setSchema(List, boolean)} method with the {@code forceOverride} parameter set to
+ * {@code true}.
+ *
+ * <p>It is a no-op to set the same schema as has been previously set; this is handled
+ * efficiently.
+ *
+ * @param schemas The schema configs for the types used by the calling app.
+ * @return the result of performing this operation.
+ *
+ * @hide
+ */
+ @NonNull
+ public AppSearchResult<Void> setSchema(@NonNull AppSearchSchema... schemas) {
+ return setSchema(Arrays.asList(schemas), /*forceOverride=*/false);
+ }
+
+ /**
+ * Sets the schema being used by documents provided to the {@link #putDocuments} method.
+ *
+ * <p>This method is similar to {@link #setSchema(AppSearchSchema...)}, except for the
+ * {@code forceOverride} parameter. If a backwards-incompatible schema is specified but the
+ * {@code forceOverride} parameter is set to {@code true}, instead of returning an
+ * {@link AppSearchResult} with the {@link AppSearchResult#RESULT_INVALID_SCHEMA} code, all
+ * documents which are not compatible with the new schema will be deleted and the incompatible
+ * schema will be applied.
+ *
+ * @param schemas The schema configs for the types used by the calling app.
+ * @param forceOverride Whether to force the new schema to be applied even if there are
+ * incompatible changes versus the previously set schema. Documents which are incompatible
+ * with the new schema will be deleted.
+ * @return the result of performing this operation.
+ *
+ * @hide
+ */
+ @NonNull
+ public AppSearchResult<Void> setSchema(
+ @NonNull List<AppSearchSchema> schemas, boolean forceOverride) {
+ // Prepare the merged schema for transmission.
+ SchemaProto.Builder schemaProtoBuilder = SchemaProto.newBuilder();
+ for (AppSearchSchema schema : schemas) {
+ schemaProtoBuilder.addTypes(schema.getProto());
+ }
+
+ // Serialize and send the schema.
+ // TODO: This should use com.android.internal.infra.RemoteStream or another mechanism to
+ // avoid binder limits.
+ byte[] schemaBytes = schemaProtoBuilder.build().toByteArray();
+ AndroidFuture<AppSearchResult> future = new AndroidFuture<>();
+ try {
+ mService.setSchema(schemaBytes, forceOverride, future);
+ } catch (RemoteException e) {
+ future.completeExceptionally(e);
+ }
+ return getFutureOrThrow(future);
+ }
+
+ /**
+ * Index {@link AppSearchDocument}s into AppSearch.
+ *
+ * <p>You should not call this method directly; instead, use the
+ * {@code AppSearch#putDocuments()} API provided by JetPack.
+ *
+ * <p>Each {@link AppSearchDocument}'s {@code schemaType} field must be set to the name of a
+ * schema type previously registered via the {@link #setSchema} method.
+ *
+ * @param documents {@link AppSearchDocument}s that need to be indexed.
+ * @return An {@link AppSearchBatchResult} mapping the document URIs to {@link Void} if they
+ * were successfully indexed, or a {@link Throwable} describing the failure if they could
+ * not be indexed.
+ * @hide
+ */
+ public AppSearchBatchResult<String, Void> putDocuments(
+ @NonNull List<AppSearchDocument> documents) {
+ // TODO(b/146386470): Transmit these documents as a RemoteStream instead of sending them in
+ // one big list.
+ List<byte[]> documentsBytes = new ArrayList<>(documents.size());
+ for (AppSearchDocument document : documents) {
+ documentsBytes.add(document.getProto().toByteArray());
+ }
+ AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
+ try {
+ mService.putDocuments(documentsBytes, future);
+ } catch (RemoteException e) {
+ future.completeExceptionally(e);
+ }
+ return getFutureOrThrow(future);
+ }
+
+ /**
+ * Retrieves {@link AppSearchDocument}s by URI.
+ *
+ * <p>You should not call this method directly; instead, use the
+ * {@code AppSearch#getDocuments()} API provided by JetPack.
+ *
+ * @param uris URIs of the documents to look up.
+ * @return An {@link AppSearchBatchResult} mapping the document URIs to
+ * {@link AppSearchDocument} values if they were successfully retrieved, a {@code null}
+ * failure if they were not found, or a {@link Throwable} failure describing the problem if
+ * an error occurred.
+ */
+ public AppSearchBatchResult<String, AppSearchDocument> getDocuments(
+ @NonNull List<String> uris) {
+ // TODO(b/146386470): Transmit the result documents as a RemoteStream instead of sending
+ // them in one big list.
+ AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
+ try {
+ mService.getDocuments(uris, future);
+ } catch (RemoteException e) {
+ future.completeExceptionally(e);
+ }
+
+ // Deserialize the protos into Document objects
+ AppSearchBatchResult<String, byte[]> protoResults = getFutureOrThrow(future);
+ AppSearchBatchResult.Builder<String, AppSearchDocument> documentResultBuilder =
+ new AppSearchBatchResult.Builder<>();
+
+ // Translate successful results
+ for (Map.Entry<String, byte[]> protoResult : protoResults.getSuccesses().entrySet()) {
+ DocumentProto documentProto;
+ try {
+ documentProto = DocumentProto.parseFrom(protoResult.getValue());
+ } catch (InvalidProtocolBufferException e) {
+ documentResultBuilder.setFailure(
+ protoResult.getKey(), AppSearchResult.RESULT_IO_ERROR, e.getMessage());
+ continue;
+ }
+ AppSearchDocument document;
+ try {
+ document = new AppSearchDocument(documentProto);
+ } catch (Throwable t) {
+ // These documents went through validation, so how could this fail? We must have
+ // done something wrong.
+ documentResultBuilder.setFailure(
+ protoResult.getKey(),
+ AppSearchResult.RESULT_INTERNAL_ERROR,
+ t.getMessage());
+ continue;
+ }
+ documentResultBuilder.setSuccess(protoResult.getKey(), document);
+ }
+
+ // Translate failed results
+ for (Map.Entry<String, AppSearchResult<byte[]>> protoResult :
+ protoResults.getFailures().entrySet()) {
+ documentResultBuilder.setFailure(
+ protoResult.getKey(),
+ protoResult.getValue().getResultCode(),
+ protoResult.getValue().getErrorMessage());
+ }
+
+ return documentResultBuilder.build();
+ }
+
+ /**
+ * Searches a document based on a given query string.
+ *
+ * <p>You should not call this method directly; instead, use the {@code AppSearch#query()} API
+ * provided by JetPack.
+ *
+ * <p>Currently we support following features in the raw query format:
+ * <ul>
+ * <li>AND
+ * <p>AND joins (e.g. “match documents that have both the terms ‘dog’ and
+ * ‘cat’”).
+ * Example: hello world matches documents that have both ‘hello’ and ‘world’
+ * <li>OR
+ * <p>OR joins (e.g. “match documents that have either the term ‘dog’ or
+ * ‘cat’”).
+ * Example: dog OR puppy
+ * <li>Exclusion
+ * <p>Exclude a term (e.g. “match documents that do
+ * not have the term ‘dog’”).
+ * Example: -dog excludes the term ‘dog’
+ * <li>Grouping terms
+ * <p>Allow for conceptual grouping of subqueries to enable hierarchical structures (e.g.
+ * “match documents that have either ‘dog’ or ‘puppy’, and either ‘cat’ or ‘kitten’”).
+ * Example: (dog puppy) (cat kitten) two one group containing two terms.
+ * <li>Property restricts
+ * <p> Specifies which properties of a document to specifically match terms in (e.g.
+ * “match documents where the ‘subject’ property contains ‘important’”).
+ * Example: subject:important matches documents with the term ‘important’ in the
+ * ‘subject’ property
+ * <li>Schema type restricts
+ * <p>This is similar to property restricts, but allows for restricts on top-level document
+ * fields, such as schema_type. Clients should be able to limit their query to documents of
+ * a certain schema_type (e.g. “match documents that are of the ‘Email’ schema_type”).
+ * Example: { schema_type_filters: “Email”, “Video”,query: “dog” } will match documents
+ * that contain the query term ‘dog’ and are of either the ‘Email’ schema type or the
+ * ‘Video’ schema type.
+ * </ul>
+ *
+ * @param queryExpression Query String to search.
+ * @param searchSpec Spec for setting filters, raw query etc.
+ * @hide
+ */
+ @NonNull
+ public AppSearchResult<SearchResults> query(
+ @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+ // TODO(b/146386470): Transmit the result documents as a RemoteStream instead of sending
+ // them in one big list.
+ AndroidFuture<AppSearchResult> searchResultFuture = new AndroidFuture<>();
+ try {
+ SearchSpecProto searchSpecProto = searchSpec.getSearchSpecProto();
+ searchSpecProto = searchSpecProto.toBuilder().setQuery(queryExpression).build();
+ mService.query(
+ searchSpecProto.toByteArray(),
+ searchSpec.getResultSpecProto().toByteArray(),
+ searchSpec.getScoringSpecProto().toByteArray(),
+ searchResultFuture);
+ } catch (RemoteException e) {
+ searchResultFuture.completeExceptionally(e);
+ }
+
+ // Deserialize the protos into Document objects
+ AppSearchResult<byte[]> searchResultBytes = getFutureOrThrow(searchResultFuture);
+ if (!searchResultBytes.isSuccess()) {
+ return AppSearchResult.newFailedResult(
+ searchResultBytes.getResultCode(), searchResultBytes.getErrorMessage());
+ }
+ SearchResultProto searchResultProto;
+ try {
+ searchResultProto = SearchResultProto.parseFrom(searchResultBytes.getResultValue());
+ } catch (InvalidProtocolBufferException e) {
+ return AppSearchResult.newFailedResult(
+ AppSearchResult.RESULT_INTERNAL_ERROR, e.getMessage());
+ }
+ if (searchResultProto.getStatus().getCode() != StatusProto.Code.OK) {
+ // This should never happen; AppSearchManagerService should catch failed searchResults
+ // entries and transmit them as a failed AppSearchResult.
+ return AppSearchResult.newFailedResult(
+ AppSearchResult.RESULT_INTERNAL_ERROR,
+ searchResultProto.getStatus().getMessage());
+ }
+
+ return AppSearchResult.newSuccessfulResult(new SearchResults(searchResultProto));
+ }
+
+ /**
+ * Deletes {@link AppSearchDocument}s by URI.
+ *
+ * <p>You should not call this method directly; instead, use the {@code AppSearch#delete()} API
+ * provided by JetPack.
+ *
+ * @param uris URIs of the documents to delete
+ * @return An {@link AppSearchBatchResult} mapping each URI to a {@code null} success if
+ * deletion was successful, to a {@code null} failure if the document did not exist, or to a
+ * {@code throwable} failure if deletion failed for another reason.
+ */
+ public AppSearchBatchResult<String, Void> delete(@NonNull List<String> uris) {
+ AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
+ try {
+ mService.delete(uris, future);
+ } catch (RemoteException e) {
+ future.completeExceptionally(e);
+ }
+ return getFutureOrThrow(future);
+ }
+
+ /**
+ * Deletes {@link android.app.appsearch.AppSearch.Document}s by schema type.
+ *
+ * <p>You should not call this method directly; instead, use the
+ * {@code AppSearch#deleteByType()} API provided by JetPack.
+ *
+ * @param schemaTypes Schema types whose documents to delete.
+ * @return An {@link AppSearchBatchResult} mapping each schema type to a {@code null} success if
+ * deletion was successful, to a {@code null} failure if the type did not exist, or to a
+ * {@code throwable} failure if deletion failed for another reason.
+ */
+ public AppSearchBatchResult<String, Void> deleteByTypes(@NonNull List<String> schemaTypes) {
+ AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
+ try {
+ mService.deleteByTypes(schemaTypes, future);
+ } catch (RemoteException e) {
+ future.completeExceptionally(e);
+ }
+ return getFutureOrThrow(future);
+ }
+
+ /** Deletes all documents owned by the calling app. */
+ public AppSearchResult<Void> deleteAll() {
+ AndroidFuture<AppSearchResult> future = new AndroidFuture<>();
+ try {
+ mService.deleteAll(future);
+ } catch (RemoteException e) {
+ future.completeExceptionally(e);
+ }
+ return getFutureOrThrow(future);
+ }
+
+ private static <T> T getFutureOrThrow(@NonNull AndroidFuture<T> future) {
+ try {
+ return future.get();
+ } catch (Throwable e) {
+ if (e instanceof ExecutionException) {
+ e = e.getCause();
+ }
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ }
+ if (e instanceof Error) {
+ throw (Error) e;
+ }
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java
new file mode 100644
index 000000000000..02cc967e7daf
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java
@@ -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.
+ */
+package android.app.appsearch;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+
+/**
+ * Class holding initialization code for the AppSearch module.
+ *
+ * @hide
+ */
+@SystemApi
+public class AppSearchManagerFrameworkInitializer {
+ private AppSearchManagerFrameworkInitializer() {}
+
+ /**
+ * Called by {@link SystemServiceRegistry}'s static initializer and registers all AppSearch
+ * 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 initialize() {
+ SystemServiceRegistry.registerStaticService(
+ Context.APP_SEARCH_SERVICE, AppSearchManager.class,
+ (service) -> new AppSearchManager(IAppSearchManager.Stub.asInterface(service)));
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchResult.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchResult.java
new file mode 100644
index 000000000000..7f3834852eda
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchResult.java
@@ -0,0 +1,215 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Information about the success or failure of an AppSearch call.
+ *
+ * @param <ValueType> The type of result object for successful calls.
+ * @hide
+ */
+public class AppSearchResult<ValueType> implements Parcelable {
+ /** Result codes from {@link AppSearchManager} methods. */
+ @IntDef(prefix = {"RESULT_"}, value = {
+ RESULT_OK,
+ RESULT_UNKNOWN_ERROR,
+ RESULT_INTERNAL_ERROR,
+ RESULT_INVALID_ARGUMENT,
+ RESULT_IO_ERROR,
+ RESULT_OUT_OF_SPACE,
+ RESULT_NOT_FOUND,
+ RESULT_INVALID_SCHEMA,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ResultCode {}
+
+ /** The call was successful. */
+ public static final int RESULT_OK = 0;
+
+ /** An unknown error occurred while processing the call. */
+ public static final int RESULT_UNKNOWN_ERROR = 1;
+
+ /**
+ * An internal error occurred within AppSearch, which the caller cannot address.
+ *
+ * This error may be considered similar to {@link IllegalStateException}
+ */
+ public static final int RESULT_INTERNAL_ERROR = 2;
+
+ /**
+ * The caller supplied invalid arguments to the call.
+ *
+ * This error may be considered similar to {@link IllegalArgumentException}.
+ */
+ public static final int RESULT_INVALID_ARGUMENT = 3;
+
+ /**
+ * An issue occurred reading or writing to storage. The call might succeed if repeated.
+ *
+ * This error may be considered similar to {@link java.io.IOException}.
+ */
+ public static final int RESULT_IO_ERROR = 4;
+
+ /** Storage is out of space, and no more space could be reclaimed. */
+ public static final int RESULT_OUT_OF_SPACE = 5;
+
+ /** An entity the caller requested to interact with does not exist in the system. */
+ public static final int RESULT_NOT_FOUND = 6;
+
+ /** The caller supplied a schema which is invalid or incompatible with the previous schema. */
+ public static final int RESULT_INVALID_SCHEMA = 7;
+
+ private final @ResultCode int mResultCode;
+ @Nullable private final ValueType mResultValue;
+ @Nullable private final String mErrorMessage;
+
+ private AppSearchResult(
+ @ResultCode int resultCode,
+ @Nullable ValueType resultValue,
+ @Nullable String errorMessage) {
+ mResultCode = resultCode;
+ mResultValue = resultValue;
+ mErrorMessage = errorMessage;
+ }
+
+ private AppSearchResult(@NonNull Parcel in) {
+ mResultCode = in.readInt();
+ mResultValue = (ValueType) in.readValue(/*loader=*/ null);
+ mErrorMessage = in.readString();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mResultCode);
+ dest.writeValue(mResultValue);
+ dest.writeString(mErrorMessage);
+ }
+
+ /** Returns {@code true} if {@link #getResultCode} equals {@link AppSearchResult#RESULT_OK}. */
+ public boolean isSuccess() {
+ return getResultCode() == RESULT_OK;
+ }
+
+ /** Returns one of the {@code RESULT} constants defined in {@link AppSearchResult}. */
+ public @ResultCode int getResultCode() {
+ return mResultCode;
+ }
+
+ /**
+ * Returns the returned value associated with this result.
+ *
+ * <p>If {@link #isSuccess} is {@code false}, the result value is always {@code null}. The value
+ * may be {@code null} even if {@link #isSuccess} is {@code true}. See the documentation of the
+ * particular {@link AppSearchManager} call producing this {@link AppSearchResult} for what is
+ * returned by {@link #getResultValue}.
+ */
+ @Nullable
+ public ValueType getResultValue() {
+ return mResultValue;
+ }
+
+ /**
+ * Returns the error message associated with this result.
+ *
+ * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}. The error
+ * message may be {@code null} even if {@link #isSuccess} is {@code false}. See the
+ * documentation of the particular {@link AppSearchManager} call producing this
+ * {@link AppSearchResult} for what is returned by {@link #getErrorMessage}.
+ */
+ @Nullable
+ public String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof AppSearchResult)) {
+ return false;
+ }
+ AppSearchResult<?> otherResult = (AppSearchResult) other;
+ return mResultCode == otherResult.mResultCode
+ && Objects.equals(mResultValue, otherResult.mResultValue)
+ && Objects.equals(mErrorMessage, otherResult.mErrorMessage);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mResultCode, mResultValue, mErrorMessage);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ if (isSuccess()) {
+ return "AppSearchResult [SUCCESS]: " + mResultValue;
+ }
+ return "AppSearchResult [FAILURE(" + mResultCode + ")]: " + mErrorMessage;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<AppSearchResult> CREATOR =
+ new Creator<AppSearchResult>() {
+ @NonNull
+ @Override
+ public AppSearchResult createFromParcel(@NonNull Parcel in) {
+ return new AppSearchResult(in);
+ }
+
+ @NonNull
+ @Override
+ public AppSearchResult[] newArray(int size) {
+ return new AppSearchResult[size];
+ }
+ };
+
+ /**
+ * Creates a new successful {@link AppSearchResult}.
+ * @hide
+ */
+ @NonNull
+ public static <ValueType> AppSearchResult<ValueType> newSuccessfulResult(
+ @Nullable ValueType value) {
+ return new AppSearchResult<>(RESULT_OK, value, /*errorMessage=*/ null);
+ }
+
+ /**
+ * Creates a new failed {@link AppSearchResult}.
+ * @hide
+ */
+ @NonNull
+ public static <ValueType> AppSearchResult<ValueType> newFailedResult(
+ @ResultCode int resultCode, @Nullable String errorMessage) {
+ return new AppSearchResult<>(resultCode, /*resultValue=*/ null, errorMessage);
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java
new file mode 100644
index 000000000000..4b0b41b1a282
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java
@@ -0,0 +1,359 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.android.icing.proto.PropertyConfigProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.android.icing.proto.TermMatchType;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Set;
+
+/**
+ * The AppSearch Schema for a particular type of document.
+ *
+ * <p>For example, an e-mail message or a music recording could be a schema type.
+ *
+ * <p>The schema consists of type information, properties, and config (like tokenization type).
+ *
+ * @hide
+ */
+public final class AppSearchSchema {
+ private final SchemaTypeConfigProto mProto;
+
+ private AppSearchSchema(SchemaTypeConfigProto proto) {
+ mProto = proto;
+ }
+
+ /**
+ * Returns the {@link SchemaTypeConfigProto} populated by this builder.
+ * @hide
+ */
+ @NonNull
+ @VisibleForTesting
+ public SchemaTypeConfigProto getProto() {
+ return mProto;
+ }
+
+ @Override
+ public String toString() {
+ return mProto.toString();
+ }
+
+ /** Builder for {@link AppSearchSchema objects}. */
+ public static final class Builder {
+ private final SchemaTypeConfigProto.Builder mProtoBuilder =
+ SchemaTypeConfigProto.newBuilder();
+
+ /** Creates a new {@link AppSearchSchema.Builder}. */
+ public Builder(@NonNull String typeName) {
+ mProtoBuilder.setSchemaType(typeName);
+ }
+
+ /** Adds a property to the given type. */
+ @NonNull
+ public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) {
+ mProtoBuilder.addProperties(propertyConfig.mProto);
+ return this;
+ }
+
+ /**
+ * Constructs a new {@link AppSearchSchema} from the contents of this builder.
+ *
+ * <p>After calling this method, the builder must no longer be used.
+ */
+ @NonNull
+ public AppSearchSchema build() {
+ Set<String> propertyNames = new ArraySet<>();
+ for (PropertyConfigProto propertyConfigProto : mProtoBuilder.getPropertiesList()) {
+ if (!propertyNames.add(propertyConfigProto.getPropertyName())) {
+ throw new IllegalSchemaException(
+ "Property defined more than once: "
+ + propertyConfigProto.getPropertyName());
+ }
+ }
+ return new AppSearchSchema(mProtoBuilder.build());
+ }
+ }
+
+ /**
+ * Configuration for a single property (field) of a document type.
+ *
+ * <p>For example, an {@code EmailMessage} would be a type and the {@code subject} would be
+ * a property.
+ */
+ public static final class PropertyConfig {
+ /** Physical data-types of the contents of the property. */
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // com.google.android.icing.proto.PropertyConfigProto.DataType.Code.
+ @IntDef(prefix = {"DATA_TYPE_"}, value = {
+ DATA_TYPE_STRING,
+ DATA_TYPE_INT64,
+ DATA_TYPE_DOUBLE,
+ DATA_TYPE_BOOLEAN,
+ DATA_TYPE_BYTES,
+ DATA_TYPE_DOCUMENT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DataType {}
+
+ public static final int DATA_TYPE_STRING = 1;
+ public static final int DATA_TYPE_INT64 = 2;
+ public static final int DATA_TYPE_DOUBLE = 3;
+ public static final int DATA_TYPE_BOOLEAN = 4;
+
+ /** Unstructured BLOB. */
+ public static final int DATA_TYPE_BYTES = 5;
+
+ /**
+ * Indicates that the property itself is an Document, making it part a hierarchical
+ * Document schema. Any property using this DataType MUST have a valid
+ * {@code schemaType}.
+ */
+ public static final int DATA_TYPE_DOCUMENT = 6;
+
+ /** The cardinality of the property (whether it is required, optional or repeated). */
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // com.google.android.icing.proto.PropertyConfigProto.Cardinality.Code.
+ @IntDef(prefix = {"CARDINALITY_"}, value = {
+ CARDINALITY_REPEATED,
+ CARDINALITY_OPTIONAL,
+ CARDINALITY_REQUIRED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Cardinality {}
+
+ /** Any number of items (including zero) [0...*]. */
+ public static final int CARDINALITY_REPEATED = 1;
+
+ /** Zero or one value [0,1]. */
+ public static final int CARDINALITY_OPTIONAL = 2;
+
+ /** Exactly one value [1]. */
+ public static final int CARDINALITY_REQUIRED = 3;
+
+ /** Encapsulates the configurations on how AppSearch should query/index these terms. */
+ @IntDef(prefix = {"INDEXING_TYPE_"}, value = {
+ INDEXING_TYPE_NONE,
+ INDEXING_TYPE_EXACT_TERMS,
+ INDEXING_TYPE_PREFIXES,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface IndexingType {}
+
+ /**
+ * Content in this property will not be tokenized or indexed.
+ *
+ * <p>Useful if the data type is not made up of terms (e.g.
+ * {@link PropertyConfig#DATA_TYPE_DOCUMENT} or {@link PropertyConfig#DATA_TYPE_BYTES}
+ * type). All the properties inside the nested property won't be indexed regardless of the
+ * value of {@code indexingType} for the nested properties.
+ */
+ public static final int INDEXING_TYPE_NONE = 0;
+
+ /**
+ * Content in this property should only be returned for queries matching the exact tokens
+ * appearing in this property.
+ *
+ * <p>Ex. A property with "fool" should NOT match a query for "foo".
+ */
+ public static final int INDEXING_TYPE_EXACT_TERMS = 1;
+
+ /**
+ * Content in this property should be returned for queries that are either exact matches or
+ * query matches of the tokens appearing in this property.
+ *
+ * <p>Ex. A property with "fool" <b>should</b> match a query for "foo".
+ */
+ public static final int INDEXING_TYPE_PREFIXES = 2;
+
+ /** Configures how tokens should be extracted from this property. */
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // com.google.android.icing.proto.IndexingConfig.TokenizerType.Code.
+ @IntDef(prefix = {"TOKENIZER_TYPE_"}, value = {
+ TOKENIZER_TYPE_NONE,
+ TOKENIZER_TYPE_PLAIN,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TokenizerType {}
+
+ /**
+ * It is only valid for tokenizer_type to be 'NONE' if the data type is
+ * {@link PropertyConfig#DATA_TYPE_DOCUMENT}.
+ */
+ public static final int TOKENIZER_TYPE_NONE = 0;
+
+ /** Tokenization for plain text. */
+ public static final int TOKENIZER_TYPE_PLAIN = 1;
+
+ private final PropertyConfigProto mProto;
+
+ private PropertyConfig(PropertyConfigProto proto) {
+ mProto = proto;
+ }
+
+ @Override
+ public String toString() {
+ return mProto.toString();
+ }
+
+ /**
+ * Builder for {@link PropertyConfig}.
+ *
+ * <p>The following properties must be set, or {@link PropertyConfig} construction will
+ * fail:
+ * <ul>
+ * <li>dataType
+ * <li>cardinality
+ * </ul>
+ *
+ * <p>In addition, if {@code schemaType} is {@link #DATA_TYPE_DOCUMENT}, {@code schemaType}
+ * is also required.
+ */
+ public static final class Builder {
+ private final PropertyConfigProto.Builder mPropertyConfigProto =
+ PropertyConfigProto.newBuilder();
+ private final com.google.android.icing.proto.IndexingConfig.Builder
+ mIndexingConfigProto =
+ com.google.android.icing.proto.IndexingConfig.newBuilder();
+
+ /** Creates a new {@link PropertyConfig.Builder}. */
+ public Builder(@NonNull String propertyName) {
+ mPropertyConfigProto.setPropertyName(propertyName);
+ }
+
+ /**
+ * Type of data the property contains (e.g. string, int, bytes, etc).
+ *
+ * <p>This property must be set.
+ */
+ @NonNull
+ public PropertyConfig.Builder setDataType(@DataType int dataType) {
+ PropertyConfigProto.DataType.Code dataTypeProto =
+ PropertyConfigProto.DataType.Code.forNumber(dataType);
+ if (dataTypeProto == null) {
+ throw new IllegalArgumentException("Invalid dataType: " + dataType);
+ }
+ mPropertyConfigProto.setDataType(dataTypeProto);
+ return this;
+ }
+
+ /**
+ * The logical schema-type of the contents of this property.
+ *
+ * <p>Only required when {@link #setDataType(int)} is set to
+ * {@link #DATA_TYPE_DOCUMENT}. Otherwise, it is ignored.
+ */
+ @NonNull
+ public PropertyConfig.Builder setSchemaType(@NonNull String schemaType) {
+ mPropertyConfigProto.setSchemaType(schemaType);
+ return this;
+ }
+
+ /**
+ * The cardinality of the property (whether it is optional, required or repeated).
+ *
+ * <p>This property must be set.
+ */
+ @NonNull
+ public PropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+ PropertyConfigProto.Cardinality.Code cardinalityProto =
+ PropertyConfigProto.Cardinality.Code.forNumber(cardinality);
+ if (cardinalityProto == null) {
+ throw new IllegalArgumentException("Invalid cardinality: " + cardinality);
+ }
+ mPropertyConfigProto.setCardinality(cardinalityProto);
+ return this;
+ }
+
+ /**
+ * Configures how a property should be indexed so that it can be retrieved by queries.
+ */
+ @NonNull
+ public PropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
+ TermMatchType.Code termMatchTypeProto;
+ switch (indexingType) {
+ case INDEXING_TYPE_NONE:
+ termMatchTypeProto = TermMatchType.Code.UNKNOWN;
+ break;
+ case INDEXING_TYPE_EXACT_TERMS:
+ termMatchTypeProto = TermMatchType.Code.EXACT_ONLY;
+ break;
+ case INDEXING_TYPE_PREFIXES:
+ termMatchTypeProto = TermMatchType.Code.PREFIX;
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid indexingType: " + indexingType);
+ }
+ mIndexingConfigProto.setTermMatchType(termMatchTypeProto);
+ return this;
+ }
+
+ /** Configures how this property should be tokenized (split into words). */
+ @NonNull
+ public PropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) {
+ com.google.android.icing.proto.IndexingConfig.TokenizerType.Code
+ tokenizerTypeProto =
+ com.google.android.icing.proto.IndexingConfig
+ .TokenizerType.Code.forNumber(tokenizerType);
+ if (tokenizerTypeProto == null) {
+ throw new IllegalArgumentException("Invalid tokenizerType: " + tokenizerType);
+ }
+ mIndexingConfigProto.setTokenizerType(tokenizerTypeProto);
+ return this;
+ }
+
+ /**
+ * Constructs a new {@link PropertyConfig} from the contents of this builder.
+ *
+ * <p>After calling this method, the builder must no longer be used.
+ *
+ * @throws IllegalSchemaException If the property is not correctly populated (e.g.
+ * missing {@code dataType}).
+ */
+ @NonNull
+ public PropertyConfig build() {
+ mPropertyConfigProto.setIndexingConfig(mIndexingConfigProto);
+ // TODO(b/147692920): Send the schema to Icing Lib for official validation, instead
+ // of partially reimplementing some of the validation Icing does here.
+ if (mPropertyConfigProto.getDataType()
+ == PropertyConfigProto.DataType.Code.UNKNOWN) {
+ throw new IllegalSchemaException("Missing field: dataType");
+ }
+ if (mPropertyConfigProto.getSchemaType().isEmpty()
+ && mPropertyConfigProto.getDataType()
+ == PropertyConfigProto.DataType.Code.DOCUMENT) {
+ throw new IllegalSchemaException(
+ "Missing field: schemaType (required for configs with "
+ + "dataType = DOCUMENT)");
+ }
+ if (mPropertyConfigProto.getCardinality()
+ == PropertyConfigProto.Cardinality.Code.UNKNOWN) {
+ throw new IllegalSchemaException("Missing field: cardinality");
+ }
+ return new PropertyConfig(mPropertyConfigProto.build());
+ }
+ }
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl
new file mode 100644
index 000000000000..68de4f068470
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl
@@ -0,0 +1,112 @@
+/**
+ * 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.appsearch;
+
+import com.android.internal.infra.AndroidFuture;
+
+parcelable AppSearchResult;
+parcelable AppSearchBatchResult;
+
+/** {@hide} */
+interface IAppSearchManager {
+ /**
+ * Sets the schema.
+ *
+ * @param schemaBytes Serialized SchemaProto.
+ * @param forceOverride Whether to apply the new schema even if it is incompatible. All
+ * incompatible documents will be deleted.
+ * @param callback {@link AndroidFuture}&lt;{@link AppSearchResult}&lt;{@link Void}&gt&gt;.
+ * The results of the call.
+ */
+ void setSchema(
+ in byte[] schemaBytes, boolean forceOverride, in AndroidFuture<AppSearchResult> callback);
+
+ /**
+ * Inserts documents into the index.
+ *
+ * @param documentsBytes {@link List}&lt;byte[]&gt; of serialized DocumentProtos.
+ * @param callback
+ * {@link AndroidFuture}&lt;{@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;&gt;.
+ * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise,
+ * {@code callback} will be completed with an
+ * {@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;
+ * where the keys are document URIs, and the values are {@code null}.
+ */
+ void putDocuments(in List documentsBytes, in AndroidFuture<AppSearchBatchResult> callback);
+
+ /**
+ * Retrieves documents from the index.
+ *
+ * @param uris The URIs of the documents to retrieve
+ * @param callback
+ * {@link AndroidFuture}&lt;{@link AppSearchBatchResult}&lt;{@link String}, {@link byte[]}&gt;&gt;.
+ * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise,
+ * {@code callback} will be completed with an
+ * {@link AppSearchBatchResult}&lt;{@link String}, {@link byte[]}&gt;
+ * where the keys are document URIs, and the values are serialized Document protos.
+ */
+ void getDocuments(in List<String> uris, in AndroidFuture<AppSearchBatchResult> callback);
+
+ /**
+ * Searches a document based on a given specifications.
+ *
+ * @param searchSpecBytes Serialized SearchSpecProto.
+ * @param resultSpecBytes Serialized SearchResultsProto.
+ * @param scoringSpecBytes Serialized ScoringSpecProto.
+ * @param callback {@link AndroidFuture}&lt;{@link AppSearchResult}&lt;{@link byte[]}&gt;&gt;
+ * Will be completed with a serialized {@link SearchResultsProto}.
+ */
+ void query(
+ in byte[] searchSpecBytes, in byte[] resultSpecBytes, in byte[] scoringSpecBytes,
+ in AndroidFuture<AppSearchResult> callback);
+
+ /**
+ * Deletes documents by URI.
+ *
+ * @param uris The URIs of the documents to delete
+ * @param callback
+ * {@link AndroidFuture}&lt;{@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;&gt;.
+ * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise,
+ * {@code callback} will be completed with an
+ * {@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;
+ * where the keys are document URIs. If a document doesn't exist, it will be reported as a
+ * failure where the {@code throwable} is {@code null}.
+ */
+ void delete(in List<String> uris, in AndroidFuture<AppSearchBatchResult> callback);
+
+ /**
+ * Deletes documents by schema type.
+ *
+ * @param schemaTypes The schema types of the documents to delete
+ * @param callback
+ * {@link AndroidFuture}&lt;{@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;&gt;.
+ * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise,
+ * {@code callback} will be completed with an
+ * {@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;
+ * where the keys are schema types. If a schema type doesn't exist, it will be reported as a
+ * failure where the {@code throwable} is {@code null}.
+ */
+ void deleteByTypes(
+ in List<String> schemaTypes, in AndroidFuture<AppSearchBatchResult> callback);
+
+ /**
+ * Deletes all documents belonging to the calling app.
+ *
+ * @param callback {@link AndroidFuture}&lt;{@link AppSearchResult}&lt;{@link Void}&gt;&gt;.
+ * Will be completed with the result of the call.
+ */
+ void deleteAll(in AndroidFuture<AppSearchResult> callback);
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/IllegalSchemaException.java b/apex/appsearch/framework/java/android/app/appsearch/IllegalSchemaException.java
new file mode 100644
index 000000000000..f9e528cd2951
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/IllegalSchemaException.java
@@ -0,0 +1,36 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.NonNull;
+
+/**
+ * Indicates that a {@link android.app.appsearch.AppSearchSchema} has logical inconsistencies such
+ * as unpopulated mandatory fields or illegal combinations of parameters.
+ *
+ * @hide
+ */
+public class IllegalSchemaException extends IllegalArgumentException {
+ /**
+ * Constructs a new {@link IllegalSchemaException}.
+ *
+ * @param message A developer-readable description of the issue with the bundle.
+ */
+ public IllegalSchemaException(@NonNull String message) {
+ super(message);
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/IllegalSearchSpecException.java b/apex/appsearch/framework/java/android/app/appsearch/IllegalSearchSpecException.java
new file mode 100644
index 000000000000..0d029f029ee5
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/IllegalSearchSpecException.java
@@ -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.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.NonNull;
+
+/**
+ * Indicates that a {@link android.app.appsearch.SearchResults} has logical inconsistencies such
+ * as unpopulated mandatory fields or illegal combinations of parameters.
+ *
+ * @hide
+ */
+public class IllegalSearchSpecException extends IllegalArgumentException {
+ /**
+ * Constructs a new {@link IllegalSearchSpecException}.
+ *
+ * @param message A developer-readable description of the issue with the bundle.
+ */
+ public IllegalSearchSpecException(@NonNull String message) {
+ super(message);
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/MatchInfo.java b/apex/appsearch/framework/java/android/app/appsearch/MatchInfo.java
new file mode 100644
index 000000000000..5ce296082d70
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/MatchInfo.java
@@ -0,0 +1,182 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.NonNull;
+import android.util.Range;
+
+import com.google.android.icing.proto.SnippetMatchProto;
+
+/**
+ * Snippet: It refers to a substring of text from the content of document that is returned as a
+ * part of search result.
+ * This class represents a match objects for any Snippets that might be present in
+ * {@link SearchResults} from query. Using this class user can get the full text, exact matches and
+ * Snippets of document content for a given match.
+ *
+ * <p>Class Example 1:
+ * A document contains following text in property subject:
+ * <p>A commonly used fake word is foo. Another nonsense word that’s used a lot is bar.
+ *
+ * <p>If the queryExpression is "foo".
+ *
+ * <p>{@link MatchInfo#getPropertyPath()} returns "subject"
+ * <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another nonsense
+ * word that’s used a lot is bar."
+ * <p>{@link MatchInfo#getExactMatchPosition()} returns [29, 32]
+ * <p>{@link MatchInfo#getExactMatch()} returns "foo"
+ * <p>{@link MatchInfo#getSnippetPosition()} returns [29, 41]
+ * <p>{@link MatchInfo#getSnippet()} returns "is foo. Another"
+ * <p>
+ * <p>Class Example 2:
+ * A document contains a property name sender which contains 2 property names name and email, so
+ * we will have 2 property paths: {@code sender.name} and {@code sender.email}.
+ * <p> Let {@code sender.name = "Test Name Jr."} and {@code sender.email = "TestNameJr@gmail.com"}
+ *
+ * <p>If the queryExpression is "Test". We will have 2 matches.
+ *
+ * <p> Match-1
+ * <p>{@link MatchInfo#getPropertyPath()} returns "sender.name"
+ * <p>{@link MatchInfo#getFullText()} returns "Test Name Jr."
+ * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 4]
+ * <p>{@link MatchInfo#getExactMatch()} returns "Test"
+ * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 9]
+ * <p>{@link MatchInfo#getSnippet()} returns "Test Name Jr."
+ * <p> Match-2
+ * <p>{@link MatchInfo#getPropertyPath()} returns "sender.email"
+ * <p>{@link MatchInfo#getFullText()} returns "TestNameJr@gmail.com"
+ * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 20]
+ * <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com"
+ * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 20]
+ * <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com"
+ * @hide
+ */
+// TODO(sidchhabra): Capture real snippet after integration with icingLib.
+public final class MatchInfo {
+
+ private final String mPropertyPath;
+ private final SnippetMatchProto mSnippetMatch;
+ private final AppSearchDocument mDocument;
+ /**
+ * List of content with same property path in a document when there are multiple matches in
+ * repeated sections.
+ */
+ private final String[] mValues;
+
+ /** @hide */
+ public MatchInfo(@NonNull String propertyPath, @NonNull SnippetMatchProto snippetMatch,
+ @NonNull AppSearchDocument document) {
+ mPropertyPath = propertyPath;
+ mSnippetMatch = snippetMatch;
+ mDocument = document;
+ // In IcingLib snippeting is available for only 3 data types i.e String, double and long,
+ // so we need to check which of these three are requested.
+ // TODO (sidchhabra): getPropertyStringArray takes property name, handle for property path.
+ String[] values = mDocument.getPropertyStringArray(propertyPath);
+ if (values == null) {
+ values = doubleToString(mDocument.getPropertyDoubleArray(propertyPath));
+ }
+ if (values == null) {
+ values = longToString(mDocument.getPropertyLongArray(propertyPath));
+ }
+ if (values == null) {
+ throw new IllegalStateException("No content found for requested property path!");
+ }
+ mValues = values;
+ }
+
+ /**
+ * Gets the property path corresponding to the given entry.
+ * <p>Property Path: '.' - delimited sequence of property names indicating which property in
+ * the Document these snippets correspond to.
+ * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc.
+ * For class example 1 this returns "subject"
+ */
+ @NonNull
+ public String getPropertyPath() {
+ return mPropertyPath;
+ }
+
+ /**
+ * Gets the full text corresponding to the given entry.
+ * <p>For class example this returns "A commonly used fake word is foo. Another nonsense word
+ * that’s used a lot is bar."
+ */
+ @NonNull
+ public String getFullText() {
+ return mValues[mSnippetMatch.getValuesIndex()];
+ }
+
+ /**
+ * Gets the exact match range corresponding to the given entry.
+ * <p>For class example 1 this returns [29, 32]
+ */
+ @NonNull
+ public Range getExactMatchPosition() {
+ return new Range(mSnippetMatch.getExactMatchPosition(),
+ mSnippetMatch.getExactMatchPosition() + mSnippetMatch.getExactMatchBytes());
+ }
+
+ /**
+ * Gets the exact match corresponding to the given entry.
+ * <p>For class example 1 this returns "foo"
+ */
+ @NonNull
+ public CharSequence getExactMatch() {
+ return getSubstring(getExactMatchPosition());
+ }
+
+ /**
+ * Gets the snippet range corresponding to the given entry.
+ * <p>For class example 1 this returns [29, 41]
+ */
+ @NonNull
+ public Range getSnippetPosition() {
+ return new Range(mSnippetMatch.getWindowPosition(),
+ mSnippetMatch.getWindowPosition() + mSnippetMatch.getWindowBytes());
+ }
+
+ /**
+ * Gets the snippet corresponding to the given entry.
+ * <p>Snippet - Provides a subset of the content to display. The
+ * length of this content can be changed {@link SearchSpec.Builder#setMaxSnippetSize(int)}.
+ * Windowing is centered around the middle of the matched token with content on either side
+ * clipped to token boundaries.
+ * <p>For class example 1 this returns "foo. Another"
+ */
+ @NonNull
+ public CharSequence getSnippet() {
+ return getSubstring(getSnippetPosition());
+ }
+
+ private CharSequence getSubstring(Range range) {
+ return getFullText()
+ .substring((int) range.getLower(), (int) range.getUpper());
+ }
+
+ /** Utility method to convert double[] to String[] */
+ private String[] doubleToString(double[] values) {
+ //TODO(sidchhabra): Implement the method.
+ return null;
+ }
+
+ /** Utility method to convert long[] to String[] */
+ private String[] longToString(long[] values) {
+ //TODO(sidchhabra): Implement the method.
+ return null;
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java
new file mode 100644
index 000000000000..7287fe68f519
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java
@@ -0,0 +1,128 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.google.android.icing.proto.SearchResultProto;
+import com.google.android.icing.proto.SnippetMatchProto;
+import com.google.android.icing.proto.SnippetProto;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+/**
+ * SearchResults are a list of results that are returned from a query. Each result from this
+ * list contains a document and may contain other fields like snippets based on request.
+ * This iterator class is not thread safe.
+ * @hide
+ */
+public final class SearchResults implements Iterator<SearchResults.Result> {
+
+ private final SearchResultProto mSearchResultProto;
+ private int mNextIdx;
+
+ /** @hide */
+ public SearchResults(SearchResultProto searchResultProto) {
+ mSearchResultProto = searchResultProto;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mNextIdx < mSearchResultProto.getResultsCount();
+ }
+
+ @NonNull
+ @Override
+ public Result next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ Result result = new Result(mSearchResultProto.getResults(mNextIdx));
+ mNextIdx++;
+ return result;
+ }
+
+
+
+ /**
+ * This class represents the result obtained from the query. It will contain the document which
+ * which matched the specified query string and specifications.
+ * @hide
+ */
+ public static final class Result {
+ private final SearchResultProto.ResultProto mResultProto;
+
+ @Nullable
+ private AppSearchDocument mDocument;
+
+ private Result(SearchResultProto.ResultProto resultProto) {
+ mResultProto = resultProto;
+ }
+
+ /**
+ * Contains the matching {@link AppSearchDocument}.
+ * @return Document object which matched the query.
+ * @hide
+ */
+ @NonNull
+ public AppSearchDocument getDocument() {
+ if (mDocument == null) {
+ mDocument = new AppSearchDocument(mResultProto.getDocument());
+ }
+ return mDocument;
+ }
+
+ /**
+ * Contains a list of Snippets that matched the request. Only populated when requested in
+ * {@link SearchSpec.Builder#setMaxSnippetSize(int)}.
+ * @return List of matches based on {@link SearchSpec}, if snippeting is disabled and this
+ * method is called it will return {@code null}. Users can also restrict snippet population
+ * using {@link SearchSpec.Builder#setNumToSnippet} and
+ * {@link SearchSpec.Builder#setNumMatchesPerProperty}, for all results after that value
+ * this method will return {@code null}.
+ * @hide
+ */
+ // TODO(sidchhabra): Replace Document with proper constructor.
+ @Nullable
+ public List<MatchInfo> getMatchInfo() {
+ if (!mResultProto.hasSnippet()) {
+ return null;
+ }
+ AppSearchDocument document = getDocument();
+ List<MatchInfo> matchList = new ArrayList<>();
+ for (Iterator entryProtoIterator = mResultProto.getSnippet()
+ .getEntriesList().iterator(); entryProtoIterator.hasNext(); ) {
+ SnippetProto.EntryProto entry = (SnippetProto.EntryProto) entryProtoIterator.next();
+ for (Iterator snippetMatchProtoIterator = entry.getSnippetMatchesList().iterator();
+ snippetMatchProtoIterator.hasNext(); ) {
+ matchList.add(new MatchInfo(entry.getPropertyName(),
+ (SnippetMatchProto) snippetMatchProtoIterator.next(), document));
+ }
+ }
+ return matchList;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return mSearchResultProto.toString();
+ }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java
new file mode 100644
index 000000000000..c276ae1fe45e
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java
@@ -0,0 +1,260 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+
+import com.google.android.icing.proto.ResultSpecProto;
+import com.google.android.icing.proto.ScoringSpecProto;
+import com.google.android.icing.proto.SearchSpecProto;
+import com.google.android.icing.proto.TermMatchType;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
+/**
+ * This class represents the specification logic for AppSearch. It can be used to set the type of
+ * search, like prefix or exact only or apply filters to search for a specific schema type only etc.
+ * @hide
+ */
+// TODO(sidchhabra) : AddResultSpec fields for Snippets etc.
+public final class SearchSpec {
+
+ private final SearchSpecProto mSearchSpecProto;
+ private final ResultSpecProto mResultSpecProto;
+ private final ScoringSpecProto mScoringSpecProto;
+
+ private SearchSpec(@NonNull SearchSpecProto searchSpecProto,
+ @NonNull ResultSpecProto resultSpecProto, @NonNull ScoringSpecProto scoringSpecProto) {
+ mSearchSpecProto = searchSpecProto;
+ mResultSpecProto = resultSpecProto;
+ mScoringSpecProto = scoringSpecProto;
+ }
+
+ /** Creates a new {@link SearchSpec.Builder}. */
+ @NonNull
+ public static SearchSpec.Builder newBuilder() {
+ return new SearchSpec.Builder();
+ }
+
+ /** @hide */
+ @NonNull
+ SearchSpecProto getSearchSpecProto() {
+ return mSearchSpecProto;
+ }
+
+ /** @hide */
+ @NonNull
+ ResultSpecProto getResultSpecProto() {
+ return mResultSpecProto;
+ }
+
+ /** @hide */
+ @NonNull
+ ScoringSpecProto getScoringSpecProto() {
+ return mScoringSpecProto;
+ }
+
+ /** Term Match Type for the query. */
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // {@link com.google.android.icing.proto.SearchSpecProto.termMatchType}
+ @IntDef(prefix = {"TERM_MATCH_TYPE_"}, value = {
+ TERM_MATCH_TYPE_EXACT_ONLY,
+ TERM_MATCH_TYPE_PREFIX
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TermMatchTypeCode {}
+
+ /**
+ * Query terms will only match exact tokens in the index.
+ * <p>Ex. A query term "foo" will only match indexed token "foo", and not "foot" or "football".
+ */
+ public static final int TERM_MATCH_TYPE_EXACT_ONLY = 1;
+ /**
+ * Query terms will match indexed tokens when the query term is a prefix of the token.
+ * <p>Ex. A query term "foo" will match indexed tokens like "foo", "foot", and "football".
+ */
+ public static final int TERM_MATCH_TYPE_PREFIX = 2;
+
+ /** Ranking Strategy for query result.*/
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // {@link ScoringSpecProto.RankingStrategy.Code }
+ @IntDef(prefix = {"RANKING_STRATEGY_"}, value = {
+ RANKING_STRATEGY_NONE,
+ RANKING_STRATEGY_DOCUMENT_SCORE,
+ RANKING_STRATEGY_CREATION_TIMESTAMP
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RankingStrategyCode {}
+
+ /** No Ranking, results are returned in arbitrary order.*/
+ public static final int RANKING_STRATEGY_NONE = 0;
+ /** Ranked by app-provided document scores. */
+ public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1;
+ /** Ranked by document creation timestamps. */
+ public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2;
+
+ /** Order for query result.*/
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // {@link ScoringSpecProto.Order.Code }
+ @IntDef(prefix = {"ORDER_"}, value = {
+ ORDER_DESCENDING,
+ ORDER_ASCENDING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OrderCode {}
+
+ /** Search results will be returned in a descending order. */
+ public static final int ORDER_DESCENDING = 0;
+ /** Search results will be returned in an ascending order. */
+ public static final int ORDER_ASCENDING = 1;
+
+ /** Builder for {@link SearchSpec objects}. */
+ public static final class Builder {
+
+ private final SearchSpecProto.Builder mSearchSpecBuilder = SearchSpecProto.newBuilder();
+ private final ResultSpecProto.Builder mResultSpecBuilder = ResultSpecProto.newBuilder();
+ private final ScoringSpecProto.Builder mScoringSpecBuilder = ScoringSpecProto.newBuilder();
+ private final ResultSpecProto.SnippetSpecProto.Builder mSnippetSpecBuilder =
+ ResultSpecProto.SnippetSpecProto.newBuilder();
+
+ private Builder() {
+ }
+
+ /**
+ * Indicates how the query terms should match {@link TermMatchTypeCode} in the index.
+ */
+ @NonNull
+ public Builder setTermMatchType(@TermMatchTypeCode int termMatchTypeCode) {
+ TermMatchType.Code termMatchTypeCodeProto =
+ TermMatchType.Code.forNumber(termMatchTypeCode);
+ if (termMatchTypeCodeProto == null) {
+ throw new IllegalArgumentException("Invalid term match type: "
+ + termMatchTypeCode);
+ }
+ mSearchSpecBuilder.setTermMatchType(termMatchTypeCodeProto);
+ return this;
+ }
+
+ /**
+ * Adds a Schema type filter to {@link SearchSpec} Entry. Only search for documents that
+ * have the specified schema types.
+ * <p>If unset, the query will search over all schema types.
+ */
+ @NonNull
+ public Builder setSchemaTypes(@NonNull String... schemaTypes) {
+ for (String schemaType : schemaTypes) {
+ mSearchSpecBuilder.addSchemaTypeFilters(schemaType);
+ }
+ return this;
+ }
+
+ /** Sets the maximum number of results to retrieve from the query */
+ @NonNull
+ public SearchSpec.Builder setNumToRetrieve(int numToRetrieve) {
+ mResultSpecBuilder.setNumToRetrieve(numToRetrieve);
+ return this;
+ }
+
+ /** Sets ranking strategy for AppSearch results.*/
+ @NonNull
+ public Builder setRankingStrategy(@RankingStrategyCode int rankingStrategy) {
+ ScoringSpecProto.RankingStrategy.Code rankingStrategyCodeProto =
+ ScoringSpecProto.RankingStrategy.Code.forNumber(rankingStrategy);
+ if (rankingStrategyCodeProto == null) {
+ throw new IllegalArgumentException("Invalid result ranking strategy: "
+ + rankingStrategyCodeProto);
+ }
+ mScoringSpecBuilder.setRankBy(rankingStrategyCodeProto);
+ return this;
+ }
+
+ /**
+ * Indicates the order of returned search results, the default is DESC, meaning that results
+ * with higher scores come first.
+ * <p>This order field will be ignored if RankingStrategy = {@code RANKING_STRATEGY_NONE}.
+ */
+ @NonNull
+ public Builder setOrder(@OrderCode int order) {
+ ScoringSpecProto.Order.Code orderCodeProto =
+ ScoringSpecProto.Order.Code.forNumber(order);
+ if (orderCodeProto == null) {
+ throw new IllegalArgumentException("Invalid result ranking order: "
+ + orderCodeProto);
+ }
+ mScoringSpecBuilder.setOrderBy(orderCodeProto);
+ return this;
+ }
+
+ /**
+ * Only the first {@code numToSnippet} documents based on the ranking strategy
+ * will have snippet information provided.
+ * <p>If set to 0 (default), snippeting is disabled and
+ * {@link SearchResults.Result#getMatchInfo} will return {@code null} for that result.
+ */
+ @NonNull
+ public SearchSpec.Builder setNumToSnippet(int numToSnippet) {
+ mSnippetSpecBuilder.setNumToSnippet(numToSnippet);
+ return this;
+ }
+
+ /**
+ * Only the first {@code numMatchesPerProperty} matches for a every property of
+ * {@link AppSearchDocument} will contain snippet information.
+ * <p>If set to 0, snippeting is disabled and {@link SearchResults.Result#getMatchInfo}
+ * will return {@code null} for that result.
+ */
+ @NonNull
+ public SearchSpec.Builder setNumMatchesPerProperty(int numMatchesPerProperty) {
+ mSnippetSpecBuilder.setNumMatchesPerProperty(numMatchesPerProperty);
+ return this;
+ }
+
+ /**
+ * Sets {@code maxSnippetSize}, the maximum snippet size. Snippet windows start at
+ * {@code maxSnippetSize/2} bytes before the middle of the matching token and end at
+ * {@code maxSnippetSize/2} bytes after the middle of the matching token. It respects
+ * token boundaries, therefore the returned window may be smaller than requested.
+ * <p> Setting {@code maxSnippetSize} to 0 will disable windowing and an empty string will
+ * be returned. If matches enabled is also set to false, then snippeting is disabled.
+ * <p>Ex. {@code maxSnippetSize} = 16. "foo bar baz bat rat" with a query of "baz" will
+ * return a window of "bar baz bat" which is only 11 bytes long.
+ */
+ @NonNull
+ public SearchSpec.Builder setMaxSnippetSize(int maxSnippetSize) {
+ mSnippetSpecBuilder.setMaxWindowBytes(maxSnippetSize);
+ return this;
+ }
+
+ /**
+ * Constructs a new {@link SearchSpec} from the contents of this builder.
+ *
+ * <p>After calling this method, the builder must no longer be used.
+ */
+ @NonNull
+ public SearchSpec build() {
+ if (mSearchSpecBuilder.getTermMatchType() == TermMatchType.Code.UNKNOWN) {
+ throw new IllegalSearchSpecException("Missing termMatchType field.");
+ }
+ mResultSpecBuilder.setSnippetSpec(mSnippetSpecBuilder);
+ return new SearchSpec(mSearchSpecBuilder.build(), mResultSpecBuilder.build(),
+ mScoringSpecBuilder.build());
+ }
+ }
+}
diff --git a/apex/appsearch/service/Android.bp b/apex/appsearch/service/Android.bp
new file mode 100644
index 000000000000..c125f5686f0b
--- /dev/null
+++ b/apex/appsearch/service/Android.bp
@@ -0,0 +1,26 @@
+// 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-appsearch",
+ installable: true,
+ srcs: ["java/**/*.java"],
+ libs: [
+ "framework",
+ "framework-appsearch",
+ "services.core",
+ ],
+ static_libs: ["icing-java-proto-lite"],
+ jarjar_rules: "jarjar-rules.txt",
+ apex_available: ["com.android.appsearch"],
+}
diff --git a/apex/appsearch/service/jarjar-rules.txt b/apex/appsearch/service/jarjar-rules.txt
new file mode 100644
index 000000000000..c48e8327fae8
--- /dev/null
+++ b/apex/appsearch/service/jarjar-rules.txt
@@ -0,0 +1,2 @@
+rule com.google.protobuf.** com.android.server.appsearch.protobuf.@1
+rule com.google.android.icing.proto.** com.android.server.appsearch.proto.@1
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchException.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchException.java
new file mode 100644
index 000000000000..9b705ceb80de
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchException.java
@@ -0,0 +1,57 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchResult;
+
+/**
+ * An exception thrown by {@link com.android.server.appsearch.AppSearchManagerService} or a
+ * subcomponent.
+ *
+ * <p>These exceptions can be converted into a failed {@link android.app.appsearch.AppSearchResult}
+ * for propagating to the client.
+ */
+public class AppSearchException extends Exception {
+ private final @AppSearchResult.ResultCode int mResultCode;
+
+ /** Initializes an {@link com.android.server.appsearch.AppSearchException} with no message. */
+ public AppSearchException(@AppSearchResult.ResultCode int resultCode) {
+ this(resultCode, /*message=*/ null);
+ }
+
+ public AppSearchException(
+ @AppSearchResult.ResultCode int resultCode, @Nullable String message) {
+ this(resultCode, message, /*cause=*/ null);
+ }
+
+ public AppSearchException(
+ @AppSearchResult.ResultCode int resultCode,
+ @Nullable String message,
+ @Nullable Throwable cause) {
+ super(message, cause);
+ mResultCode = resultCode;
+ }
+
+ /**
+ * Converts this {@link java.lang.Exception} into a failed
+ * {@link android.app.appsearch.AppSearchResult}
+ */
+ public <T> AppSearchResult<T> toAppSearchResult() {
+ return AppSearchResult.newFailedResult(mResultCode, getMessage());
+ }
+}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
new file mode 100644
index 000000000000..16948b257392
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
@@ -0,0 +1,287 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.IAppSearchManager;
+import android.content.Context;
+import android.os.Binder;
+import android.os.UserHandle;
+
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.util.Preconditions;
+import com.android.server.SystemService;
+import com.android.server.appsearch.impl.AppSearchImpl;
+import com.android.server.appsearch.impl.ImplInstanceManager;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.ResultSpecProto;
+import com.google.android.icing.proto.SchemaProto;
+import com.google.android.icing.proto.ScoringSpecProto;
+import com.google.android.icing.proto.SearchResultProto;
+import com.google.android.icing.proto.SearchSpecProto;
+import com.google.android.icing.proto.StatusProto;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * TODO(b/142567528): add comments when implement this class
+ */
+public class AppSearchManagerService extends SystemService {
+
+ public AppSearchManagerService(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(Context.APP_SEARCH_SERVICE, new Stub());
+ }
+
+ private class Stub extends IAppSearchManager.Stub {
+ @Override
+ public void setSchema(
+ @NonNull byte[] schemaBytes,
+ boolean forceOverride,
+ @NonNull AndroidFuture<AppSearchResult> callback) {
+ Preconditions.checkNotNull(schemaBytes);
+ Preconditions.checkNotNull(callback);
+ int callingUid = Binder.getCallingUidOrThrow();
+ int callingUserId = UserHandle.getUserId(callingUid);
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ SchemaProto schema = SchemaProto.parseFrom(schemaBytes);
+ AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ impl.setSchema(callingUid, schema, forceOverride);
+ callback.complete(AppSearchResult.newSuccessfulResult(/*value=*/ null));
+ } catch (Throwable t) {
+ callback.complete(throwableToFailedResult(t));
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ @Override
+ public void putDocuments(
+ @NonNull List documentsBytes,
+ @NonNull AndroidFuture<AppSearchBatchResult> callback) {
+ Preconditions.checkNotNull(documentsBytes);
+ Preconditions.checkNotNull(callback);
+ int callingUid = Binder.getCallingUidOrThrow();
+ int callingUserId = UserHandle.getUserId(callingUid);
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ AppSearchBatchResult.Builder<String, Void> resultBuilder =
+ new AppSearchBatchResult.Builder<>();
+ for (int i = 0; i < documentsBytes.size(); i++) {
+ byte[] documentBytes = (byte[]) documentsBytes.get(i);
+ DocumentProto document = DocumentProto.parseFrom(documentBytes);
+ try {
+ impl.putDocument(callingUid, document);
+ resultBuilder.setSuccess(document.getUri(), /*value=*/ null);
+ } catch (Throwable t) {
+ resultBuilder.setResult(document.getUri(), throwableToFailedResult(t));
+ }
+ }
+ callback.complete(resultBuilder.build());
+ } catch (Throwable t) {
+ callback.completeExceptionally(t);
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ @Override
+ public void getDocuments(
+ @NonNull List<String> uris, @NonNull AndroidFuture<AppSearchBatchResult> callback) {
+ Preconditions.checkNotNull(uris);
+ Preconditions.checkNotNull(callback);
+ int callingUid = Binder.getCallingUidOrThrow();
+ int callingUserId = UserHandle.getUserId(callingUid);
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ AppSearchBatchResult.Builder<String, byte[]> resultBuilder =
+ new AppSearchBatchResult.Builder<>();
+ for (int i = 0; i < uris.size(); i++) {
+ String uri = uris.get(i);
+ try {
+ DocumentProto document = impl.getDocument(callingUid, uri);
+ if (document == null) {
+ resultBuilder.setFailure(
+ uri, AppSearchResult.RESULT_NOT_FOUND, /*errorMessage=*/ null);
+ } else {
+ resultBuilder.setSuccess(uri, document.toByteArray());
+ }
+ } catch (Throwable t) {
+ resultBuilder.setResult(uri, throwableToFailedResult(t));
+ }
+ }
+ callback.complete(resultBuilder.build());
+ } catch (Throwable t) {
+ callback.completeExceptionally(t);
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ // TODO(sidchhabra): Do this in a threadpool.
+ @Override
+ public void query(
+ @NonNull byte[] searchSpecBytes,
+ @NonNull byte[] resultSpecBytes,
+ @NonNull byte[] scoringSpecBytes,
+ @NonNull AndroidFuture<AppSearchResult> callback) {
+ Preconditions.checkNotNull(searchSpecBytes);
+ Preconditions.checkNotNull(resultSpecBytes);
+ Preconditions.checkNotNull(scoringSpecBytes);
+ Preconditions.checkNotNull(callback);
+ int callingUid = Binder.getCallingUidOrThrow();
+ int callingUserId = UserHandle.getUserId(callingUid);
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ SearchSpecProto searchSpecProto = SearchSpecProto.parseFrom(searchSpecBytes);
+ ResultSpecProto resultSpecProto = ResultSpecProto.parseFrom(resultSpecBytes);
+ ScoringSpecProto scoringSpecProto = ScoringSpecProto.parseFrom(scoringSpecBytes);
+ AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ SearchResultProto searchResultProto =
+ impl.query(callingUid, searchSpecProto, resultSpecProto, scoringSpecProto);
+ // TODO(sidchhabra): Translate SearchResultProto errors into error codes. This might
+ // better be done in AppSearchImpl by throwing an AppSearchException.
+ if (searchResultProto.getStatus().getCode() != StatusProto.Code.OK) {
+ callback.complete(
+ AppSearchResult.newFailedResult(
+ AppSearchResult.RESULT_INTERNAL_ERROR,
+ searchResultProto.getStatus().getMessage()));
+ } else {
+ callback.complete(
+ AppSearchResult.newSuccessfulResult(searchResultProto.toByteArray()));
+ }
+ } catch (Throwable t) {
+ callback.complete(throwableToFailedResult(t));
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ @Override
+ public void delete(List<String> uris, AndroidFuture<AppSearchBatchResult> callback) {
+ Preconditions.checkNotNull(uris);
+ Preconditions.checkNotNull(callback);
+ int callingUid = Binder.getCallingUidOrThrow();
+ int callingUserId = UserHandle.getUserId(callingUid);
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ AppSearchBatchResult.Builder<String, Void> resultBuilder =
+ new AppSearchBatchResult.Builder<>();
+ for (int i = 0; i < uris.size(); i++) {
+ String uri = uris.get(i);
+ try {
+ if (!impl.delete(callingUid, uri)) {
+ resultBuilder.setFailure(
+ uri, AppSearchResult.RESULT_NOT_FOUND, /*errorMessage=*/ null);
+ } else {
+ resultBuilder.setSuccess(uri, /*value= */null);
+ }
+ } catch (Throwable t) {
+ resultBuilder.setResult(uri, throwableToFailedResult(t));
+ }
+ }
+ callback.complete(resultBuilder.build());
+ } catch (Throwable t) {
+ callback.completeExceptionally(t);
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ @Override
+ public void deleteByTypes(
+ List<String> schemaTypes, AndroidFuture<AppSearchBatchResult> callback) {
+ Preconditions.checkNotNull(schemaTypes);
+ Preconditions.checkNotNull(callback);
+ int callingUid = Binder.getCallingUidOrThrow();
+ int callingUserId = UserHandle.getUserId(callingUid);
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ AppSearchBatchResult.Builder<String, Void> resultBuilder =
+ new AppSearchBatchResult.Builder<>();
+ for (int i = 0; i < schemaTypes.size(); i++) {
+ String schemaType = schemaTypes.get(i);
+ try {
+ if (!impl.deleteByType(callingUid, schemaType)) {
+ resultBuilder.setFailure(
+ schemaType,
+ AppSearchResult.RESULT_NOT_FOUND,
+ /*errorMessage=*/ null);
+ } else {
+ resultBuilder.setSuccess(schemaType, /*value=*/ null);
+ }
+ } catch (Throwable t) {
+ resultBuilder.setResult(schemaType, throwableToFailedResult(t));
+ }
+ }
+ callback.complete(resultBuilder.build());
+ } catch (Throwable t) {
+ callback.completeExceptionally(t);
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ @Override
+ public void deleteAll(@NonNull AndroidFuture<AppSearchResult> callback) {
+ Preconditions.checkNotNull(callback);
+ int callingUid = Binder.getCallingUidOrThrow();
+ int callingUserId = UserHandle.getUserId(callingUid);
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ impl.deleteAll(callingUid);
+ callback.complete(AppSearchResult.newSuccessfulResult(null));
+ } catch (Throwable t) {
+ callback.complete(throwableToFailedResult(t));
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ private <ValueType> AppSearchResult<ValueType> throwableToFailedResult(
+ @NonNull Throwable t) {
+ if (t instanceof AppSearchException) {
+ return ((AppSearchException) t).toAppSearchResult();
+ }
+
+ @AppSearchResult.ResultCode int resultCode;
+ if (t instanceof IllegalStateException) {
+ resultCode = AppSearchResult.RESULT_INTERNAL_ERROR;
+ } else if (t instanceof IllegalArgumentException) {
+ resultCode = AppSearchResult.RESULT_INVALID_ARGUMENT;
+ } else if (t instanceof IOException) {
+ resultCode = AppSearchResult.RESULT_IO_ERROR;
+ } else {
+ resultCode = AppSearchResult.RESULT_UNKNOWN_ERROR;
+ }
+ return AppSearchResult.newFailedResult(resultCode, t.getMessage());
+ }
+ }
+}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/TEST_MAPPING b/apex/appsearch/service/java/com/android/server/appsearch/TEST_MAPPING
new file mode 100644
index 000000000000..ca5b8841ea49
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/TEST_MAPPING
@@ -0,0 +1,23 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsAppSearchTestCases"
+ },
+ {
+ "name": "FrameworksServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.appsearch"
+ }
+ ]
+ },
+ {
+ "name": "FrameworksCoreTests",
+ "options": [
+ {
+ "include-filter": "android.app.appsearch"
+ }
+ ]
+ }
+ ]
+}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/impl/AppSearchImpl.java b/apex/appsearch/service/java/com/android/server/appsearch/impl/AppSearchImpl.java
new file mode 100644
index 000000000000..4358d2086181
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/impl/AppSearchImpl.java
@@ -0,0 +1,332 @@
+/*
+ * 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.appsearch.impl;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.PropertyConfigProto;
+import com.google.android.icing.proto.PropertyProto;
+import com.google.android.icing.proto.ResultSpecProto;
+import com.google.android.icing.proto.SchemaProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.android.icing.proto.ScoringSpecProto;
+import com.google.android.icing.proto.SearchResultProto;
+import com.google.android.icing.proto.SearchSpecProto;
+
+import java.util.Set;
+
+/**
+ * Manages interaction with {@link FakeIcing} and other components to implement AppSearch
+ * functionality.
+ */
+public final class AppSearchImpl {
+ private final Context mContext;
+ private final @UserIdInt int mUserId;
+ private final FakeIcing mFakeIcing = new FakeIcing();
+
+ AppSearchImpl(@NonNull Context context, @UserIdInt int userId) {
+ mContext = context;
+ mUserId = userId;
+ }
+
+ /**
+ * Updates the AppSearch schema for this app.
+ *
+ * @param callingUid The uid of the app calling AppSearch.
+ * @param origSchema The schema to set for this app.
+ * @param forceOverride Whether to force-apply the schema even if it is incompatible. Documents
+ * which do not comply with the new schema will be deleted.
+ */
+ public void setSchema(int callingUid, @NonNull SchemaProto origSchema, boolean forceOverride) {
+ // Rewrite schema type names to include the calling app's package and uid.
+ String typePrefix = getTypePrefix(callingUid);
+ SchemaProto.Builder schemaBuilder = origSchema.toBuilder();
+ rewriteSchemaTypes(typePrefix, schemaBuilder);
+
+ // TODO(b/145635424): Save in schema type map
+ // TODO(b/145635424): Apply the schema to Icing and report results
+ }
+
+ /**
+ * Rewrites all types mentioned in the given {@code schemaBuilder} to prepend
+ * {@code typePrefix}.
+ *
+ * @param typePrefix The prefix to add
+ * @param schemaBuilder The schema to mutate
+ */
+ @VisibleForTesting
+ void rewriteSchemaTypes(
+ @NonNull String typePrefix, @NonNull SchemaProto.Builder schemaBuilder) {
+ for (int typeIdx = 0; typeIdx < schemaBuilder.getTypesCount(); typeIdx++) {
+ SchemaTypeConfigProto.Builder typeConfigBuilder =
+ schemaBuilder.getTypes(typeIdx).toBuilder();
+
+ // Rewrite SchemaProto.types.schema_type
+ String newSchemaType = typePrefix + typeConfigBuilder.getSchemaType();
+ typeConfigBuilder.setSchemaType(newSchemaType);
+
+ // Rewrite SchemaProto.types.properties.schema_type
+ for (int propertyIdx = 0;
+ propertyIdx < typeConfigBuilder.getPropertiesCount();
+ propertyIdx++) {
+ PropertyConfigProto.Builder propertyConfigBuilder =
+ typeConfigBuilder.getProperties(propertyIdx).toBuilder();
+ if (!propertyConfigBuilder.getSchemaType().isEmpty()) {
+ String newPropertySchemaType =
+ typePrefix + propertyConfigBuilder.getSchemaType();
+ propertyConfigBuilder.setSchemaType(newPropertySchemaType);
+ typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
+ }
+ }
+
+ schemaBuilder.setTypes(typeIdx, typeConfigBuilder);
+ }
+ }
+
+ /**
+ * Adds a document to the AppSearch index.
+ *
+ * @param callingUid The uid of the app calling AppSearch.
+ * @param origDocument The document to index.
+ */
+ public void putDocument(int callingUid, @NonNull DocumentProto origDocument) {
+ // Rewrite the type names to include the app's prefix
+ String typePrefix = getTypePrefix(callingUid);
+ DocumentProto.Builder documentBuilder = origDocument.toBuilder();
+ rewriteDocumentTypes(typePrefix, documentBuilder, /*add=*/ true);
+ mFakeIcing.put(documentBuilder.build());
+ }
+
+ /**
+ * Retrieves a document from the AppSearch index by URI.
+ *
+ * @param callingUid The uid of the app calling AppSearch.
+ * @param uri The URI of the document to get.
+ * @return The Document contents, or {@code null} if no such URI exists in the system.
+ */
+ @Nullable
+ public DocumentProto getDocument(int callingUid, @NonNull String uri) {
+ String typePrefix = getTypePrefix(callingUid);
+ DocumentProto document = mFakeIcing.get(uri);
+ if (document == null) {
+ return null;
+ }
+
+ // TODO(b/146526096): Since FakeIcing doesn't currently handle namespaces, we perform a
+ // post-filter to make sure we don't return documents we shouldn't. This should be removed
+ // once the real Icing Lib is implemented.
+ if (!document.getNamespace().equals(typePrefix)) {
+ return null;
+ }
+
+ // Rewrite the type names to remove the app's prefix
+ DocumentProto.Builder documentBuilder = document.toBuilder();
+ rewriteDocumentTypes(typePrefix, documentBuilder, /*add=*/ false);
+ return documentBuilder.build();
+ }
+
+ /**
+ * Executes a query against the AppSearch index and returns results.
+ *
+ * @param callingUid The uid of the app calling AppSearch.
+ * @param searchSpec Defines what and how to search
+ * @param resultSpec Defines what results to show
+ * @param scoringSpec Defines how to order results
+ * @return The results of performing this search The proto might have no {@code results} if no
+ * documents matched the query.
+ */
+ @NonNull
+ public SearchResultProto query(
+ int callingUid,
+ @NonNull SearchSpecProto searchSpec,
+ @NonNull ResultSpecProto resultSpec,
+ @NonNull ScoringSpecProto scoringSpec) {
+ String typePrefix = getTypePrefix(callingUid);
+ SearchResultProto searchResults = mFakeIcing.query(searchSpec.getQuery());
+ if (searchResults.getResultsCount() == 0) {
+ return searchResults;
+ }
+ Set<String> qualifiedSearchFilters = null;
+ if (searchSpec.getSchemaTypeFiltersCount() > 0) {
+ qualifiedSearchFilters = new ArraySet<>(searchSpec.getSchemaTypeFiltersCount());
+ for (String schema : searchSpec.getSchemaTypeFiltersList()) {
+ String qualifiedSchema = typePrefix + schema;
+ qualifiedSearchFilters.add(qualifiedSchema);
+ }
+ }
+ // Rewrite the type names to remove the app's prefix
+ SearchResultProto.Builder searchResultsBuilder = searchResults.toBuilder();
+ for (int i = 0; i < searchResultsBuilder.getResultsCount(); i++) {
+ if (searchResults.getResults(i).hasDocument()) {
+ SearchResultProto.ResultProto.Builder resultBuilder =
+ searchResultsBuilder.getResults(i).toBuilder();
+
+ // TODO(b/145631811): Since FakeIcing doesn't currently handle namespaces, we
+ // perform a post-filter to make sure we don't return documents we shouldn't. This
+ // should be removed once the real Icing Lib is implemented.
+ if (!resultBuilder.getDocument().getNamespace().equals(typePrefix)) {
+ searchResultsBuilder.removeResults(i);
+ i--;
+ continue;
+ }
+
+ // TODO(b/145631811): Since FakeIcing doesn't currently handle type names, we
+ // perform a post-filter to make sure we don't return documents we shouldn't. This
+ // should be removed once the real Icing Lib is implemented.
+ if (qualifiedSearchFilters != null
+ && !qualifiedSearchFilters.contains(
+ resultBuilder.getDocument().getSchema())) {
+ searchResultsBuilder.removeResults(i);
+ i--;
+ continue;
+ }
+
+ DocumentProto.Builder documentBuilder = resultBuilder.getDocument().toBuilder();
+ rewriteDocumentTypes(typePrefix, documentBuilder, /*add=*/false);
+ resultBuilder.setDocument(documentBuilder);
+ searchResultsBuilder.setResults(i, resultBuilder);
+ }
+ }
+ return searchResultsBuilder.build();
+ }
+
+ /** Deletes the given document by URI */
+ public boolean delete(int callingUid, @NonNull String uri) {
+ DocumentProto document = mFakeIcing.get(uri);
+ if (document == null) {
+ return false;
+ }
+
+ // TODO(b/146526096): Since FakeIcing doesn't currently handle namespaces, we perform a
+ // post-filter to make sure we don't delete documents we shouldn't. This should be
+ // removed once the real Icing Lib is implemented.
+ String typePrefix = getTypePrefix(callingUid);
+ if (!typePrefix.equals(document.getNamespace())) {
+ throw new SecurityException(
+ "Failed to delete document " + uri + "; URI collision in FakeIcing");
+ }
+
+ return mFakeIcing.delete(uri);
+ }
+
+ /** Deletes all documents having the given {@code schemaType}. */
+ public boolean deleteByType(int callingUid, @NonNull String schemaType) {
+ String typePrefix = getTypePrefix(callingUid);
+ String qualifiedType = typePrefix + schemaType;
+ return mFakeIcing.deleteByType(qualifiedType);
+ }
+
+ /**
+ * Deletes all documents owned by the calling app.
+ *
+ * @param callingUid The uid of the app calling AppSearch.
+ */
+ public void deleteAll(int callingUid) {
+ String namespace = getTypePrefix(callingUid);
+ mFakeIcing.deleteByNamespace(namespace);
+ }
+
+ /**
+ * Rewrites all types mentioned anywhere in {@code documentBuilder} to prepend or remove
+ * {@code typePrefix}.
+ *
+ * @param typePrefix The prefix to add or remove
+ * @param documentBuilder The document to mutate
+ * @param add Whether to add typePrefix to the types. If {@code false}, typePrefix will be
+ * removed from the types.
+ * @throws IllegalArgumentException If {@code add=false} and the document has a type that
+ * doesn't start with {@code typePrefix}.
+ */
+ @VisibleForTesting
+ void rewriteDocumentTypes(
+ @NonNull String typePrefix,
+ @NonNull DocumentProto.Builder documentBuilder,
+ boolean add) {
+ // Rewrite the type name to include/remove the app's prefix
+ String newSchema;
+ if (add) {
+ newSchema = typePrefix + documentBuilder.getSchema();
+ } else {
+ newSchema = removePrefix(typePrefix, documentBuilder.getSchema());
+ }
+ documentBuilder.setSchema(newSchema);
+
+ // Add/remove namespace. If we ever allow users to set their own namespaces, this will have
+ // to change to prepend the prefix instead of setting the whole namespace. We will also have
+ // to store the namespaces in a map similar to the type map so we can rewrite queries with
+ // empty namespaces.
+ if (add) {
+ documentBuilder.setNamespace(typePrefix);
+ } else if (!documentBuilder.getNamespace().equals(typePrefix)) {
+ throw new IllegalStateException(
+ "Unexpected namespace \"" + documentBuilder.getNamespace()
+ + "\" (expected \"" + typePrefix + "\")");
+ } else {
+ documentBuilder.clearNamespace();
+ }
+
+ // Recurse into derived documents
+ for (int propertyIdx = 0;
+ propertyIdx < documentBuilder.getPropertiesCount();
+ propertyIdx++) {
+ int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
+ if (documentCount > 0) {
+ PropertyProto.Builder propertyBuilder =
+ documentBuilder.getProperties(propertyIdx).toBuilder();
+ for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
+ DocumentProto.Builder derivedDocumentBuilder =
+ propertyBuilder.getDocumentValues(documentIdx).toBuilder();
+ rewriteDocumentTypes(typePrefix, derivedDocumentBuilder, add);
+ propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
+ }
+ documentBuilder.setProperties(propertyIdx, propertyBuilder);
+ }
+ }
+ }
+
+ /**
+ * Returns a type prefix in a format like {@code com.example.package@1000/} or
+ * {@code com.example.sharedname:5678@1000/}.
+ */
+ @NonNull
+ private String getTypePrefix(int callingUid) {
+ // For regular apps, this call will return the package name. If callingUid is an
+ // android:sharedUserId, this value may be another type of name and have a :uid suffix.
+ String callingUidName = mContext.getPackageManager().getNameForUid(callingUid);
+ if (callingUidName == null) {
+ // Not sure how this is possible --- maybe app was uninstalled?
+ throw new IllegalStateException("Failed to look up package name for uid " + callingUid);
+ }
+ return callingUidName + "@" + mUserId + "/";
+ }
+
+ @NonNull
+ private static String removePrefix(@NonNull String prefix, @NonNull String input) {
+ if (!input.startsWith(prefix)) {
+ throw new IllegalArgumentException(
+ "Input \"" + input + "\" does not start with \"" + prefix + "\"");
+ }
+ return input.substring(prefix.length());
+ }
+}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/impl/FakeIcing.java b/apex/appsearch/service/java/com/android/server/appsearch/impl/FakeIcing.java
new file mode 100644
index 000000000000..da1573463b73
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/impl/FakeIcing.java
@@ -0,0 +1,216 @@
+/*
+ * 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.appsearch.impl;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.SparseArray;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.PropertyProto;
+import com.google.android.icing.proto.SearchResultProto;
+import com.google.android.icing.proto.StatusProto;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Fake in-memory implementation of the Icing key-value store and reverse index.
+ * <p>
+ * Currently, only queries by single exact term are supported. There is no support for persistence,
+ * namespaces, i18n tokenization, or schema.
+ */
+public class FakeIcing {
+ private final AtomicInteger mNextDocId = new AtomicInteger();
+ private final Map<String, Integer> mUriToDocIdMap = new ArrayMap<>();
+ /** Array of Documents where index into the array is the docId. */
+ private final SparseArray<DocumentProto> mDocStore = new SparseArray<>();
+ /** Map of term to posting-list (the set of DocIds containing that term). */
+ private final Map<String, Set<Integer>> mIndex = new ArrayMap<>();
+
+ /**
+ * Inserts a document into the index.
+ *
+ * @param document The document to insert.
+ */
+ public void put(@NonNull DocumentProto document) {
+ String uri = document.getUri();
+
+ // Update mDocIdMap
+ Integer docId = mUriToDocIdMap.get(uri);
+ if (docId != null) {
+ // Delete the old doc
+ mDocStore.remove(docId);
+ }
+
+ // Allocate a new docId
+ docId = mNextDocId.getAndIncrement();
+ mUriToDocIdMap.put(uri, docId);
+
+ // Update mDocStore
+ mDocStore.put(docId, document);
+
+ // Update mIndex
+ indexDocument(docId, document);
+ }
+
+ /**
+ * Retrieves a document from the index.
+ *
+ * @param uri The URI of the document to retrieve.
+ * @return The body of the document, or {@code null} if no such document exists.
+ */
+ @Nullable
+ public DocumentProto get(@NonNull String uri) {
+ Integer docId = mUriToDocIdMap.get(uri);
+ if (docId == null) {
+ return null;
+ }
+ return mDocStore.get(docId);
+ }
+
+ /**
+ * Returns documents containing all words in the given query string.
+ *
+ * @param queryExpression A set of words to search for. They will be implicitly AND-ed together.
+ * No operators are supported.
+ * @return A {@link SearchResultProto} containing the matching documents, which may have no
+ * results if no documents match.
+ */
+ @NonNull
+ public SearchResultProto query(@NonNull String queryExpression) {
+ String[] terms = normalizeString(queryExpression).split("\\s+");
+ SearchResultProto.Builder results = SearchResultProto.newBuilder()
+ .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK));
+ if (terms.length == 0) {
+ return results.build();
+ }
+ Set<Integer> docIds = mIndex.get(terms[0]);
+ if (docIds == null || docIds.isEmpty()) {
+ return results.build();
+ }
+ for (int i = 1; i < terms.length; i++) {
+ Set<Integer> termDocIds = mIndex.get(terms[i]);
+ if (termDocIds == null) {
+ return results.build();
+ }
+ docIds.retainAll(termDocIds);
+ if (docIds.isEmpty()) {
+ return results.build();
+ }
+ }
+ for (int docId : docIds) {
+ DocumentProto document = mDocStore.get(docId);
+ if (document != null) {
+ results.addResults(
+ SearchResultProto.ResultProto.newBuilder().setDocument(document));
+ }
+ }
+ return results.build();
+ }
+
+ /**
+ * Deletes a document by its URI.
+ *
+ * @param uri The URI of the document to be deleted.
+ * @return Whether deletion was successful.
+ */
+ public boolean delete(@NonNull String uri) {
+ // Update mDocIdMap
+ Integer docId = mUriToDocIdMap.get(uri);
+ if (docId != null) {
+ // Delete the old doc
+ mDocStore.remove(docId);
+ mUriToDocIdMap.remove(uri);
+ return true;
+ }
+ return false;
+ }
+
+ /** Deletes all documents having the given namespace. */
+ public void deleteByNamespace(@NonNull String namespace) {
+ for (int i = 0; i < mDocStore.size(); i++) {
+ DocumentProto document = mDocStore.valueAt(i);
+ if (namespace.equals(document.getNamespace())) {
+ mDocStore.removeAt(i);
+ mUriToDocIdMap.remove(document.getUri());
+ i--;
+ }
+ }
+ }
+
+ /**
+ * Deletes all documents having the given type.
+ *
+ * @return true if any documents were deleted.
+ */
+ public boolean deleteByType(@NonNull String type) {
+ boolean deletedAny = false;
+ for (int i = 0; i < mDocStore.size(); i++) {
+ DocumentProto document = mDocStore.valueAt(i);
+ if (type.equals(document.getSchema())) {
+ mDocStore.removeAt(i);
+ mUriToDocIdMap.remove(document.getUri());
+ i--;
+ deletedAny = true;
+ }
+ }
+ return deletedAny;
+ }
+
+ private void indexDocument(int docId, DocumentProto document) {
+ for (PropertyProto property : document.getPropertiesList()) {
+ for (String stringValue : property.getStringValuesList()) {
+ String[] words = normalizeString(stringValue).split("\\s+");
+ for (String word : words) {
+ indexTerm(docId, word);
+ }
+ }
+ for (Long longValue : property.getInt64ValuesList()) {
+ indexTerm(docId, longValue.toString());
+ }
+ for (Double doubleValue : property.getDoubleValuesList()) {
+ indexTerm(docId, doubleValue.toString());
+ }
+ for (Boolean booleanValue : property.getBooleanValuesList()) {
+ indexTerm(docId, booleanValue.toString());
+ }
+ // Intentionally skipping bytes values
+ for (DocumentProto documentValue : property.getDocumentValuesList()) {
+ indexDocument(docId, documentValue);
+ }
+ }
+ }
+
+ private void indexTerm(int docId, String term) {
+ Set<Integer> postingList = mIndex.get(term);
+ if (postingList == null) {
+ postingList = new ArraySet<>();
+ mIndex.put(term, postingList);
+ }
+ postingList.add(docId);
+ }
+
+ /** Strips out punctuation and converts to lowercase. */
+ private static String normalizeString(String input) {
+ return input.replaceAll("\\p{P}", "").toLowerCase(Locale.getDefault());
+ }
+}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/impl/ImplInstanceManager.java b/apex/appsearch/service/java/com/android/server/appsearch/impl/ImplInstanceManager.java
new file mode 100644
index 000000000000..395e30e89dc0
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/impl/ImplInstanceManager.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 com.android.server.appsearch.impl;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.util.SparseArray;
+
+/**
+ * Manages the lifecycle of instances of {@link AppSearchImpl}.
+ *
+ * <p>These instances are managed per unique device-user.
+ */
+public final class ImplInstanceManager {
+ private static final SparseArray<AppSearchImpl> sInstances = new SparseArray<>();
+
+ /**
+ * Gets an instance of AppSearchImpl for the given user.
+ *
+ * <p>If no AppSearchImpl instance exists for this user, Icing will be initialized and one will
+ * be created.
+ *
+ * @param context The Android context
+ * @param userId The multi-user userId of the device user calling AppSearch
+ * @return An initialized {@link AppSearchImpl} for this user
+ */
+ @NonNull
+ public static AppSearchImpl getInstance(@NonNull Context context, @UserIdInt int userId) {
+ AppSearchImpl instance = sInstances.get(userId);
+ if (instance == null) {
+ synchronized (ImplInstanceManager.class) {
+ instance = sInstances.get(userId);
+ if (instance == null) {
+ instance = new AppSearchImpl(context, userId);
+ sInstances.put(userId, instance);
+ }
+ }
+ }
+ return instance;
+ }
+}
diff --git a/apex/jobscheduler/framework/Android.bp b/apex/jobscheduler/framework/Android.bp
index ec074262fb13..dab295bc7985 100644
--- a/apex/jobscheduler/framework/Android.bp
+++ b/apex/jobscheduler/framework/Android.bp
@@ -2,10 +2,7 @@ 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",
+ "java/**/*.aidl",
],
path: "java",
}
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.aidl b/apex/jobscheduler/framework/java/android/app/AlarmManager.aidl
new file mode 100644
index 000000000000..c9547b28740a
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.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;
+
+parcelable AlarmManager.AlarmClockInfo;
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
new file mode 100644
index 000000000000..b3137d84c3f1
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
@@ -0,0 +1,1152 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.annotation.IntDef;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.WorkSource;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+
+import libcore.timezone.ZoneInfoDb;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.WeakHashMap;
+
+/**
+ * This class provides access to the system alarm services. These allow you
+ * to schedule your application to be run at some point in the future. When
+ * an alarm goes off, the {@link Intent} that had been registered for it
+ * is broadcast by the system, automatically starting the target application
+ * if it is not already running. Registered alarms are retained while the
+ * device is asleep (and can optionally wake the device up if they go off
+ * during that time), but will be cleared if it is turned off and rebooted.
+ *
+ * <p>The Alarm Manager holds a CPU wake lock as long as the alarm receiver's
+ * onReceive() method is executing. This guarantees that the phone will not sleep
+ * until you have finished handling the broadcast. Once onReceive() returns, the
+ * Alarm Manager releases this wake lock. This means that the phone will in some
+ * cases sleep as soon as your onReceive() method completes. If your alarm receiver
+ * called {@link android.content.Context#startService Context.startService()}, it
+ * is possible that the phone will sleep before the requested service is launched.
+ * To prevent this, your BroadcastReceiver and Service will need to implement a
+ * separate wake lock policy to ensure that the phone continues running until the
+ * service becomes available.
+ *
+ * <p><b>Note: The Alarm Manager is intended for cases where you want to have
+ * your application code run at a specific time, even if your application is
+ * not currently running. For normal timing operations (ticks, timeouts,
+ * etc) it is easier and much more efficient to use
+ * {@link android.os.Handler}.</b>
+ *
+ * <p class="caution"><strong>Note:</strong> Beginning with API 19
+ * ({@link android.os.Build.VERSION_CODES#KITKAT}) alarm delivery is inexact:
+ * the OS will shift alarms in order to minimize wakeups and battery use. There are
+ * new APIs to support applications which need strict delivery guarantees; see
+ * {@link #setWindow(int, long, long, PendingIntent)} and
+ * {@link #setExact(int, long, PendingIntent)}. Applications whose {@code targetSdkVersion}
+ * is earlier than API 19 will continue to see the previous behavior in which all
+ * alarms are delivered exactly when requested.
+ */
+@SystemService(Context.ALARM_SERVICE)
+public class AlarmManager {
+ private static final String TAG = "AlarmManager";
+
+ /** @hide */
+ @IntDef(prefix = { "RTC", "ELAPSED" }, value = {
+ RTC_WAKEUP,
+ RTC,
+ ELAPSED_REALTIME_WAKEUP,
+ ELAPSED_REALTIME,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AlarmType {}
+
+ /**
+ * Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
+ * (wall clock time in UTC), which will wake up the device when
+ * it goes off.
+ */
+ public static final int RTC_WAKEUP = 0;
+ /**
+ * Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
+ * (wall clock time in UTC). This alarm does not wake the
+ * device up; if it goes off while the device is asleep, it will not be
+ * delivered until the next time the device wakes up.
+ */
+ public static final int RTC = 1;
+ /**
+ * Alarm time in {@link android.os.SystemClock#elapsedRealtime
+ * SystemClock.elapsedRealtime()} (time since boot, including sleep),
+ * which will wake up the device when it goes off.
+ */
+ public static final int ELAPSED_REALTIME_WAKEUP = 2;
+ /**
+ * Alarm time in {@link android.os.SystemClock#elapsedRealtime
+ * SystemClock.elapsedRealtime()} (time since boot, including sleep).
+ * This alarm does not wake the device up; if it goes off while the device
+ * is asleep, it will not be delivered until the next time the device
+ * wakes up.
+ */
+ public static final int ELAPSED_REALTIME = 3;
+
+ /**
+ * Broadcast Action: Sent after the value returned by
+ * {@link #getNextAlarmClock()} has changed.
+ *
+ * <p class="note">This is a protected intent that can only be sent by the system.
+ * It is only sent to registered receivers.</p>
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NEXT_ALARM_CLOCK_CHANGED =
+ "android.app.action.NEXT_ALARM_CLOCK_CHANGED";
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public static final long WINDOW_EXACT = 0;
+ /** @hide */
+ @UnsupportedAppUsage
+ public static final long WINDOW_HEURISTIC = -1;
+
+ /**
+ * Flag for alarms: this is to be a stand-alone alarm, that should not be batched with
+ * other alarms.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static final int FLAG_STANDALONE = 1<<0;
+
+ /**
+ * Flag for alarms: this alarm would like to wake the device even if it is idle. This
+ * is, for example, an alarm for an alarm clock.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static final int FLAG_WAKE_FROM_IDLE = 1<<1;
+
+ /**
+ * Flag for alarms: this alarm would like to still execute even if the device is
+ * idle. This won't bring the device out of idle, just allow this specific alarm to
+ * run. Note that this means the actual time this alarm goes off can be inconsistent
+ * with the time of non-allow-while-idle alarms (it could go earlier than the time
+ * requested by another alarm).
+ *
+ * @hide
+ */
+ public static final int FLAG_ALLOW_WHILE_IDLE = 1<<2;
+
+ /**
+ * Flag for alarms: same as {@link #FLAG_ALLOW_WHILE_IDLE}, but doesn't have restrictions
+ * on how frequently it can be scheduled. Only available (and automatically applied) to
+ * system alarms.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static final int FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED = 1<<3;
+
+ /**
+ * Flag for alarms: this alarm marks the point where we would like to come out of idle
+ * mode. It may be moved by the alarm manager to match the first wake-from-idle alarm.
+ * Scheduling an alarm with this flag puts the alarm manager in to idle mode, where it
+ * avoids scheduling any further alarms until the marker alarm is executed.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static final int FLAG_IDLE_UNTIL = 1<<4;
+
+ @UnsupportedAppUsage
+ private final IAlarmManager mService;
+ private final Context mContext;
+ private final String mPackageName;
+ private final boolean mAlwaysExact;
+ private final int mTargetSdkVersion;
+ private final Handler mMainThreadHandler;
+
+ /**
+ * Direct-notification alarms: the requester must be running continuously from the
+ * time the alarm is set to the time it is delivered, or delivery will fail. Only
+ * one-shot alarms can be set using this mechanism, not repeating alarms.
+ */
+ public interface OnAlarmListener {
+ /**
+ * Callback method that is invoked by the system when the alarm time is reached.
+ */
+ public void onAlarm();
+ }
+
+ final class ListenerWrapper extends IAlarmListener.Stub implements Runnable {
+ final OnAlarmListener mListener;
+ Handler mHandler;
+ IAlarmCompleteListener mCompletion;
+
+ public ListenerWrapper(OnAlarmListener listener) {
+ mListener = listener;
+ }
+
+ public void setHandler(Handler h) {
+ mHandler = h;
+ }
+
+ public void cancel() {
+ try {
+ mService.remove(null, this);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void doAlarm(IAlarmCompleteListener alarmManager) {
+ mCompletion = alarmManager;
+
+ mHandler.post(this);
+ }
+
+ @Override
+ public void run() {
+ // Now deliver it to the app
+ try {
+ mListener.onAlarm();
+ } finally {
+ // No catch -- make sure to report completion to the system process,
+ // but continue to allow the exception to crash the app.
+
+ try {
+ mCompletion.alarmComplete(this);
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to report completion to Alarm Manager!", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Tracking of the OnAlarmListener -> ListenerWrapper mapping, for cancel() support.
+ * An entry is guaranteed to stay in this map as long as its ListenerWrapper is held by the
+ * server.
+ *
+ * <p>Access is synchronized on the AlarmManager class object.
+ */
+ private static WeakHashMap<OnAlarmListener, WeakReference<ListenerWrapper>> sWrappers;
+
+ /**
+ * package private on purpose
+ */
+ AlarmManager(IAlarmManager service, Context ctx) {
+ mService = service;
+
+ mContext = ctx;
+ mPackageName = ctx.getPackageName();
+ mTargetSdkVersion = ctx.getApplicationInfo().targetSdkVersion;
+ mAlwaysExact = (mTargetSdkVersion < Build.VERSION_CODES.KITKAT);
+ mMainThreadHandler = new Handler(ctx.getMainLooper());
+ }
+
+ private long legacyExactLength() {
+ return (mAlwaysExact ? WINDOW_EXACT : WINDOW_HEURISTIC);
+ }
+
+ /**
+ * <p>Schedule an alarm. <b>Note: for timing operations (ticks, timeouts,
+ * etc) it is easier and much more efficient to use {@link android.os.Handler}.</b>
+ * If there is already an alarm scheduled for the same IntentSender, that previous
+ * alarm will first be canceled.
+ *
+ * <p>If the stated trigger time is in the past, the alarm will be triggered
+ * immediately. If there is already an alarm for this Intent
+ * scheduled (with the equality of two intents being defined by
+ * {@link Intent#filterEquals}), then it will be removed and replaced by
+ * this one.
+ *
+ * <p>
+ * The alarm is an Intent broadcast that goes to a broadcast receiver that
+ * you registered with {@link android.content.Context#registerReceiver}
+ * or through the &lt;receiver&gt; tag in an AndroidManifest.xml file.
+ *
+ * <p>
+ * Alarm intents are delivered with a data extra of type int called
+ * {@link Intent#EXTRA_ALARM_COUNT Intent.EXTRA_ALARM_COUNT} that indicates
+ * how many past alarm events have been accumulated into this intent
+ * broadcast. Recurring alarms that have gone undelivered because the
+ * phone was asleep may have a count greater than one when delivered.
+ *
+ * <div class="note">
+ * <p>
+ * <b>Note:</b> Beginning in API 19, the trigger time passed to this method
+ * is treated as inexact: the alarm will not be delivered before this time, but
+ * may be deferred and delivered some time later. The OS will use
+ * this policy in order to "batch" alarms together across the entire system,
+ * minimizing the number of times the device needs to "wake up" and minimizing
+ * battery use. In general, alarms scheduled in the near future will not
+ * be deferred as long as alarms scheduled far in the future.
+ *
+ * <p>
+ * With the new batching policy, delivery ordering guarantees are not as
+ * strong as they were previously. If the application sets multiple alarms,
+ * it is possible that these alarms' <em>actual</em> delivery ordering may not match
+ * the order of their <em>requested</em> delivery times. If your application has
+ * strong ordering requirements there are other APIs that you can use to get
+ * the necessary behavior; see {@link #setWindow(int, long, long, PendingIntent)}
+ * and {@link #setExact(int, long, PendingIntent)}.
+ *
+ * <p>
+ * Applications whose {@code targetSdkVersion} is before API 19 will
+ * continue to get the previous alarm behavior: all of their scheduled alarms
+ * will be treated as exact.
+ * </div>
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should go
+ * off, using the appropriate clock (depending on the alarm type).
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see android.os.Handler
+ * @see #setExact
+ * @see #setRepeating
+ * @see #setWindow
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void set(@AlarmType int type, long triggerAtMillis, PendingIntent operation) {
+ setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, operation, null, null,
+ null, null, null);
+ }
+
+ /**
+ * Direct callback version of {@link #set(int, long, PendingIntent)}. Rather than
+ * supplying a PendingIntent to be sent when the alarm time is reached, this variant
+ * supplies an {@link OnAlarmListener} instance that will be invoked at that time.
+ * <p>
+ * The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Handler, or on the application's main looper
+ * if {@code null} is passed as the {@code targetHandler} parameter.
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should go
+ * off, using the appropriate clock (depending on the alarm type).
+ * @param tag string describing the alarm, used for logging and battery-use
+ * attribution
+ * @param listener {@link OnAlarmListener} instance whose
+ * {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * called when the alarm time is reached. A given OnAlarmListener instance can
+ * only be the target of a single pending alarm, just as a given PendingIntent
+ * can only be used with one alarm at a time.
+ * @param targetHandler {@link Handler} on which to execute the listener's onAlarm()
+ * callback, or {@code null} to run that callback on the main looper.
+ */
+ public void set(@AlarmType int type, long triggerAtMillis, String tag, OnAlarmListener listener,
+ Handler targetHandler) {
+ setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, null, listener, tag,
+ targetHandler, null, null);
+ }
+
+ /**
+ * Schedule a repeating alarm. <b>Note: for timing operations (ticks,
+ * timeouts, etc) it is easier and much more efficient to use
+ * {@link android.os.Handler}.</b> If there is already an alarm scheduled
+ * for the same IntentSender, it will first be canceled.
+ *
+ * <p>Like {@link #set}, except you can also supply a period at which
+ * the alarm will automatically repeat. This alarm continues
+ * repeating until explicitly removed with {@link #cancel}. If the stated
+ * trigger time is in the past, the alarm will be triggered immediately, with an
+ * alarm count depending on how far in the past the trigger time is relative
+ * to the repeat interval.
+ *
+ * <p>If an alarm is delayed (by system sleep, for example, for non
+ * _WAKEUP alarm types), a skipped repeat will be delivered as soon as
+ * possible. After that, future alarms will be delivered according to the
+ * original schedule; they do not drift over time. For example, if you have
+ * set a recurring alarm for the top of every hour but the phone was asleep
+ * from 7:45 until 8:45, an alarm will be sent as soon as the phone awakens,
+ * then the next alarm will be sent at 9:00.
+ *
+ * <p>If your application wants to allow the delivery times to drift in
+ * order to guarantee that at least a certain time interval always elapses
+ * between alarms, then the approach to take is to use one-time alarms,
+ * scheduling the next one yourself when handling each alarm delivery.
+ *
+ * <p class="note">
+ * <b>Note:</b> as of API 19, all repeating alarms are inexact. If your
+ * application needs precise delivery times then it must use one-time
+ * exact alarms, rescheduling each time as described above. Legacy applications
+ * whose {@code targetSdkVersion} is earlier than API 19 will continue to have all
+ * of their alarms, including repeating alarms, treated as exact.
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should first
+ * go off, using the appropriate clock (depending on the alarm type).
+ * @param intervalMillis interval in milliseconds between subsequent repeats
+ * of the alarm.
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see android.os.Handler
+ * @see #set
+ * @see #setExact
+ * @see #setWindow
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void setRepeating(@AlarmType int type, long triggerAtMillis,
+ long intervalMillis, PendingIntent operation) {
+ setImpl(type, triggerAtMillis, legacyExactLength(), intervalMillis, 0, operation,
+ null, null, null, null, null);
+ }
+
+ /**
+ * Schedule an alarm to be delivered within a given window of time. This method
+ * is similar to {@link #set(int, long, PendingIntent)}, but allows the
+ * application to precisely control the degree to which its delivery might be
+ * adjusted by the OS. This method allows an application to take advantage of the
+ * battery optimizations that arise from delivery batching even when it has
+ * modest timeliness requirements for its alarms.
+ *
+ * <p>
+ * This method can also be used to achieve strict ordering guarantees among
+ * multiple alarms by ensuring that the windows requested for each alarm do
+ * not intersect.
+ *
+ * <p>
+ * When precise delivery is not required, applications should use the standard
+ * {@link #set(int, long, PendingIntent)} method. This will give the OS the most
+ * flexibility to minimize wakeups and battery use. For alarms that must be delivered
+ * at precisely-specified times with no acceptable variation, applications can use
+ * {@link #setExact(int, long, PendingIntent)}.
+ *
+ * @param type type of alarm.
+ * @param windowStartMillis The earliest time, in milliseconds, that the alarm should
+ * be delivered, expressed in the appropriate clock's units (depending on the alarm
+ * type).
+ * @param windowLengthMillis The length of the requested delivery window,
+ * in milliseconds. The alarm will be delivered no later than this many
+ * milliseconds after {@code windowStartMillis}. Note that this parameter
+ * is a <i>duration,</i> not the timestamp of the end of the window.
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see #set
+ * @see #setExact
+ * @see #setRepeating
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
+ PendingIntent operation) {
+ setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, operation,
+ null, null, null, null, null);
+ }
+
+ /**
+ * Direct callback version of {@link #setWindow(int, long, long, PendingIntent)}. Rather
+ * than supplying a PendingIntent to be sent when the alarm time is reached, this variant
+ * supplies an {@link OnAlarmListener} instance that will be invoked at that time.
+ * <p>
+ * The OnAlarmListener {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Handler, or on the application's main looper
+ * if {@code null} is passed as the {@code targetHandler} parameter.
+ */
+ public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
+ String tag, OnAlarmListener listener, Handler targetHandler) {
+ setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, null, listener, tag,
+ targetHandler, null, null);
+ }
+
+ /**
+ * Schedule an alarm to be delivered precisely at the stated time.
+ *
+ * <p>
+ * This method is like {@link #set(int, long, PendingIntent)}, but does not permit
+ * the OS to adjust the delivery time. The alarm will be delivered as nearly as
+ * possible to the requested trigger time.
+ *
+ * <p>
+ * <b>Note:</b> only alarms for which there is a strong demand for exact-time
+ * delivery (such as an alarm clock ringing at the requested time) should be
+ * scheduled as exact. Applications are strongly discouraged from using exact
+ * alarms unnecessarily as they reduce the OS's ability to minimize battery use.
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should go
+ * off, using the appropriate clock (depending on the alarm type).
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see #set
+ * @see #setRepeating
+ * @see #setWindow
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void setExact(@AlarmType int type, long triggerAtMillis, PendingIntent operation) {
+ setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, operation, null, null, null,
+ null, null);
+ }
+
+ /**
+ * Direct callback version of {@link #setExact(int, long, PendingIntent)}. Rather
+ * than supplying a PendingIntent to be sent when the alarm time is reached, this variant
+ * supplies an {@link OnAlarmListener} instance that will be invoked at that time.
+ * <p>
+ * The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Handler, or on the application's main looper
+ * if {@code null} is passed as the {@code targetHandler} parameter.
+ */
+ public void setExact(@AlarmType int type, long triggerAtMillis, String tag,
+ OnAlarmListener listener, Handler targetHandler) {
+ setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, null, listener, tag,
+ targetHandler, null, null);
+ }
+
+ /**
+ * Schedule an idle-until alarm, which will keep the alarm manager idle until
+ * the given time.
+ * @hide
+ */
+ public void setIdleUntil(@AlarmType int type, long triggerAtMillis, String tag,
+ OnAlarmListener listener, Handler targetHandler) {
+ setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_IDLE_UNTIL, null,
+ listener, tag, targetHandler, null, null);
+ }
+
+ /**
+ * Schedule an alarm that represents an alarm clock, which will be used to notify the user
+ * when it goes off. The expectation is that when this alarm triggers, the application will
+ * further wake up the device to tell the user about the alarm -- turning on the screen,
+ * playing a sound, vibrating, etc. As such, the system will typically also use the
+ * information supplied here to tell the user about this upcoming alarm if appropriate.
+ *
+ * <p>Due to the nature of this kind of alarm, similar to {@link #setExactAndAllowWhileIdle},
+ * these alarms will be allowed to trigger even if the system is in a low-power idle
+ * (a.k.a. doze) mode. The system may also do some prep-work when it sees that such an
+ * alarm coming up, to reduce the amount of background work that could happen if this
+ * causes the device to fully wake up -- this is to avoid situations such as a large number
+ * of devices having an alarm set at the same time in the morning, all waking up at that
+ * time and suddenly swamping the network with pending background work. As such, these
+ * types of alarms can be extremely expensive on battery use and should only be used for
+ * their intended purpose.</p>
+ *
+ * <p>
+ * This method is like {@link #setExact(int, long, PendingIntent)}, but implies
+ * {@link #RTC_WAKEUP}.
+ *
+ * @param info
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see #set
+ * @see #setRepeating
+ * @see #setWindow
+ * @see #setExact
+ * @see #cancel
+ * @see #getNextAlarmClock()
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ */
+ public void setAlarmClock(AlarmClockInfo info, PendingIntent operation) {
+ setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0, 0, operation,
+ null, null, null, null, info);
+ }
+
+ /** @hide */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
+ public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
+ long intervalMillis, PendingIntent operation, WorkSource workSource) {
+ setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, operation, null, null,
+ null, workSource, null);
+ }
+
+ /**
+ * Direct callback version of {@link #set(int, long, long, long, PendingIntent, WorkSource)}.
+ * Note that repeating alarms must use the PendingIntent variant, not an OnAlarmListener.
+ * <p>
+ * The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Handler, or on the application's main looper
+ * if {@code null} is passed as the {@code targetHandler} parameter.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
+ long intervalMillis, String tag, OnAlarmListener listener, Handler targetHandler,
+ WorkSource workSource) {
+ setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, tag,
+ targetHandler, workSource, null);
+ }
+
+ /**
+ * Direct callback version of {@link #set(int, long, long, long, PendingIntent, WorkSource)}.
+ * Note that repeating alarms must use the PendingIntent variant, not an OnAlarmListener.
+ * <p>
+ * The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Handler, or on the application's main looper
+ * if {@code null} is passed as the {@code targetHandler} parameter.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
+ public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
+ long intervalMillis, OnAlarmListener listener, Handler targetHandler,
+ WorkSource workSource) {
+ setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, null,
+ targetHandler, workSource, null);
+ }
+
+ private void setImpl(@AlarmType int type, long triggerAtMillis, long windowMillis,
+ long intervalMillis, int flags, PendingIntent operation, final OnAlarmListener listener,
+ String listenerTag, Handler targetHandler, WorkSource workSource,
+ AlarmClockInfo alarmClock) {
+ if (triggerAtMillis < 0) {
+ /* NOTYET
+ if (mAlwaysExact) {
+ // Fatal error for KLP+ apps to use negative trigger times
+ throw new IllegalArgumentException("Invalid alarm trigger time "
+ + triggerAtMillis);
+ }
+ */
+ triggerAtMillis = 0;
+ }
+
+ ListenerWrapper recipientWrapper = null;
+ if (listener != null) {
+ synchronized (AlarmManager.class) {
+ if (sWrappers == null) {
+ sWrappers = new WeakHashMap<>();
+ }
+
+ final WeakReference<ListenerWrapper> weakRef = sWrappers.get(listener);
+ if (weakRef != null) {
+ recipientWrapper = weakRef.get();
+ }
+ // no existing wrapper => build a new one
+ if (recipientWrapper == null) {
+ recipientWrapper = new ListenerWrapper(listener);
+ sWrappers.put(listener, new WeakReference<>(recipientWrapper));
+ }
+ }
+
+ final Handler handler = (targetHandler != null) ? targetHandler : mMainThreadHandler;
+ recipientWrapper.setHandler(handler);
+ }
+
+ try {
+ mService.set(mPackageName, type, triggerAtMillis, windowMillis, intervalMillis, flags,
+ operation, recipientWrapper, listenerTag, workSource, alarmClock);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Available inexact recurrence interval recognized by
+ * {@link #setInexactRepeating(int, long, long, PendingIntent)}
+ * when running on Android prior to API 19.
+ */
+ public static final long INTERVAL_FIFTEEN_MINUTES = 15 * 60 * 1000;
+
+ /**
+ * Available inexact recurrence interval recognized by
+ * {@link #setInexactRepeating(int, long, long, PendingIntent)}
+ * when running on Android prior to API 19.
+ */
+ public static final long INTERVAL_HALF_HOUR = 2*INTERVAL_FIFTEEN_MINUTES;
+
+ /**
+ * Available inexact recurrence interval recognized by
+ * {@link #setInexactRepeating(int, long, long, PendingIntent)}
+ * when running on Android prior to API 19.
+ */
+ public static final long INTERVAL_HOUR = 2*INTERVAL_HALF_HOUR;
+
+ /**
+ * Available inexact recurrence interval recognized by
+ * {@link #setInexactRepeating(int, long, long, PendingIntent)}
+ * when running on Android prior to API 19.
+ */
+ public static final long INTERVAL_HALF_DAY = 12*INTERVAL_HOUR;
+
+ /**
+ * Available inexact recurrence interval recognized by
+ * {@link #setInexactRepeating(int, long, long, PendingIntent)}
+ * when running on Android prior to API 19.
+ */
+ public static final long INTERVAL_DAY = 2*INTERVAL_HALF_DAY;
+
+ /**
+ * Schedule a repeating alarm that has inexact trigger time requirements;
+ * for example, an alarm that repeats every hour, but not necessarily at
+ * the top of every hour. These alarms are more power-efficient than
+ * the strict recurrences traditionally supplied by {@link #setRepeating}, since the
+ * system can adjust alarms' delivery times to cause them to fire simultaneously,
+ * avoiding waking the device from sleep more than necessary.
+ *
+ * <p>Your alarm's first trigger will not be before the requested time,
+ * but it might not occur for almost a full interval after that time. In
+ * addition, while the overall period of the repeating alarm will be as
+ * requested, the time between any two successive firings of the alarm
+ * may vary. If your application demands very low jitter, use
+ * one-shot alarms with an appropriate window instead; see {@link
+ * #setWindow(int, long, long, PendingIntent)} and
+ * {@link #setExact(int, long, PendingIntent)}.
+ *
+ * <p class="note">
+ * As of API 19, all repeating alarms are inexact. Because this method has
+ * been available since API 3, your application can safely call it and be
+ * assured that it will get similar behavior on both current and older versions
+ * of Android.
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should first
+ * go off, using the appropriate clock (depending on the alarm type). This
+ * is inexact: the alarm will not fire before this time, but there may be a
+ * delay of almost an entire alarm interval before the first invocation of
+ * the alarm.
+ * @param intervalMillis interval in milliseconds between subsequent repeats
+ * of the alarm. Prior to API 19, if this is one of INTERVAL_FIFTEEN_MINUTES,
+ * INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_HALF_DAY, or INTERVAL_DAY
+ * then the alarm will be phase-aligned with other alarms to reduce the
+ * number of wakeups. Otherwise, the alarm will be set as though the
+ * application had called {@link #setRepeating}. As of API 19, all repeating
+ * alarms will be inexact and subject to batching with other alarms regardless
+ * of their stated repeat interval.
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see android.os.Handler
+ * @see #set
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ * @see #INTERVAL_FIFTEEN_MINUTES
+ * @see #INTERVAL_HALF_HOUR
+ * @see #INTERVAL_HOUR
+ * @see #INTERVAL_HALF_DAY
+ * @see #INTERVAL_DAY
+ */
+ public void setInexactRepeating(@AlarmType int type, long triggerAtMillis,
+ long intervalMillis, PendingIntent operation) {
+ setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, intervalMillis, 0, operation, null,
+ null, null, null, null);
+ }
+
+ /**
+ * Like {@link #set(int, long, PendingIntent)}, but this alarm will be allowed to execute
+ * even when the system is in low-power idle (a.k.a. doze) modes. This type of alarm must
+ * <b>only</b> be used for situations where it is actually required that the alarm go off while
+ * in idle -- a reasonable example would be for a calendar notification that should make a
+ * sound so the user is aware of it. When the alarm is dispatched, the app will also be
+ * added to the system's temporary whitelist for approximately 10 seconds to allow that
+ * application to acquire further wake locks in which to complete its work.</p>
+ *
+ * <p>These alarms can significantly impact the power use
+ * of the device when idle (and thus cause significant battery blame to the app scheduling
+ * them), so they should be used with care. To reduce abuse, there are restrictions on how
+ * frequently these alarms will go off for a particular application.
+ * Under normal system operation, it will not dispatch these
+ * alarms more than about every minute (at which point every such pending alarm is
+ * dispatched); when in low-power idle modes this duration may be significantly longer,
+ * such as 15 minutes.</p>
+ *
+ * <p>Unlike other alarms, the system is free to reschedule this type of alarm to happen
+ * out of order with any other alarms, even those from the same app. This will clearly happen
+ * when the device is idle (since this alarm can go off while idle, when any other alarms
+ * from the app will be held until later), but may also happen even when not idle.</p>
+ *
+ * <p>Regardless of the app's target SDK version, this call always allows batching of the
+ * alarm.</p>
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should go
+ * off, using the appropriate clock (depending on the alarm type).
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see #set(int, long, PendingIntent)
+ * @see #setExactAndAllowWhileIdle
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void setAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
+ PendingIntent operation) {
+ setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, 0, FLAG_ALLOW_WHILE_IDLE,
+ operation, null, null, null, null, null);
+ }
+
+ /**
+ * Like {@link #setExact(int, long, PendingIntent)}, but this alarm will be allowed to execute
+ * even when the system is in low-power idle modes. If you don't need exact scheduling of
+ * the alarm but still need to execute while idle, consider using
+ * {@link #setAndAllowWhileIdle}. This type of alarm must <b>only</b>
+ * be used for situations where it is actually required that the alarm go off while in
+ * idle -- a reasonable example would be for a calendar notification that should make a
+ * sound so the user is aware of it. When the alarm is dispatched, the app will also be
+ * added to the system's temporary whitelist for approximately 10 seconds to allow that
+ * application to acquire further wake locks in which to complete its work.</p>
+ *
+ * <p>These alarms can significantly impact the power use
+ * of the device when idle (and thus cause significant battery blame to the app scheduling
+ * them), so they should be used with care. To reduce abuse, there are restrictions on how
+ * frequently these alarms will go off for a particular application.
+ * Under normal system operation, it will not dispatch these
+ * alarms more than about every minute (at which point every such pending alarm is
+ * dispatched); when in low-power idle modes this duration may be significantly longer,
+ * such as 15 minutes.</p>
+ *
+ * <p>Unlike other alarms, the system is free to reschedule this type of alarm to happen
+ * out of order with any other alarms, even those from the same app. This will clearly happen
+ * when the device is idle (since this alarm can go off while idle, when any other alarms
+ * from the app will be held until later), but may also happen even when not idle.
+ * Note that the OS will allow itself more flexibility for scheduling these alarms than
+ * regular exact alarms, since the application has opted into this behavior. When the
+ * device is idle it may take even more liberties with scheduling in order to optimize
+ * for battery life.</p>
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should go
+ * off, using the appropriate clock (depending on the alarm type).
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see #set
+ * @see #setRepeating
+ * @see #setWindow
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
+ PendingIntent operation) {
+ setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, operation,
+ null, null, null, null, null);
+ }
+
+ /**
+ * Remove any alarms with a matching {@link Intent}.
+ * Any alarm, of any type, whose Intent matches this one (as defined by
+ * {@link Intent#filterEquals}), will be canceled.
+ *
+ * @param operation IntentSender which matches a previously added
+ * IntentSender. This parameter must not be {@code null}.
+ *
+ * @see #set
+ */
+ public void cancel(PendingIntent operation) {
+ if (operation == null) {
+ final String msg = "cancel() called with a null PendingIntent";
+ if (mTargetSdkVersion >= Build.VERSION_CODES.N) {
+ throw new NullPointerException(msg);
+ } else {
+ Log.e(TAG, msg);
+ return;
+ }
+ }
+
+ try {
+ mService.remove(operation, null);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove any alarm scheduled to be delivered to the given {@link OnAlarmListener}.
+ *
+ * @param listener OnAlarmListener instance that is the target of a currently-set alarm.
+ */
+ public void cancel(OnAlarmListener listener) {
+ if (listener == null) {
+ throw new NullPointerException("cancel() called with a null OnAlarmListener");
+ }
+
+ ListenerWrapper wrapper = null;
+ synchronized (AlarmManager.class) {
+ if (sWrappers != null) {
+ final WeakReference<ListenerWrapper> weakRef = sWrappers.get(listener);
+ if (weakRef != null) {
+ wrapper = weakRef.get();
+ }
+ }
+ }
+
+ if (wrapper == null) {
+ Log.w(TAG, "Unrecognized alarm listener " + listener);
+ return;
+ }
+
+ wrapper.cancel();
+ }
+
+ /**
+ * Set the system wall clock time.
+ * Requires the permission android.permission.SET_TIME.
+ *
+ * @param millis time in milliseconds since the Epoch
+ */
+ @RequiresPermission(android.Manifest.permission.SET_TIME)
+ public void setTime(long millis) {
+ try {
+ mService.setTime(millis);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the system's persistent default time zone. This is the time zone for all apps, even
+ * after a reboot. Use {@link java.util.TimeZone#setDefault} if you just want to change the
+ * time zone within your app, and even then prefer to pass an explicit
+ * {@link java.util.TimeZone} to APIs that require it rather than changing the time zone for
+ * all threads.
+ *
+ * <p> On android M and above, it is an error to pass in a non-Olson timezone to this
+ * function. Note that this is a bad idea on all Android releases because POSIX and
+ * the {@code TimeZone} class have opposite interpretations of {@code '+'} and {@code '-'}
+ * in the same non-Olson ID.
+ *
+ * @param timeZone one of the Olson ids from the list returned by
+ * {@link java.util.TimeZone#getAvailableIDs}
+ */
+ @RequiresPermission(android.Manifest.permission.SET_TIME_ZONE)
+ public void setTimeZone(String timeZone) {
+ if (TextUtils.isEmpty(timeZone)) {
+ return;
+ }
+
+ // Reject this timezone if it isn't an Olson zone we recognize.
+ if (mTargetSdkVersion >= Build.VERSION_CODES.M) {
+ boolean hasTimeZone = ZoneInfoDb.getInstance().hasTimeZone(timeZone);
+ if (!hasTimeZone) {
+ throw new IllegalArgumentException("Timezone: " + timeZone + " is not an Olson ID");
+ }
+ }
+
+ try {
+ mService.setTimeZone(timeZone);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public long getNextWakeFromIdleTime() {
+ try {
+ return mService.getNextWakeFromIdleTime();
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets information about the next alarm clock currently scheduled.
+ *
+ * The alarm clocks considered are those scheduled by any application
+ * using the {@link #setAlarmClock} method.
+ *
+ * @return An {@link AlarmClockInfo} object describing the next upcoming alarm
+ * clock event that will occur. If there are no alarm clock events currently
+ * scheduled, this method will return {@code null}.
+ *
+ * @see #setAlarmClock
+ * @see AlarmClockInfo
+ * @see #ACTION_NEXT_ALARM_CLOCK_CHANGED
+ */
+ public AlarmClockInfo getNextAlarmClock() {
+ return getNextAlarmClock(mContext.getUserId());
+ }
+
+ /**
+ * Gets information about the next alarm clock currently scheduled.
+ *
+ * The alarm clocks considered are those scheduled by any application
+ * using the {@link #setAlarmClock} method within the given user.
+ *
+ * @return An {@link AlarmClockInfo} object describing the next upcoming alarm
+ * clock event that will occur within the given user. If there are no alarm clock
+ * events currently scheduled in that user, this method will return {@code null}.
+ *
+ * @see #setAlarmClock
+ * @see AlarmClockInfo
+ * @see #ACTION_NEXT_ALARM_CLOCK_CHANGED
+ *
+ * @hide
+ */
+ public AlarmClockInfo getNextAlarmClock(int userId) {
+ try {
+ return mService.getNextAlarmClock(userId);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * An immutable description of a scheduled "alarm clock" event.
+ *
+ * @see AlarmManager#setAlarmClock
+ * @see AlarmManager#getNextAlarmClock
+ */
+ public static final class AlarmClockInfo implements Parcelable {
+
+ private final long mTriggerTime;
+ private final PendingIntent mShowIntent;
+
+ /**
+ * Creates a new alarm clock description.
+ *
+ * @param triggerTime time at which the underlying alarm is triggered in wall time
+ * milliseconds since the epoch
+ * @param showIntent an intent that can be used to show or edit details of
+ * the alarm clock.
+ */
+ public AlarmClockInfo(long triggerTime, PendingIntent showIntent) {
+ mTriggerTime = triggerTime;
+ mShowIntent = showIntent;
+ }
+
+ /**
+ * Use the {@link #CREATOR}
+ * @hide
+ */
+ AlarmClockInfo(Parcel in) {
+ mTriggerTime = in.readLong();
+ mShowIntent = in.readParcelable(PendingIntent.class.getClassLoader());
+ }
+
+ /**
+ * Returns the time at which the alarm is going to trigger.
+ *
+ * This value is UTC wall clock time in milliseconds, as returned by
+ * {@link System#currentTimeMillis()} for example.
+ */
+ public long getTriggerTime() {
+ return mTriggerTime;
+ }
+
+ /**
+ * Returns an intent that can be used to show or edit details of the alarm clock in
+ * the application that scheduled it.
+ *
+ * <p class="note">Beware that any application can retrieve and send this intent,
+ * potentially with additional fields filled in. See
+ * {@link PendingIntent#send(android.content.Context, int, android.content.Intent)
+ * PendingIntent.send()} and {@link android.content.Intent#fillIn Intent.fillIn()}
+ * for details.
+ */
+ public PendingIntent getShowIntent() {
+ return mShowIntent;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(mTriggerTime);
+ dest.writeParcelable(mShowIntent, flags);
+ }
+
+ public static final @android.annotation.NonNull Creator<AlarmClockInfo> CREATOR = new Creator<AlarmClockInfo>() {
+ @Override
+ public AlarmClockInfo createFromParcel(Parcel in) {
+ return new AlarmClockInfo(in);
+ }
+
+ @Override
+ public AlarmClockInfo[] newArray(int size) {
+ return new AlarmClockInfo[size];
+ }
+ };
+
+ /** @hide */
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(AlarmClockInfoProto.TRIGGER_TIME_MS, mTriggerTime);
+ if (mShowIntent != null) {
+ mShowIntent.dumpDebug(proto, AlarmClockInfoProto.SHOW_INTENT);
+ }
+ proto.end(token);
+ }
+ }
+}
diff --git a/apex/jobscheduler/framework/java/android/app/IAlarmCompleteListener.aidl b/apex/jobscheduler/framework/java/android/app/IAlarmCompleteListener.aidl
new file mode 100644
index 000000000000..9f9ee409fda4
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/app/IAlarmCompleteListener.aidl
@@ -0,0 +1,27 @@
+/*
+** Copyright 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.app;
+
+import android.os.IBinder;
+
+/**
+ * Callback from app into system process to indicate that processing of
+ * a direct-call alarm has completed.
+ * {@hide}
+ */
+interface IAlarmCompleteListener {
+ void alarmComplete(in IBinder who);
+}
diff --git a/apex/jobscheduler/framework/java/android/app/IAlarmListener.aidl b/apex/jobscheduler/framework/java/android/app/IAlarmListener.aidl
new file mode 100644
index 000000000000..a110d4ddf2c8
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/app/IAlarmListener.aidl
@@ -0,0 +1,29 @@
+/*
+** Copyright 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.app;
+
+import android.app.IAlarmCompleteListener;
+
+/**
+ * System private API for direct alarm callbacks (non-broadcast deliver). See the
+ * AlarmManager#set() variants that take an AlarmReceiver callback object
+ * rather than a PendingIntent.
+ *
+ * {@hide}
+ */
+oneway interface IAlarmListener {
+ void doAlarm(in IAlarmCompleteListener callback);
+}
diff --git a/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl b/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl
new file mode 100644
index 000000000000..6f624ee672e6
--- /dev/null
+++ b/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl
@@ -0,0 +1,44 @@
+/* //device/java/android/android/app/IAlarmManager.aidl
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES 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.AlarmManager;
+import android.app.IAlarmListener;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.WorkSource;
+
+/**
+ * System private API for talking with the alarm manager service.
+ *
+ * {@hide}
+ */
+interface IAlarmManager {
+ /** windowLength == 0 means exact; windowLength < 0 means the let the OS decide */
+ @UnsupportedAppUsage
+ void set(String callingPackage, int type, long triggerAtTime, long windowLength,
+ long interval, int flags, in PendingIntent operation, in IAlarmListener listener,
+ String listenerTag, in WorkSource workSource, in AlarmManager.AlarmClockInfo alarmClock);
+ @UnsupportedAppUsage
+ boolean setTime(long millis);
+ void setTimeZone(String zone);
+ void remove(in PendingIntent operation, in IAlarmListener listener);
+ long getNextWakeFromIdleTime();
+ @UnsupportedAppUsage
+ AlarmManager.AlarmClockInfo getNextAlarmClock(int userId);
+ long currentNetworkTimeMillis();
+}
diff --git a/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java
index f863718d6ce7..ada562ecebc6 100644
--- a/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java
+++ b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java
@@ -17,6 +17,8 @@
package android.os;
import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.content.Context;
@@ -28,6 +30,7 @@ import android.content.Context;
* @hide
*/
@TestApi
+@SystemApi
@SystemService(Context.DEVICE_IDLE_CONTROLLER)
public class DeviceIdleManager {
private final Context mContext;
@@ -46,9 +49,26 @@ public class DeviceIdleManager {
}
/**
+ * Ends any active idle session.
+ *
+ * @param reason The reason to end. Used for debugging purposes.
+ */
+ @RequiresPermission(android.Manifest.permission.DEVICE_POWER)
+ public void endIdle(@NonNull String reason) {
+ try {
+ mService.exitIdle(reason);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* @return package names the system has white-listed to opt out of power save restrictions,
* except for device idle mode.
+ *
+ * @hide Should be migrated to PowerWhitelistManager
*/
+ @TestApi
public @NonNull String[] getSystemPowerWhitelistExceptIdle() {
try {
return mService.getSystemPowerWhitelistExceptIdle();
@@ -60,7 +80,10 @@ public class DeviceIdleManager {
/**
* @return package names the system has white-listed to opt out of power save restrictions for
* all modes.
+ *
+ * @hide Should be migrated to PowerWhitelistManager
*/
+ @TestApi
public @NonNull String[] getSystemPowerWhitelist() {
try {
return mService.getSystemPowerWhitelist();
diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
index 887d82c6413f..648a1e1722be 100644
--- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
@@ -8,8 +8,7 @@ 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 android.util.IndentingPrintWriter;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
diff --git a/apex/jobscheduler/service/Android.bp b/apex/jobscheduler/service/Android.bp
index 69a9fd844729..47267dfc90aa 100644
--- a/apex/jobscheduler/service/Android.bp
+++ b/apex/jobscheduler/service/Android.bp
@@ -6,11 +6,17 @@ java_library {
srcs: [
"java/**/*.java",
+ ":framework-jobscheduler-shared-srcs",
+ ":statslog-framework-java-gen", // FrameworkStatsLog.java
],
libs: [
"app-compat-annotations",
"framework",
"services.core",
+ "unsupportedappusage",
],
+
+ // Rename classes shared with the framework
+ jarjar_rules: "jarjar-rules.txt",
}
diff --git a/apex/jobscheduler/service/jarjar-rules.txt b/apex/jobscheduler/service/jarjar-rules.txt
new file mode 100644
index 000000000000..2f01c4b570d8
--- /dev/null
+++ b/apex/jobscheduler/service/jarjar-rules.txt
@@ -0,0 +1,18 @@
+# Rename all com.android.internal.util classes to prevent class name collisions
+# between this module and the other versions of the utility classes linked into
+# the framework.
+
+# These must be kept in sync with the framework-jobscheduler-shared-srcs filegroup.
+rule com.android.internal.util.ArrayUtils* com.android.internal.util.jobs.ArrayUtils@1
+rule com.android.internal.util.BitUtils* com.android.internal.util.jobs.BitUtils@1
+rule com.android.internal.util.CollectionUtils* com.android.internal.util.jobs.CollectionUtils@1
+rule com.android.internal.util.ConcurrentUtils* com.android.internal.util.jobs.ConcurrentUtils@1
+rule com.android.internal.util.DumpUtils* com.android.internal.util.jobs.DumpUtils@1
+rule com.android.internal.util.FastPrintWriter* com.android.internal.util.jobs.FastPrintWriter@1
+rule com.android.internal.util.FastXmlSerializer* com.android.internal.util.jobs.FastXmlSerializer@1
+rule com.android.internal.util.FunctionalUtils* com.android.internal.util.jobs.FunctionalUtils@1
+rule com.android.internal.util.ParseUtils* com.android.internal.util.jobs.ParseUtils@1
+rule com.android.internal.util.Preconditions* com.android.internal.util.jobs.Preconditions@1
+rule com.android.internal.util.RingBufferIndices* com.android.internal.util.jobs.RingBufferIndices@1
+rule com.android.internal.util.StatLogger* com.android.internal.util.jobs.StatLogger@1
+rule com.android.internal.util.XmlUtils* com.android.internal.util.jobs.XmlUtils@1
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
new file mode 100644
index 000000000000..42c56c1adfbe
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -0,0 +1,4888 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.alarm;
+
+import static android.app.AlarmManager.ELAPSED_REALTIME;
+import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
+import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE;
+import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
+import static android.app.AlarmManager.RTC;
+import static android.app.AlarmManager.RTC_WAKEUP;
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+import static android.os.UserHandle.USER_SYSTEM;
+
+import android.annotation.UserIdInt;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
+import android.app.IAlarmCompleteListener;
+import android.app.IAlarmListener;
+import android.app.IAlarmManager;
+import android.app.IUidObserver;
+import android.app.PendingIntent;
+import android.app.usage.UsageStatsManager;
+import android.app.usage.UsageStatsManagerInternal;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManagerInternal;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.BatteryManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelableException;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.ThreadLocalWorkSource;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.provider.Settings;
+import android.system.Os;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.KeyValueListParser;
+import android.util.Log;
+import android.util.LongArrayQueue;
+import android.util.MutableBoolean;
+import android.util.NtpTrustedTime;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import android.util.SparseLongArray;
+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.util.ArrayUtils;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.LocalLog;
+import com.android.internal.util.StatLogger;
+import com.android.server.AlarmManagerInternal;
+import com.android.server.AppStateTracker;
+import com.android.server.AppStateTracker.Listener;
+import com.android.server.DeviceIdleInternal;
+import com.android.server.EventLogTags;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.SystemServiceManager;
+import com.android.server.usage.AppStandbyInternal;
+import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.time.DateTimeException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Random;
+import java.util.TimeZone;
+import java.util.TreeSet;
+import java.util.function.Predicate;
+
+/**
+ * Alarm manager implementation.
+ *
+ * Unit test:
+ atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/AlarmManagerServiceTest.java
+ */
+public class AlarmManagerService extends SystemService {
+ private static final int RTC_WAKEUP_MASK = 1 << RTC_WAKEUP;
+ private static final int RTC_MASK = 1 << RTC;
+ private static final int ELAPSED_REALTIME_WAKEUP_MASK = 1 << ELAPSED_REALTIME_WAKEUP;
+ private static final int ELAPSED_REALTIME_MASK = 1 << ELAPSED_REALTIME;
+ static final int TIME_CHANGED_MASK = 1 << 16;
+ static final int IS_WAKEUP_MASK = RTC_WAKEUP_MASK|ELAPSED_REALTIME_WAKEUP_MASK;
+
+ // Mask for testing whether a given alarm type is wakeup vs non-wakeup
+ static final int TYPE_NONWAKEUP_MASK = 0x1; // low bit => non-wakeup
+
+ static final String TAG = "AlarmManager";
+ static final boolean localLOGV = false;
+ static final boolean DEBUG_BATCH = localLOGV || false;
+ static final boolean DEBUG_VALIDATE = localLOGV || false;
+ static final boolean DEBUG_ALARM_CLOCK = localLOGV || false;
+ static final boolean DEBUG_LISTENER_CALLBACK = localLOGV || false;
+ static final boolean DEBUG_WAKELOCK = localLOGV || false;
+ static final boolean DEBUG_BG_LIMIT = localLOGV || false;
+ static final boolean DEBUG_STANDBY = localLOGV || false;
+ static final boolean RECORD_ALARMS_IN_HISTORY = true;
+ static final boolean RECORD_DEVICE_IDLE_ALARMS = false;
+ static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
+
+ static final int TICK_HISTORY_DEPTH = 10;
+ static final long MILLIS_IN_DAY = 24 * 60 * 60 * 1000;
+
+ // Indices into the KEYS_APP_STANDBY_QUOTAS array.
+ static final int ACTIVE_INDEX = 0;
+ static final int WORKING_INDEX = 1;
+ static final int FREQUENT_INDEX = 2;
+ static final int RARE_INDEX = 3;
+ static final int NEVER_INDEX = 4;
+
+ private final Intent mBackgroundIntent
+ = new Intent().addFlags(Intent.FLAG_FROM_BACKGROUND);
+ static final IncreasingTimeOrder sIncreasingTimeOrder = new IncreasingTimeOrder();
+
+ static final boolean WAKEUP_STATS = false;
+
+ private static final Intent NEXT_ALARM_CLOCK_CHANGED_INTENT =
+ new Intent(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)
+ .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+ | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+
+ final LocalLog mLog = new LocalLog(TAG);
+
+ AppOpsManager mAppOps;
+ DeviceIdleInternal mLocalDeviceIdleController;
+ private UsageStatsManagerInternal mUsageStatsManagerInternal;
+
+ final Object mLock = new Object();
+
+ // List of alarms per uid deferred due to user applied background restrictions on the source app
+ SparseArray<ArrayList<Alarm>> mPendingBackgroundAlarms = new SparseArray<>();
+ private long mNextWakeup;
+ private long mNextNonWakeup;
+ private long mNextWakeUpSetAt;
+ private long mNextNonWakeUpSetAt;
+ private long mLastWakeup;
+ private long mLastTrigger;
+
+ private long mLastTickSet;
+ private long mLastTickReceived;
+ private long mLastTickAdded;
+ private long mLastTickRemoved;
+ // ring buffer of recent TIME_TICK issuance, in the elapsed timebase
+ private final long[] mTickHistory = new long[TICK_HISTORY_DEPTH];
+ private int mNextTickHistory;
+
+ private final Injector mInjector;
+ int mBroadcastRefCount = 0;
+ PowerManager.WakeLock mWakeLock;
+ SparseIntArray mAlarmsPerUid = new SparseIntArray();
+ ArrayList<Alarm> mPendingNonWakeupAlarms = new ArrayList<>();
+ ArrayList<InFlight> mInFlight = new ArrayList<>();
+ private final ArrayList<AlarmManagerInternal.InFlightListener> mInFlightListeners =
+ new ArrayList<>();
+ AlarmHandler mHandler;
+ AppWakeupHistory mAppWakeupHistory;
+ ClockReceiver mClockReceiver;
+ final DeliveryTracker mDeliveryTracker = new DeliveryTracker();
+ IBinder.DeathRecipient mListenerDeathRecipient;
+ Intent mTimeTickIntent;
+ IAlarmListener mTimeTickTrigger;
+ PendingIntent mDateChangeSender;
+ Random mRandom;
+ boolean mInteractive = true;
+ long mNonInteractiveStartTime;
+ long mNonInteractiveTime;
+ long mLastAlarmDeliveryTime;
+ long mStartCurrentDelayTime;
+ long mNextNonWakeupDeliveryTime;
+ long mLastTimeChangeClockTime;
+ long mLastTimeChangeRealtime;
+ int mNumTimeChanged;
+
+ /**
+ * At boot we use SYSTEM_UI_SELF_PERMISSION to look up the definer's uid.
+ */
+ int mSystemUiUid;
+
+ /**
+ * For each uid, this is the last time we dispatched an "allow while idle" alarm,
+ * used to determine the earliest we can dispatch the next such alarm. Times are in the
+ * 'elapsed' timebase.
+ */
+ final SparseLongArray mLastAllowWhileIdleDispatch = new SparseLongArray();
+
+ /**
+ * For each uid, we store whether the last allow-while-idle alarm was dispatched while
+ * the uid was in foreground or not. We will use the allow_while_idle_short_time in such cases.
+ */
+ final SparseBooleanArray mUseAllowWhileIdleShortTime = new SparseBooleanArray();
+
+ final static class IdleDispatchEntry {
+ int uid;
+ String pkg;
+ String tag;
+ String op;
+ long elapsedRealtime;
+ long argRealtime;
+ }
+ final ArrayList<IdleDispatchEntry> mAllowWhileIdleDispatches = new ArrayList();
+
+ interface Stats {
+ int REBATCH_ALL_ALARMS = 0;
+ int REORDER_ALARMS_FOR_STANDBY = 1;
+ }
+
+ private final StatLogger mStatLogger = new StatLogger(new String[] {
+ "REBATCH_ALL_ALARMS",
+ "REORDER_ALARMS_FOR_STANDBY",
+ });
+
+ /**
+ * Broadcast options to use for FLAG_ALLOW_WHILE_IDLE.
+ */
+ Bundle mIdleOptions;
+
+ private final SparseArray<AlarmManager.AlarmClockInfo> mNextAlarmClockForUser =
+ new SparseArray<>();
+ private final SparseArray<AlarmManager.AlarmClockInfo> mTmpSparseAlarmClockArray =
+ new SparseArray<>();
+ private final SparseBooleanArray mPendingSendNextAlarmClockChangedForUser =
+ new SparseBooleanArray();
+ private boolean mNextAlarmClockMayChange;
+
+ // May only use on mHandler's thread, locking not required.
+ private final SparseArray<AlarmManager.AlarmClockInfo> mHandlerSparseAlarmClockArray =
+ new SparseArray<>();
+
+ private AppStateTracker mAppStateTracker;
+ private boolean mAppStandbyParole;
+
+ /**
+ * A rolling window history of previous times when an alarm was sent to a package.
+ */
+ private static class AppWakeupHistory {
+ private ArrayMap<Pair<String, Integer>, LongArrayQueue> mPackageHistory =
+ new ArrayMap<>();
+ private long mWindowSize;
+
+ AppWakeupHistory(long windowSize) {
+ mWindowSize = windowSize;
+ }
+
+ void recordAlarmForPackage(String packageName, int userId, long nowElapsed) {
+ final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
+ LongArrayQueue history = mPackageHistory.get(packageUser);
+ if (history == null) {
+ history = new LongArrayQueue();
+ mPackageHistory.put(packageUser, history);
+ }
+ if (history.size() == 0 || history.peekLast() < nowElapsed) {
+ history.addLast(nowElapsed);
+ }
+ snapToWindow(history);
+ }
+
+ void removeForUser(int userId) {
+ for (int i = mPackageHistory.size() - 1; i >= 0; i--) {
+ final Pair<String, Integer> packageUserKey = mPackageHistory.keyAt(i);
+ if (packageUserKey.second == userId) {
+ mPackageHistory.removeAt(i);
+ }
+ }
+ }
+
+ void removeForPackage(String packageName, int userId) {
+ final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
+ mPackageHistory.remove(packageUser);
+ }
+
+ private void snapToWindow(LongArrayQueue history) {
+ while (history.peekFirst() + mWindowSize < history.peekLast()) {
+ history.removeFirst();
+ }
+ }
+
+ int getTotalWakeupsInWindow(String packageName, int userId) {
+ final LongArrayQueue history = mPackageHistory.get(Pair.create(packageName, userId));
+ return (history == null) ? 0 : history.size();
+ }
+
+ /**
+ * @param n The desired nth-last wakeup
+ * (1=1st-last=the ultimate wakeup and 2=2nd-last=the penultimate wakeup)
+ */
+ long getNthLastWakeupForPackage(String packageName, int userId, int n) {
+ final LongArrayQueue history = mPackageHistory.get(Pair.create(packageName, userId));
+ if (history == null) {
+ return 0;
+ }
+ final int i = history.size() - n;
+ return (i < 0) ? 0 : history.get(i);
+ }
+
+ void dump(PrintWriter pw, String prefix, long nowElapsed) {
+ dump(new IndentingPrintWriter(pw, " ").setIndent(prefix), nowElapsed);
+ }
+
+ void dump(IndentingPrintWriter pw, long nowElapsed) {
+ pw.println("App Alarm history:");
+ pw.increaseIndent();
+ for (int i = 0; i < mPackageHistory.size(); i++) {
+ final Pair<String, Integer> packageUser = mPackageHistory.keyAt(i);
+ final LongArrayQueue timestamps = mPackageHistory.valueAt(i);
+ pw.print(packageUser.first);
+ pw.print(", u");
+ pw.print(packageUser.second);
+ pw.print(": ");
+ // limit dumping to a max of 100 values
+ final int lastIdx = Math.max(0, timestamps.size() - 100);
+ for (int j = timestamps.size() - 1; j >= lastIdx; j--) {
+ TimeUtils.formatDuration(timestamps.get(j), nowElapsed, pw);
+ pw.print(", ");
+ }
+ pw.println();
+ }
+ pw.decreaseIndent();
+ }
+ }
+
+ /**
+ * 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 AlarmManagerService.mLock lock.
+ */
+ @VisibleForTesting
+ final class Constants extends ContentObserver {
+ // Key names stored in the settings value.
+ @VisibleForTesting
+ static final String KEY_MIN_FUTURITY = "min_futurity";
+ @VisibleForTesting
+ static final String KEY_MIN_INTERVAL = "min_interval";
+ @VisibleForTesting
+ static final String KEY_MAX_INTERVAL = "max_interval";
+ @VisibleForTesting
+ static final String KEY_ALLOW_WHILE_IDLE_SHORT_TIME = "allow_while_idle_short_time";
+ @VisibleForTesting
+ static final String KEY_ALLOW_WHILE_IDLE_LONG_TIME = "allow_while_idle_long_time";
+ @VisibleForTesting
+ static final String KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION
+ = "allow_while_idle_whitelist_duration";
+ @VisibleForTesting
+ static final String KEY_LISTENER_TIMEOUT = "listener_timeout";
+ @VisibleForTesting
+ static final String KEY_MAX_ALARMS_PER_UID = "max_alarms_per_uid";
+ private static final String KEY_APP_STANDBY_WINDOW = "app_standby_window";
+ @VisibleForTesting
+ final String[] KEYS_APP_STANDBY_QUOTAS = {
+ "standby_active_quota",
+ "standby_working_quota",
+ "standby_frequent_quota",
+ "standby_rare_quota",
+ "standby_never_quota",
+ };
+ // Not putting this in the KEYS_APP_STANDBY_QUOTAS array because this uses a different
+ // window size.
+ private static final String KEY_APP_STANDBY_RESTRICTED_QUOTA = "standby_restricted_quota";
+ private static final String KEY_APP_STANDBY_RESTRICTED_WINDOW =
+ "app_standby_restricted_window";
+
+ private static final long DEFAULT_MIN_FUTURITY = 5 * 1000;
+ private static final long DEFAULT_MIN_INTERVAL = 60 * 1000;
+ private static final long DEFAULT_MAX_INTERVAL = 365 * DateUtils.DAY_IN_MILLIS;
+ private static final long DEFAULT_ALLOW_WHILE_IDLE_SHORT_TIME = DEFAULT_MIN_FUTURITY;
+ private static final long DEFAULT_ALLOW_WHILE_IDLE_LONG_TIME = 9*60*1000;
+ private static final long DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION = 10*1000;
+ private static final long DEFAULT_LISTENER_TIMEOUT = 5 * 1000;
+ private static final int DEFAULT_MAX_ALARMS_PER_UID = 500;
+ private static final long DEFAULT_APP_STANDBY_WINDOW = 60 * 60 * 1000; // 1 hr
+ /**
+ * Max number of times an app can receive alarms in {@link #APP_STANDBY_WINDOW}
+ */
+ private final int[] DEFAULT_APP_STANDBY_QUOTAS = {
+ 720, // Active
+ 10, // Working
+ 2, // Frequent
+ 1, // Rare
+ 0 // Never
+ };
+ private static final int DEFAULT_APP_STANDBY_RESTRICTED_QUOTA = 1;
+ private static final long DEFAULT_APP_STANDBY_RESTRICTED_WINDOW = MILLIS_IN_DAY;
+
+ // Minimum futurity of a new alarm
+ public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY;
+
+ // Minimum alarm recurrence interval
+ public long MIN_INTERVAL = DEFAULT_MIN_INTERVAL;
+
+ // Maximum alarm recurrence interval
+ public long MAX_INTERVAL = DEFAULT_MAX_INTERVAL;
+
+ // Minimum time between ALLOW_WHILE_IDLE alarms when system is not idle.
+ public long ALLOW_WHILE_IDLE_SHORT_TIME = DEFAULT_ALLOW_WHILE_IDLE_SHORT_TIME;
+
+ // Minimum time between ALLOW_WHILE_IDLE alarms when system is idling.
+ public long ALLOW_WHILE_IDLE_LONG_TIME = DEFAULT_ALLOW_WHILE_IDLE_LONG_TIME;
+
+ // BroadcastOptions.setTemporaryAppWhitelistDuration() to use for FLAG_ALLOW_WHILE_IDLE.
+ public long ALLOW_WHILE_IDLE_WHITELIST_DURATION
+ = DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION;
+
+ // Direct alarm listener callback timeout
+ public long LISTENER_TIMEOUT = DEFAULT_LISTENER_TIMEOUT;
+ public int MAX_ALARMS_PER_UID = DEFAULT_MAX_ALARMS_PER_UID;
+
+ public long APP_STANDBY_WINDOW = DEFAULT_APP_STANDBY_WINDOW;
+ public int[] APP_STANDBY_QUOTAS = new int[DEFAULT_APP_STANDBY_QUOTAS.length];
+ public int APP_STANDBY_RESTRICTED_QUOTA = DEFAULT_APP_STANDBY_RESTRICTED_QUOTA;
+ public long APP_STANDBY_RESTRICTED_WINDOW = DEFAULT_APP_STANDBY_RESTRICTED_WINDOW;
+
+ private ContentResolver mResolver;
+ private final KeyValueListParser mParser = new KeyValueListParser(',');
+ private long mLastAllowWhileIdleWhitelistDuration = -1;
+
+ public Constants(Handler handler) {
+ super(handler);
+ updateAllowWhileIdleWhitelistDurationLocked();
+ }
+
+ public void start(ContentResolver resolver) {
+ mResolver = resolver;
+ mResolver.registerContentObserver(Settings.Global.getUriFor(
+ Settings.Global.ALARM_MANAGER_CONSTANTS), false, this);
+ updateConstants();
+ }
+
+ public void updateAllowWhileIdleWhitelistDurationLocked() {
+ if (mLastAllowWhileIdleWhitelistDuration != ALLOW_WHILE_IDLE_WHITELIST_DURATION) {
+ mLastAllowWhileIdleWhitelistDuration = ALLOW_WHILE_IDLE_WHITELIST_DURATION;
+ BroadcastOptions opts = BroadcastOptions.makeBasic();
+ opts.setTemporaryAppWhitelistDuration(ALLOW_WHILE_IDLE_WHITELIST_DURATION);
+ mIdleOptions = opts.toBundle();
+ }
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ updateConstants();
+ }
+
+ private void updateConstants() {
+ synchronized (mLock) {
+ try {
+ mParser.setString(Settings.Global.getString(mResolver,
+ Settings.Global.ALARM_MANAGER_CONSTANTS));
+ } catch (IllegalArgumentException e) {
+ // Failed to parse the settings string, log this and move on
+ // with defaults.
+ Slog.e(TAG, "Bad alarm manager settings", e);
+ }
+
+ MIN_FUTURITY = mParser.getLong(KEY_MIN_FUTURITY, DEFAULT_MIN_FUTURITY);
+ MIN_INTERVAL = mParser.getLong(KEY_MIN_INTERVAL, DEFAULT_MIN_INTERVAL);
+ MAX_INTERVAL = mParser.getLong(KEY_MAX_INTERVAL, DEFAULT_MAX_INTERVAL);
+ ALLOW_WHILE_IDLE_SHORT_TIME = mParser.getLong(KEY_ALLOW_WHILE_IDLE_SHORT_TIME,
+ DEFAULT_ALLOW_WHILE_IDLE_SHORT_TIME);
+ ALLOW_WHILE_IDLE_LONG_TIME = mParser.getLong(KEY_ALLOW_WHILE_IDLE_LONG_TIME,
+ DEFAULT_ALLOW_WHILE_IDLE_LONG_TIME);
+ ALLOW_WHILE_IDLE_WHITELIST_DURATION = mParser.getLong(
+ KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION,
+ DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION);
+ LISTENER_TIMEOUT = mParser.getLong(KEY_LISTENER_TIMEOUT,
+ DEFAULT_LISTENER_TIMEOUT);
+
+ APP_STANDBY_WINDOW = mParser.getLong(KEY_APP_STANDBY_WINDOW,
+ DEFAULT_APP_STANDBY_WINDOW);
+ if (APP_STANDBY_WINDOW > DEFAULT_APP_STANDBY_WINDOW) {
+ Slog.w(TAG, "Cannot exceed the app_standby_window size of "
+ + DEFAULT_APP_STANDBY_WINDOW);
+ APP_STANDBY_WINDOW = DEFAULT_APP_STANDBY_WINDOW;
+ } else if (APP_STANDBY_WINDOW < DEFAULT_APP_STANDBY_WINDOW) {
+ // Not recommended outside of testing.
+ Slog.w(TAG, "Using a non-default app_standby_window of " + APP_STANDBY_WINDOW);
+ }
+
+ APP_STANDBY_QUOTAS[ACTIVE_INDEX] = mParser.getInt(
+ KEYS_APP_STANDBY_QUOTAS[ACTIVE_INDEX],
+ DEFAULT_APP_STANDBY_QUOTAS[ACTIVE_INDEX]);
+ for (int i = WORKING_INDEX; i < KEYS_APP_STANDBY_QUOTAS.length; i++) {
+ APP_STANDBY_QUOTAS[i] = mParser.getInt(KEYS_APP_STANDBY_QUOTAS[i],
+ Math.min(APP_STANDBY_QUOTAS[i - 1], DEFAULT_APP_STANDBY_QUOTAS[i]));
+ }
+
+ APP_STANDBY_RESTRICTED_QUOTA = Math.max(1,
+ mParser.getInt(KEY_APP_STANDBY_RESTRICTED_QUOTA,
+ DEFAULT_APP_STANDBY_RESTRICTED_QUOTA));
+
+ APP_STANDBY_RESTRICTED_WINDOW = Math.max(APP_STANDBY_WINDOW,
+ mParser.getLong(KEY_APP_STANDBY_RESTRICTED_WINDOW,
+ DEFAULT_APP_STANDBY_RESTRICTED_WINDOW));
+
+ MAX_ALARMS_PER_UID = mParser.getInt(KEY_MAX_ALARMS_PER_UID,
+ DEFAULT_MAX_ALARMS_PER_UID);
+ if (MAX_ALARMS_PER_UID < DEFAULT_MAX_ALARMS_PER_UID) {
+ Slog.w(TAG, "Cannot set " + KEY_MAX_ALARMS_PER_UID + " lower than "
+ + DEFAULT_MAX_ALARMS_PER_UID);
+ MAX_ALARMS_PER_UID = DEFAULT_MAX_ALARMS_PER_UID;
+ }
+
+ updateAllowWhileIdleWhitelistDurationLocked();
+ }
+ }
+
+ void dump(PrintWriter pw, String prefix) {
+ dump(new IndentingPrintWriter(pw, " ").setIndent(prefix));
+ }
+
+ void dump(IndentingPrintWriter pw) {
+ pw.println("Settings:");
+
+ pw.increaseIndent();
+
+ pw.print(KEY_MIN_FUTURITY); pw.print("=");
+ TimeUtils.formatDuration(MIN_FUTURITY, pw);
+ pw.println();
+
+ pw.print(KEY_MIN_INTERVAL); pw.print("=");
+ TimeUtils.formatDuration(MIN_INTERVAL, pw);
+ pw.println();
+
+ pw.print(KEY_MAX_INTERVAL); pw.print("=");
+ TimeUtils.formatDuration(MAX_INTERVAL, pw);
+ pw.println();
+
+ pw.print(KEY_LISTENER_TIMEOUT); pw.print("=");
+ TimeUtils.formatDuration(LISTENER_TIMEOUT, pw);
+ pw.println();
+
+ pw.print(KEY_ALLOW_WHILE_IDLE_SHORT_TIME); pw.print("=");
+ TimeUtils.formatDuration(ALLOW_WHILE_IDLE_SHORT_TIME, pw);
+ pw.println();
+
+ pw.print(KEY_ALLOW_WHILE_IDLE_LONG_TIME); pw.print("=");
+ TimeUtils.formatDuration(ALLOW_WHILE_IDLE_LONG_TIME, pw);
+ pw.println();
+
+ pw.print(KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION); pw.print("=");
+ TimeUtils.formatDuration(ALLOW_WHILE_IDLE_WHITELIST_DURATION, pw);
+ pw.println();
+
+ pw.print(KEY_MAX_ALARMS_PER_UID); pw.print("=");
+ pw.println(MAX_ALARMS_PER_UID);
+
+ pw.print(KEY_APP_STANDBY_WINDOW); pw.print("=");
+ TimeUtils.formatDuration(APP_STANDBY_WINDOW, pw);
+ pw.println();
+
+ for (int i = 0; i < KEYS_APP_STANDBY_QUOTAS.length; i++) {
+ pw.print(KEYS_APP_STANDBY_QUOTAS[i]); pw.print("=");
+ pw.println(APP_STANDBY_QUOTAS[i]);
+ }
+
+ pw.print(KEY_APP_STANDBY_RESTRICTED_QUOTA); pw.print("=");
+ pw.println(APP_STANDBY_RESTRICTED_QUOTA);
+
+ pw.print(KEY_APP_STANDBY_RESTRICTED_WINDOW); pw.print("=");
+ TimeUtils.formatDuration(APP_STANDBY_RESTRICTED_WINDOW, pw);
+ pw.println();
+
+ pw.decreaseIndent();
+ }
+
+ void dumpProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(ConstantsProto.MIN_FUTURITY_DURATION_MS, MIN_FUTURITY);
+ proto.write(ConstantsProto.MIN_INTERVAL_DURATION_MS, MIN_INTERVAL);
+ proto.write(ConstantsProto.MAX_INTERVAL_DURATION_MS, MAX_INTERVAL);
+ proto.write(ConstantsProto.LISTENER_TIMEOUT_DURATION_MS, LISTENER_TIMEOUT);
+ proto.write(ConstantsProto.ALLOW_WHILE_IDLE_SHORT_DURATION_MS,
+ ALLOW_WHILE_IDLE_SHORT_TIME);
+ proto.write(ConstantsProto.ALLOW_WHILE_IDLE_LONG_DURATION_MS,
+ ALLOW_WHILE_IDLE_LONG_TIME);
+ proto.write(ConstantsProto.ALLOW_WHILE_IDLE_WHITELIST_DURATION_MS,
+ ALLOW_WHILE_IDLE_WHITELIST_DURATION);
+
+ proto.end(token);
+ }
+ }
+
+ Constants mConstants;
+
+ // Alarm delivery ordering bookkeeping
+ static final int PRIO_TICK = 0;
+ static final int PRIO_WAKEUP = 1;
+ static final int PRIO_NORMAL = 2;
+
+ final class PriorityClass {
+ int seq;
+ int priority;
+
+ PriorityClass() {
+ seq = mCurrentSeq - 1;
+ priority = PRIO_NORMAL;
+ }
+ }
+
+ final HashMap<String, PriorityClass> mPriorities = new HashMap<>();
+ int mCurrentSeq = 0;
+
+ static final class WakeupEvent {
+ public long when;
+ public int uid;
+ public String action;
+
+ public WakeupEvent(long theTime, int theUid, String theAction) {
+ when = theTime;
+ uid = theUid;
+ action = theAction;
+ }
+ }
+
+ final LinkedList<WakeupEvent> mRecentWakeups = new LinkedList<WakeupEvent>();
+ final long RECENT_WAKEUP_PERIOD = 1000L * 60 * 60 * 24; // one day
+
+ final class Batch {
+ long start; // These endpoints are always in ELAPSED
+ long end;
+ int flags; // Flags for alarms, such as FLAG_STANDALONE.
+
+ final ArrayList<Alarm> alarms = new ArrayList<Alarm>();
+
+ Batch(Alarm seed) {
+ start = seed.whenElapsed;
+ end = clampPositive(seed.maxWhenElapsed);
+ flags = seed.flags;
+ alarms.add(seed);
+ if (seed.listener == mTimeTickTrigger) {
+ mLastTickAdded = mInjector.getCurrentTimeMillis();
+ }
+ }
+
+ int size() {
+ return alarms.size();
+ }
+
+ Alarm get(int index) {
+ return alarms.get(index);
+ }
+
+ boolean canHold(long whenElapsed, long maxWhen) {
+ return (end >= whenElapsed) && (start <= maxWhen);
+ }
+
+ boolean add(Alarm alarm) {
+ boolean newStart = false;
+ // narrows the batch if necessary; presumes that canHold(alarm) is true
+ int index = Collections.binarySearch(alarms, alarm, sIncreasingTimeOrder);
+ if (index < 0) {
+ index = 0 - index - 1;
+ }
+ alarms.add(index, alarm);
+ if (alarm.listener == mTimeTickTrigger) {
+ mLastTickAdded = mInjector.getCurrentTimeMillis();
+ }
+ if (DEBUG_BATCH) {
+ Slog.v(TAG, "Adding " + alarm + " to " + this);
+ }
+ if (alarm.whenElapsed > start) {
+ start = alarm.whenElapsed;
+ newStart = true;
+ }
+ if (alarm.maxWhenElapsed < end) {
+ end = alarm.maxWhenElapsed;
+ }
+ flags |= alarm.flags;
+
+ if (DEBUG_BATCH) {
+ Slog.v(TAG, " => now " + this);
+ }
+ return newStart;
+ }
+
+ /**
+ * Remove an alarm from this batch.
+ * <p> <b> Should be used only while re-ordering the alarm within the service </b> as it
+ * does not update {@link #mAlarmsPerUid}
+ */
+ boolean remove(Alarm alarm) {
+ return remove(a -> (a == alarm), true);
+ }
+
+ boolean remove(Predicate<Alarm> predicate, boolean reOrdering) {
+ boolean didRemove = false;
+ long newStart = 0; // recalculate endpoints as we go
+ long newEnd = Long.MAX_VALUE;
+ int newFlags = 0;
+ for (int i = 0; i < alarms.size(); ) {
+ Alarm alarm = alarms.get(i);
+ if (predicate.test(alarm)) {
+ alarms.remove(i);
+ if (!reOrdering) {
+ decrementAlarmCount(alarm.uid, 1);
+ }
+ didRemove = true;
+ if (alarm.alarmClock != null) {
+ mNextAlarmClockMayChange = true;
+ }
+ if (alarm.listener == mTimeTickTrigger) {
+ mLastTickRemoved = mInjector.getCurrentTimeMillis();
+ }
+ } else {
+ if (alarm.whenElapsed > newStart) {
+ newStart = alarm.whenElapsed;
+ }
+ if (alarm.maxWhenElapsed < newEnd) {
+ newEnd = alarm.maxWhenElapsed;
+ }
+ newFlags |= alarm.flags;
+ i++;
+ }
+ }
+ if (didRemove) {
+ // commit the new batch bounds
+ start = newStart;
+ end = newEnd;
+ flags = newFlags;
+ }
+ return didRemove;
+ }
+
+ boolean hasPackage(final String packageName) {
+ final int N = alarms.size();
+ for (int i = 0; i < N; i++) {
+ Alarm a = alarms.get(i);
+ if (a.matches(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean hasWakeups() {
+ final int N = alarms.size();
+ for (int i = 0; i < N; i++) {
+ Alarm a = alarms.get(i);
+ // non-wakeup alarms are types 1 and 3, i.e. have the low bit set
+ if ((a.type & TYPE_NONWAKEUP_MASK) == 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder b = new StringBuilder(40);
+ b.append("Batch{"); b.append(Integer.toHexString(this.hashCode()));
+ b.append(" num="); b.append(size());
+ b.append(" start="); b.append(start);
+ b.append(" end="); b.append(end);
+ if (flags != 0) {
+ b.append(" flgs=0x");
+ b.append(Integer.toHexString(flags));
+ }
+ b.append('}');
+ return b.toString();
+ }
+
+ public void dumpDebug(ProtoOutputStream proto, long fieldId, long nowElapsed,
+ long nowRTC) {
+ final long token = proto.start(fieldId);
+
+ proto.write(BatchProto.START_REALTIME, start);
+ proto.write(BatchProto.END_REALTIME, end);
+ proto.write(BatchProto.FLAGS, flags);
+ for (Alarm a : alarms) {
+ a.dumpDebug(proto, BatchProto.ALARMS, nowElapsed, nowRTC);
+ }
+
+ proto.end(token);
+ }
+ }
+
+ static class BatchTimeOrder implements Comparator<Batch> {
+ public int compare(Batch b1, Batch b2) {
+ long when1 = b1.start;
+ long when2 = b2.start;
+ if (when1 > when2) {
+ return 1;
+ }
+ if (when1 < when2) {
+ return -1;
+ }
+ return 0;
+ }
+ }
+
+ final Comparator<Alarm> mAlarmDispatchComparator = new Comparator<Alarm>() {
+ @Override
+ public int compare(Alarm lhs, Alarm rhs) {
+ // priority class trumps everything. TICK < WAKEUP < NORMAL
+ if (lhs.priorityClass.priority < rhs.priorityClass.priority) {
+ return -1;
+ } else if (lhs.priorityClass.priority > rhs.priorityClass.priority) {
+ return 1;
+ }
+
+ // within each class, sort by nominal delivery time
+ if (lhs.whenElapsed < rhs.whenElapsed) {
+ return -1;
+ } else if (lhs.whenElapsed > rhs.whenElapsed) {
+ return 1;
+ }
+
+ // same priority class + same target delivery time
+ return 0;
+ }
+ };
+
+ void calculateDeliveryPriorities(ArrayList<Alarm> alarms) {
+ final int N = alarms.size();
+ for (int i = 0; i < N; i++) {
+ Alarm a = alarms.get(i);
+
+ final int alarmPrio;
+ if (a.listener == mTimeTickTrigger) {
+ alarmPrio = PRIO_TICK;
+ } else if (a.wakeup) {
+ alarmPrio = PRIO_WAKEUP;
+ } else {
+ alarmPrio = PRIO_NORMAL;
+ }
+
+ PriorityClass packagePrio = a.priorityClass;
+ String alarmPackage = a.sourcePackage;
+ if (packagePrio == null) packagePrio = mPriorities.get(alarmPackage);
+ if (packagePrio == null) {
+ packagePrio = a.priorityClass = new PriorityClass(); // lowest prio & stale sequence
+ mPriorities.put(alarmPackage, packagePrio);
+ }
+ a.priorityClass = packagePrio;
+
+ if (packagePrio.seq != mCurrentSeq) {
+ // first alarm we've seen in the current delivery generation from this package
+ packagePrio.priority = alarmPrio;
+ packagePrio.seq = mCurrentSeq;
+ } else {
+ // Multiple alarms from this package being delivered in this generation;
+ // bump the package's delivery class if it's warranted.
+ // TICK < WAKEUP < NORMAL
+ if (alarmPrio < packagePrio.priority) {
+ packagePrio.priority = alarmPrio;
+ }
+ }
+ }
+ }
+
+ // minimum recurrence period or alarm futurity for us to be able to fuzz it
+ static final long MIN_FUZZABLE_INTERVAL = 10000;
+ static final BatchTimeOrder sBatchOrder = new BatchTimeOrder();
+ final ArrayList<Batch> mAlarmBatches = new ArrayList<>();
+
+ // set to non-null if in idle mode; while in this mode, any alarms we don't want
+ // to run during this time are placed in mPendingWhileIdleAlarms
+ Alarm mPendingIdleUntil = null;
+ Alarm mNextWakeFromIdle = null;
+ ArrayList<Alarm> mPendingWhileIdleAlarms = new ArrayList<>();
+
+ @VisibleForTesting
+ AlarmManagerService(Context context, Injector injector) {
+ super(context);
+ mInjector = injector;
+ }
+
+ public AlarmManagerService(Context context) {
+ this(context, new Injector(context));
+ }
+
+ private long convertToElapsed(long when, int type) {
+ final boolean isRtc = (type == RTC || type == RTC_WAKEUP);
+ if (isRtc) {
+ when -= mInjector.getCurrentTimeMillis() - mInjector.getElapsedRealtime();
+ }
+ return when;
+ }
+
+ // Apply a heuristic to { recurrence interval, futurity of the trigger time } to
+ // calculate the end of our nominal delivery window for the alarm.
+ static long maxTriggerTime(long now, long triggerAtTime, long interval) {
+ // Current heuristic: batchable window is 75% of either the recurrence interval
+ // [for a periodic alarm] or of the time from now to the desired delivery time,
+ // with a minimum delay/interval of 10 seconds, under which we will simply not
+ // defer the alarm.
+ long futurity = (interval == 0)
+ ? (triggerAtTime - now)
+ : interval;
+ if (futurity < MIN_FUZZABLE_INTERVAL) {
+ futurity = 0;
+ }
+ return clampPositive(triggerAtTime + (long)(.75 * futurity));
+ }
+
+ // returns true if the batch was added at the head
+ static boolean addBatchLocked(ArrayList<Batch> list, Batch newBatch) {
+ int index = Collections.binarySearch(list, newBatch, sBatchOrder);
+ if (index < 0) {
+ index = 0 - index - 1;
+ }
+ list.add(index, newBatch);
+ return (index == 0);
+ }
+
+ private void insertAndBatchAlarmLocked(Alarm alarm) {
+ final int whichBatch = ((alarm.flags & AlarmManager.FLAG_STANDALONE) != 0) ? -1
+ : attemptCoalesceLocked(alarm.whenElapsed, alarm.maxWhenElapsed);
+
+ if (whichBatch < 0) {
+ addBatchLocked(mAlarmBatches, new Batch(alarm));
+ } else {
+ final Batch batch = mAlarmBatches.get(whichBatch);
+ if (batch.add(alarm)) {
+ // The start time of this batch advanced, so batch ordering may
+ // have just been broken. Move it to where it now belongs.
+ mAlarmBatches.remove(whichBatch);
+ addBatchLocked(mAlarmBatches, batch);
+ }
+ }
+ }
+
+ // Return the index of the matching batch, or -1 if none found.
+ int attemptCoalesceLocked(long whenElapsed, long maxWhen) {
+ final int N = mAlarmBatches.size();
+ for (int i = 0; i < N; i++) {
+ Batch b = mAlarmBatches.get(i);
+ if ((b.flags&AlarmManager.FLAG_STANDALONE) == 0 && b.canHold(whenElapsed, maxWhen)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ /** @return total count of the alarms in a set of alarm batches. */
+ static int getAlarmCount(ArrayList<Batch> batches) {
+ int ret = 0;
+
+ final int size = batches.size();
+ for (int i = 0; i < size; i++) {
+ ret += batches.get(i).size();
+ }
+ return ret;
+ }
+
+ boolean haveAlarmsTimeTickAlarm(ArrayList<Alarm> alarms) {
+ if (alarms.size() == 0) {
+ return false;
+ }
+ final int batchSize = alarms.size();
+ for (int j = 0; j < batchSize; j++) {
+ if (alarms.get(j).listener == mTimeTickTrigger) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean haveBatchesTimeTickAlarm(ArrayList<Batch> batches) {
+ final int numBatches = batches.size();
+ for (int i = 0; i < numBatches; i++) {
+ if (haveAlarmsTimeTickAlarm(batches.get(i).alarms)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // The RTC clock has moved arbitrarily, so we need to recalculate all the batching
+ void rebatchAllAlarms() {
+ synchronized (mLock) {
+ rebatchAllAlarmsLocked(true);
+ }
+ }
+
+ void rebatchAllAlarmsLocked(boolean doValidate) {
+ final long start = mStatLogger.getTime();
+ final int oldCount =
+ getAlarmCount(mAlarmBatches) + ArrayUtils.size(mPendingWhileIdleAlarms);
+ final boolean oldHasTick = haveBatchesTimeTickAlarm(mAlarmBatches)
+ || haveAlarmsTimeTickAlarm(mPendingWhileIdleAlarms);
+
+ ArrayList<Batch> oldSet = (ArrayList<Batch>) mAlarmBatches.clone();
+ mAlarmBatches.clear();
+ Alarm oldPendingIdleUntil = mPendingIdleUntil;
+ final long nowElapsed = mInjector.getElapsedRealtime();
+ final int oldBatches = oldSet.size();
+ for (int batchNum = 0; batchNum < oldBatches; batchNum++) {
+ Batch batch = oldSet.get(batchNum);
+ final int N = batch.size();
+ for (int i = 0; i < N; i++) {
+ reAddAlarmLocked(batch.get(i), nowElapsed, doValidate);
+ }
+ }
+ if (oldPendingIdleUntil != null && oldPendingIdleUntil != mPendingIdleUntil) {
+ Slog.wtf(TAG, "Rebatching: idle until changed from " + oldPendingIdleUntil
+ + " to " + mPendingIdleUntil);
+ if (mPendingIdleUntil == null) {
+ // Somehow we lost this... we need to restore all of the pending alarms.
+ restorePendingWhileIdleAlarmsLocked();
+ }
+ }
+ final int newCount =
+ getAlarmCount(mAlarmBatches) + ArrayUtils.size(mPendingWhileIdleAlarms);
+ final boolean newHasTick = haveBatchesTimeTickAlarm(mAlarmBatches)
+ || haveAlarmsTimeTickAlarm(mPendingWhileIdleAlarms);
+
+ if (oldCount != newCount) {
+ Slog.wtf(TAG, "Rebatching: total count changed from " + oldCount + " to " + newCount);
+ }
+ if (oldHasTick != newHasTick) {
+ Slog.wtf(TAG, "Rebatching: hasTick changed from " + oldHasTick + " to " + newHasTick);
+ }
+
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ mStatLogger.logDurationStat(Stats.REBATCH_ALL_ALARMS, start);
+ }
+
+ /**
+ * Re-orders the alarm batches based on newly evaluated send times based on the current
+ * app-standby buckets
+ * @param targetPackages [Package, User] pairs for which alarms need to be re-evaluated,
+ * null indicates all
+ * @return True if there was any reordering done to the current list.
+ */
+ boolean reorderAlarmsBasedOnStandbyBuckets(ArraySet<Pair<String, Integer>> targetPackages) {
+ final long start = mStatLogger.getTime();
+ final ArrayList<Alarm> rescheduledAlarms = new ArrayList<>();
+
+ for (int batchIndex = mAlarmBatches.size() - 1; batchIndex >= 0; batchIndex--) {
+ final Batch batch = mAlarmBatches.get(batchIndex);
+ for (int alarmIndex = batch.size() - 1; alarmIndex >= 0; alarmIndex--) {
+ final Alarm alarm = batch.get(alarmIndex);
+ final Pair<String, Integer> packageUser =
+ Pair.create(alarm.sourcePackage, UserHandle.getUserId(alarm.creatorUid));
+ if (targetPackages != null && !targetPackages.contains(packageUser)) {
+ continue;
+ }
+ if (adjustDeliveryTimeBasedOnBucketLocked(alarm)) {
+ batch.remove(alarm);
+ rescheduledAlarms.add(alarm);
+ }
+ }
+ if (batch.size() == 0) {
+ mAlarmBatches.remove(batchIndex);
+ }
+ }
+ for (int i = 0; i < rescheduledAlarms.size(); i++) {
+ final Alarm a = rescheduledAlarms.get(i);
+ insertAndBatchAlarmLocked(a);
+ }
+
+ mStatLogger.logDurationStat(Stats.REORDER_ALARMS_FOR_STANDBY, start);
+ return rescheduledAlarms.size() > 0;
+ }
+
+ void reAddAlarmLocked(Alarm a, long nowElapsed, boolean doValidate) {
+ a.when = a.origWhen;
+ long whenElapsed = convertToElapsed(a.when, a.type);
+ final long maxElapsed;
+ if (a.windowLength == AlarmManager.WINDOW_EXACT) {
+ // Exact
+ maxElapsed = whenElapsed;
+ } else {
+ // Not exact. Preserve any explicit window, otherwise recalculate
+ // the window based on the alarm's new futurity. Note that this
+ // reflects a policy of preferring timely to deferred delivery.
+ maxElapsed = (a.windowLength > 0)
+ ? clampPositive(whenElapsed + a.windowLength)
+ : maxTriggerTime(nowElapsed, whenElapsed, a.repeatInterval);
+ }
+ a.expectedWhenElapsed = a.whenElapsed = whenElapsed;
+ a.expectedMaxWhenElapsed = a.maxWhenElapsed = maxElapsed;
+ setImplLocked(a, true, doValidate);
+ }
+
+ static long clampPositive(long val) {
+ return (val >= 0) ? val : Long.MAX_VALUE;
+ }
+
+ /**
+ * Sends alarms that were blocked due to user applied background restrictions - either because
+ * the user lifted those or the uid came to foreground.
+ *
+ * @param uid uid to filter on
+ * @param packageName package to filter on, or null for all packages in uid
+ */
+ void sendPendingBackgroundAlarmsLocked(int uid, String packageName) {
+ final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.get(uid);
+ if (alarmsForUid == null || alarmsForUid.size() == 0) {
+ return;
+ }
+ final ArrayList<Alarm> alarmsToDeliver;
+ if (packageName != null) {
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG, "Sending blocked alarms for uid " + uid + ", package " + packageName);
+ }
+ alarmsToDeliver = new ArrayList<>();
+ for (int i = alarmsForUid.size() - 1; i >= 0; i--) {
+ final Alarm a = alarmsForUid.get(i);
+ if (a.matches(packageName)) {
+ alarmsToDeliver.add(alarmsForUid.remove(i));
+ }
+ }
+ if (alarmsForUid.size() == 0) {
+ mPendingBackgroundAlarms.remove(uid);
+ }
+ } else {
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG, "Sending blocked alarms for uid " + uid);
+ }
+ alarmsToDeliver = alarmsForUid;
+ mPendingBackgroundAlarms.remove(uid);
+ }
+ deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, mInjector.getElapsedRealtime());
+ }
+
+ /**
+ * Check all alarms in {@link #mPendingBackgroundAlarms} and send the ones that are not
+ * restricted.
+ *
+ * This is only called when the global "force all apps-standby" flag changes or when the
+ * power save whitelist changes, so it's okay to be slow.
+ */
+ void sendAllUnrestrictedPendingBackgroundAlarmsLocked() {
+ final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+
+ findAllUnrestrictedPendingBackgroundAlarmsLockedInner(
+ mPendingBackgroundAlarms, alarmsToDeliver, this::isBackgroundRestricted);
+
+ if (alarmsToDeliver.size() > 0) {
+ deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, mInjector.getElapsedRealtime());
+ }
+ }
+
+ @VisibleForTesting
+ static void findAllUnrestrictedPendingBackgroundAlarmsLockedInner(
+ SparseArray<ArrayList<Alarm>> pendingAlarms, ArrayList<Alarm> unrestrictedAlarms,
+ Predicate<Alarm> isBackgroundRestricted) {
+
+ for (int uidIndex = pendingAlarms.size() - 1; uidIndex >= 0; uidIndex--) {
+ final int uid = pendingAlarms.keyAt(uidIndex);
+ final ArrayList<Alarm> alarmsForUid = pendingAlarms.valueAt(uidIndex);
+
+ for (int alarmIndex = alarmsForUid.size() - 1; alarmIndex >= 0; alarmIndex--) {
+ final Alarm alarm = alarmsForUid.get(alarmIndex);
+
+ if (isBackgroundRestricted.test(alarm)) {
+ continue;
+ }
+
+ unrestrictedAlarms.add(alarm);
+ alarmsForUid.remove(alarmIndex);
+ }
+
+ if (alarmsForUid.size() == 0) {
+ pendingAlarms.removeAt(uidIndex);
+ }
+ }
+ }
+
+ private void deliverPendingBackgroundAlarmsLocked(ArrayList<Alarm> alarms, long nowELAPSED) {
+ final int N = alarms.size();
+ boolean hasWakeup = false;
+ for (int i = 0; i < N; i++) {
+ final Alarm alarm = alarms.get(i);
+ if (alarm.wakeup) {
+ hasWakeup = true;
+ }
+ alarm.count = 1;
+ // Recurring alarms may have passed several alarm intervals while the
+ // alarm was kept pending. Send the appropriate trigger count.
+ if (alarm.repeatInterval > 0) {
+ alarm.count += (nowELAPSED - alarm.expectedWhenElapsed) / alarm.repeatInterval;
+ // Also schedule its next recurrence
+ final long delta = alarm.count * alarm.repeatInterval;
+ final long nextElapsed = alarm.expectedWhenElapsed + delta;
+ setImplLocked(alarm.type, alarm.when + delta, nextElapsed, alarm.windowLength,
+ maxTriggerTime(nowELAPSED, nextElapsed, alarm.repeatInterval),
+ alarm.repeatInterval, alarm.operation, null, null, alarm.flags, true,
+ alarm.workSource, alarm.alarmClock, alarm.uid, alarm.packageName);
+ // Kernel alarms will be rescheduled as needed in setImplLocked
+ }
+ }
+ if (!hasWakeup && checkAllowNonWakeupDelayLocked(nowELAPSED)) {
+ // No need to wakeup for non wakeup alarms
+ if (mPendingNonWakeupAlarms.size() == 0) {
+ mStartCurrentDelayTime = nowELAPSED;
+ mNextNonWakeupDeliveryTime = nowELAPSED
+ + ((currentNonWakeupFuzzLocked(nowELAPSED)*3)/2);
+ }
+ mPendingNonWakeupAlarms.addAll(alarms);
+ mNumDelayedAlarms += alarms.size();
+ } else {
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG, "Waking up to deliver pending blocked alarms");
+ }
+ // Since we are waking up, also deliver any pending non wakeup alarms we have.
+ if (mPendingNonWakeupAlarms.size() > 0) {
+ alarms.addAll(mPendingNonWakeupAlarms);
+ final long thisDelayTime = nowELAPSED - mStartCurrentDelayTime;
+ mTotalDelayTime += thisDelayTime;
+ if (mMaxDelayTime < thisDelayTime) {
+ mMaxDelayTime = thisDelayTime;
+ }
+ mPendingNonWakeupAlarms.clear();
+ }
+ calculateDeliveryPriorities(alarms);
+ Collections.sort(alarms, mAlarmDispatchComparator);
+ deliverAlarmsLocked(alarms, nowELAPSED);
+ }
+ }
+
+ void restorePendingWhileIdleAlarmsLocked() {
+ if (RECORD_DEVICE_IDLE_ALARMS) {
+ IdleDispatchEntry ent = new IdleDispatchEntry();
+ ent.uid = 0;
+ ent.pkg = "FINISH IDLE";
+ ent.elapsedRealtime = mInjector.getElapsedRealtime();
+ mAllowWhileIdleDispatches.add(ent);
+ }
+
+ // Bring pending alarms back into the main list.
+ if (mPendingWhileIdleAlarms.size() > 0) {
+ ArrayList<Alarm> alarms = mPendingWhileIdleAlarms;
+ mPendingWhileIdleAlarms = new ArrayList<>();
+ final long nowElapsed = mInjector.getElapsedRealtime();
+ for (int i=alarms.size() - 1; i >= 0; i--) {
+ Alarm a = alarms.get(i);
+ reAddAlarmLocked(a, nowElapsed, false);
+ }
+ }
+
+ // Reschedule everything.
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+
+ }
+
+ static final class InFlight {
+ final PendingIntent mPendingIntent;
+ final long mWhenElapsed;
+ final IBinder mListener;
+ final WorkSource mWorkSource;
+ final int mUid;
+ final int mCreatorUid;
+ final String mTag;
+ final BroadcastStats mBroadcastStats;
+ final FilterStats mFilterStats;
+ final int mAlarmType;
+
+ InFlight(AlarmManagerService service, Alarm alarm, long nowELAPSED) {
+ mPendingIntent = alarm.operation;
+ mWhenElapsed = nowELAPSED;
+ mListener = alarm.listener != null ? alarm.listener.asBinder() : null;
+ mWorkSource = alarm.workSource;
+ mUid = alarm.uid;
+ mCreatorUid = alarm.creatorUid;
+ mTag = alarm.statsTag;
+ mBroadcastStats = (alarm.operation != null)
+ ? service.getStatsLocked(alarm.operation)
+ : service.getStatsLocked(alarm.uid, alarm.packageName);
+ FilterStats fs = mBroadcastStats.filterStats.get(mTag);
+ if (fs == null) {
+ fs = new FilterStats(mBroadcastStats, mTag);
+ mBroadcastStats.filterStats.put(mTag, fs);
+ }
+ fs.lastTime = nowELAPSED;
+ mFilterStats = fs;
+ mAlarmType = alarm.type;
+ }
+
+ boolean isBroadcast() {
+ return mPendingIntent != null && mPendingIntent.isBroadcast();
+ }
+
+ @Override
+ public String toString() {
+ return "InFlight{"
+ + "pendingIntent=" + mPendingIntent
+ + ", when=" + mWhenElapsed
+ + ", workSource=" + mWorkSource
+ + ", uid=" + mUid
+ + ", creatorUid=" + mCreatorUid
+ + ", tag=" + mTag
+ + ", broadcastStats=" + mBroadcastStats
+ + ", filterStats=" + mFilterStats
+ + ", alarmType=" + mAlarmType
+ + "}";
+ }
+
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(InFlightProto.UID, mUid);
+ proto.write(InFlightProto.TAG, mTag);
+ proto.write(InFlightProto.WHEN_ELAPSED_MS, mWhenElapsed);
+ proto.write(InFlightProto.ALARM_TYPE, mAlarmType);
+ if (mPendingIntent != null) {
+ mPendingIntent.dumpDebug(proto, InFlightProto.PENDING_INTENT);
+ }
+ if (mBroadcastStats != null) {
+ mBroadcastStats.dumpDebug(proto, InFlightProto.BROADCAST_STATS);
+ }
+ if (mFilterStats != null) {
+ mFilterStats.dumpDebug(proto, InFlightProto.FILTER_STATS);
+ }
+ if (mWorkSource != null) {
+ mWorkSource.dumpDebug(proto, InFlightProto.WORK_SOURCE);
+ }
+
+ proto.end(token);
+ }
+ }
+
+ private void notifyBroadcastAlarmPendingLocked(int uid) {
+ final int numListeners = mInFlightListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ mInFlightListeners.get(i).broadcastAlarmPending(uid);
+ }
+ }
+
+ private void notifyBroadcastAlarmCompleteLocked(int uid) {
+ final int numListeners = mInFlightListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ mInFlightListeners.get(i).broadcastAlarmComplete(uid);
+ }
+ }
+
+ static final class FilterStats {
+ final BroadcastStats mBroadcastStats;
+ final String mTag;
+
+ long lastTime;
+ long aggregateTime;
+ int count;
+ int numWakeup;
+ long startTime;
+ int nesting;
+
+ FilterStats(BroadcastStats broadcastStats, String tag) {
+ mBroadcastStats = broadcastStats;
+ mTag = tag;
+ }
+
+ @Override
+ public String toString() {
+ return "FilterStats{"
+ + "tag=" + mTag
+ + ", lastTime=" + lastTime
+ + ", aggregateTime=" + aggregateTime
+ + ", count=" + count
+ + ", numWakeup=" + numWakeup
+ + ", startTime=" + startTime
+ + ", nesting=" + nesting
+ + "}";
+ }
+
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(FilterStatsProto.TAG, mTag);
+ proto.write(FilterStatsProto.LAST_FLIGHT_TIME_REALTIME, lastTime);
+ proto.write(FilterStatsProto.TOTAL_FLIGHT_DURATION_MS, aggregateTime);
+ proto.write(FilterStatsProto.COUNT, count);
+ proto.write(FilterStatsProto.WAKEUP_COUNT, numWakeup);
+ proto.write(FilterStatsProto.START_TIME_REALTIME, startTime);
+ proto.write(FilterStatsProto.NESTING, nesting);
+
+ proto.end(token);
+ }
+ }
+
+ static final class BroadcastStats {
+ final int mUid;
+ final String mPackageName;
+
+ long aggregateTime;
+ int count;
+ int numWakeup;
+ long startTime;
+ int nesting;
+ final ArrayMap<String, FilterStats> filterStats = new ArrayMap<String, FilterStats>();
+
+ BroadcastStats(int uid, String packageName) {
+ mUid = uid;
+ mPackageName = packageName;
+ }
+
+ @Override
+ public String toString() {
+ return "BroadcastStats{"
+ + "uid=" + mUid
+ + ", packageName=" + mPackageName
+ + ", aggregateTime=" + aggregateTime
+ + ", count=" + count
+ + ", numWakeup=" + numWakeup
+ + ", startTime=" + startTime
+ + ", nesting=" + nesting
+ + "}";
+ }
+
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(BroadcastStatsProto.UID, mUid);
+ proto.write(BroadcastStatsProto.PACKAGE_NAME, mPackageName);
+ proto.write(BroadcastStatsProto.TOTAL_FLIGHT_DURATION_MS, aggregateTime);
+ proto.write(BroadcastStatsProto.COUNT, count);
+ proto.write(BroadcastStatsProto.WAKEUP_COUNT, numWakeup);
+ proto.write(BroadcastStatsProto.START_TIME_REALTIME, startTime);
+ proto.write(BroadcastStatsProto.NESTING, nesting);
+
+ proto.end(token);
+ }
+ }
+
+ final SparseArray<ArrayMap<String, BroadcastStats>> mBroadcastStats
+ = new SparseArray<ArrayMap<String, BroadcastStats>>();
+
+ int mNumDelayedAlarms = 0;
+ long mTotalDelayTime = 0;
+ long mMaxDelayTime = 0;
+
+ @Override
+ public void onStart() {
+ mInjector.init();
+
+ mListenerDeathRecipient = new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ }
+
+ @Override
+ public void binderDied(IBinder who) {
+ final IAlarmListener listener = IAlarmListener.Stub.asInterface(who);
+ removeImpl(null, listener);
+ }
+ };
+
+ synchronized (mLock) {
+ mHandler = new AlarmHandler();
+ mConstants = new Constants(mHandler);
+ mAppWakeupHistory = new AppWakeupHistory(Constants.DEFAULT_APP_STANDBY_WINDOW);
+
+ mNextWakeup = mNextNonWakeup = 0;
+
+ // We have to set current TimeZone info to kernel
+ // because kernel doesn't keep this after reboot
+ setTimeZoneImpl(SystemProperties.get(TIMEZONE_PROPERTY));
+
+ // Ensure that we're booting with a halfway sensible current time. Use the
+ // most recent of Build.TIME, the root file system's timestamp, and the
+ // value of the ro.build.date.utc system property (which is in seconds).
+ final long systemBuildTime = Long.max(
+ 1000L * SystemProperties.getLong("ro.build.date.utc", -1L),
+ Long.max(Environment.getRootDirectory().lastModified(), Build.TIME));
+ if (mInjector.getCurrentTimeMillis() < systemBuildTime) {
+ Slog.i(TAG, "Current time only " + mInjector.getCurrentTimeMillis()
+ + ", advancing to build time " + systemBuildTime);
+ mInjector.setKernelTime(systemBuildTime);
+ }
+
+ // Determine SysUI's uid
+ mSystemUiUid = mInjector.getSystemUiUid();
+ if (mSystemUiUid <= 0) {
+ Slog.wtf(TAG, "SysUI package not found!");
+ }
+ mWakeLock = mInjector.getAlarmWakeLock();
+
+ mTimeTickIntent = new Intent(Intent.ACTION_TIME_TICK).addFlags(
+ Intent.FLAG_RECEIVER_REGISTERED_ONLY
+ | Intent.FLAG_RECEIVER_FOREGROUND
+ | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
+
+ mTimeTickTrigger = new IAlarmListener.Stub() {
+ @Override
+ public void doAlarm(final IAlarmCompleteListener callback) throws RemoteException {
+ if (DEBUG_BATCH) {
+ Slog.v(TAG, "Received TIME_TICK alarm; rescheduling");
+ }
+
+ // Via handler because dispatch invokes this within its lock. OnAlarmListener
+ // takes care of this automatically, but we're using the direct internal
+ // interface here rather than that client-side wrapper infrastructure.
+ mHandler.post(() -> {
+ getContext().sendBroadcastAsUser(mTimeTickIntent, UserHandle.ALL);
+
+ try {
+ callback.alarmComplete(this);
+ } catch (RemoteException e) { /* local method call */ }
+ });
+
+ synchronized (mLock) {
+ mLastTickReceived = mInjector.getCurrentTimeMillis();
+ }
+ mClockReceiver.scheduleTimeTickEvent();
+ }
+ };
+
+ Intent intent = new Intent(Intent.ACTION_DATE_CHANGED);
+ intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+ | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
+ mDateChangeSender = PendingIntent.getBroadcastAsUser(getContext(), 0, intent,
+ Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT, UserHandle.ALL);
+
+ mClockReceiver = mInjector.getClockReceiver(this);
+ new ChargingReceiver();
+ new InteractiveStateReceiver();
+ new UninstallReceiver();
+
+ if (mInjector.isAlarmDriverPresent()) {
+ AlarmThread waitThread = new AlarmThread();
+ waitThread.start();
+ } else {
+ Slog.w(TAG, "Failed to open alarm driver. Falling back to a handler.");
+ }
+
+ try {
+ ActivityManager.getService().registerUidObserver(new UidObserver(),
+ 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
+ }
+ }
+ publishLocalService(AlarmManagerInternal.class, new LocalService());
+ publishBinderService(Context.ALARM_SERVICE, mService);
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase == PHASE_SYSTEM_SERVICES_READY) {
+ synchronized (mLock) {
+ mConstants.start(getContext().getContentResolver());
+ mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
+ mLocalDeviceIdleController =
+ LocalServices.getService(DeviceIdleInternal.class);
+ mUsageStatsManagerInternal =
+ LocalServices.getService(UsageStatsManagerInternal.class);
+ AppStandbyInternal appStandbyInternal =
+ LocalServices.getService(AppStandbyInternal.class);
+ appStandbyInternal.addListener(new AppStandbyTracker());
+
+ mAppStateTracker = LocalServices.getService(AppStateTracker.class);
+ mAppStateTracker.addListener(mForceAppStandbyListener);
+
+ mClockReceiver.scheduleTimeTickEvent();
+ mClockReceiver.scheduleDateChangedEvent();
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ mInjector.close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ boolean setTimeImpl(long millis) {
+ if (!mInjector.isAlarmDriverPresent()) {
+ Slog.w(TAG, "Not setting time since no alarm driver is available.");
+ return false;
+ }
+
+ synchronized (mLock) {
+ final long currentTimeMillis = mInjector.getCurrentTimeMillis();
+ mInjector.setKernelTime(millis);
+ final TimeZone timeZone = TimeZone.getDefault();
+ final int currentTzOffset = timeZone.getOffset(currentTimeMillis);
+ final int newTzOffset = timeZone.getOffset(millis);
+ if (currentTzOffset != newTzOffset) {
+ Slog.i(TAG, "Timezone offset has changed, updating kernel timezone");
+ mInjector.setKernelTimezone(-(newTzOffset / 60000));
+ }
+ // The native implementation of setKernelTime can return -1 even when the kernel
+ // time was set correctly, so assume setting kernel time was successful and always
+ // return true.
+ return true;
+ }
+ }
+
+ void setTimeZoneImpl(String tz) {
+ if (TextUtils.isEmpty(tz)) {
+ return;
+ }
+
+ TimeZone zone = TimeZone.getTimeZone(tz);
+ // Prevent reentrant calls from stepping on each other when writing
+ // the time zone property
+ boolean timeZoneWasChanged = false;
+ synchronized (this) {
+ String current = SystemProperties.get(TIMEZONE_PROPERTY);
+ if (current == null || !current.equals(zone.getID())) {
+ if (localLOGV) {
+ Slog.v(TAG, "timezone changed: " + current + ", new=" + zone.getID());
+ }
+ timeZoneWasChanged = true;
+ SystemProperties.set(TIMEZONE_PROPERTY, zone.getID());
+ }
+
+ // Update the kernel timezone information
+ // Kernel tracks time offsets as 'minutes west of GMT'
+ int gmtOffset = zone.getOffset(mInjector.getCurrentTimeMillis());
+ mInjector.setKernelTimezone(-(gmtOffset / 60000));
+ }
+
+ TimeZone.setDefault(null);
+
+ if (timeZoneWasChanged) {
+ // Don't wait for broadcasts to update our midnight alarm
+ mClockReceiver.scheduleDateChangedEvent();
+
+ // And now let everyone else know
+ Intent intent = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+ intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+ | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
+ | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+ | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
+ intent.putExtra(Intent.EXTRA_TIMEZONE, zone.getID());
+ getContext().sendBroadcastAsUser(intent, UserHandle.ALL);
+ }
+ }
+
+ void removeImpl(PendingIntent operation, IAlarmListener listener) {
+ synchronized (mLock) {
+ removeLocked(operation, listener);
+ }
+ }
+
+ void setImpl(int type, long triggerAtTime, long windowLength, long interval,
+ PendingIntent operation, IAlarmListener directReceiver, String listenerTag,
+ int flags, WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock,
+ int callingUid, String callingPackage) {
+ // must be *either* PendingIntent or AlarmReceiver, but not both
+ if ((operation == null && directReceiver == null)
+ || (operation != null && directReceiver != null)) {
+ Slog.w(TAG, "Alarms must either supply a PendingIntent or an AlarmReceiver");
+ // NB: previous releases failed silently here, so we are continuing to do the same
+ // rather than throw an IllegalArgumentException.
+ return;
+ }
+
+ if (directReceiver != null) {
+ try {
+ directReceiver.asBinder().linkToDeath(mListenerDeathRecipient, 0);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Dropping unreachable alarm listener " + listenerTag);
+ return;
+ }
+ }
+
+ // Sanity check the window length. This will catch people mistakenly
+ // trying to pass an end-of-window timestamp rather than a duration.
+ if (windowLength > AlarmManager.INTERVAL_HALF_DAY) {
+ Slog.w(TAG, "Window length " + windowLength
+ + "ms suspiciously long; limiting to 1 hour");
+ windowLength = AlarmManager.INTERVAL_HOUR;
+ }
+
+ // Sanity check the recurrence interval. This will catch people who supply
+ // seconds when the API expects milliseconds, or apps trying shenanigans
+ // around intentional period overflow, etc.
+ final long minInterval = mConstants.MIN_INTERVAL;
+ if (interval > 0 && interval < minInterval) {
+ Slog.w(TAG, "Suspiciously short interval " + interval
+ + " millis; expanding to " + (minInterval/1000)
+ + " seconds");
+ interval = minInterval;
+ } else if (interval > mConstants.MAX_INTERVAL) {
+ Slog.w(TAG, "Suspiciously long interval " + interval
+ + " millis; clamping");
+ interval = mConstants.MAX_INTERVAL;
+ }
+
+ if (type < RTC_WAKEUP || type > ELAPSED_REALTIME) {
+ throw new IllegalArgumentException("Invalid alarm type " + type);
+ }
+
+ if (triggerAtTime < 0) {
+ final long what = Binder.getCallingPid();
+ Slog.w(TAG, "Invalid alarm trigger time! " + triggerAtTime + " from uid=" + callingUid
+ + " pid=" + what);
+ triggerAtTime = 0;
+ }
+
+ final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nominalTrigger = convertToElapsed(triggerAtTime, type);
+ // Try to prevent spamming by making sure apps aren't firing alarms in the immediate future
+ final long minTrigger = nowElapsed
+ + (UserHandle.isCore(callingUid) ? 0L : mConstants.MIN_FUTURITY);
+ final long triggerElapsed = (nominalTrigger > minTrigger) ? nominalTrigger : minTrigger;
+
+ final long maxElapsed;
+ if (windowLength == AlarmManager.WINDOW_EXACT) {
+ maxElapsed = triggerElapsed;
+ } else if (windowLength < 0) {
+ maxElapsed = maxTriggerTime(nowElapsed, triggerElapsed, interval);
+ // Fix this window in place, so that as time approaches we don't collapse it.
+ windowLength = maxElapsed - triggerElapsed;
+ } else {
+ maxElapsed = triggerElapsed + windowLength;
+ }
+ synchronized (mLock) {
+ if (DEBUG_BATCH) {
+ Slog.v(TAG, "set(" + operation + ") : type=" + type
+ + " triggerAtTime=" + triggerAtTime + " win=" + windowLength
+ + " tElapsed=" + triggerElapsed + " maxElapsed=" + maxElapsed
+ + " interval=" + interval + " flags=0x" + Integer.toHexString(flags));
+ }
+ if (mAlarmsPerUid.get(callingUid, 0) >= mConstants.MAX_ALARMS_PER_UID) {
+ final String errorMsg =
+ "Maximum limit of concurrent alarms " + mConstants.MAX_ALARMS_PER_UID
+ + " reached for uid: " + UserHandle.formatUid(callingUid)
+ + ", callingPackage: " + callingPackage;
+ Slog.w(TAG, errorMsg);
+ throw new IllegalStateException(errorMsg);
+ }
+ setImplLocked(type, triggerAtTime, triggerElapsed, windowLength, maxElapsed,
+ interval, operation, directReceiver, listenerTag, flags, true, workSource,
+ alarmClock, callingUid, callingPackage);
+ }
+ }
+
+ private void setImplLocked(int type, long when, long whenElapsed, long windowLength,
+ long maxWhen, long interval, PendingIntent operation, IAlarmListener directReceiver,
+ String listenerTag, int flags, boolean doValidate, WorkSource workSource,
+ AlarmManager.AlarmClockInfo alarmClock, int callingUid, String callingPackage) {
+ Alarm a = new Alarm(type, when, whenElapsed, windowLength, maxWhen, interval,
+ operation, directReceiver, listenerTag, workSource, flags, alarmClock,
+ callingUid, callingPackage);
+ try {
+ if (ActivityManager.getService().isAppStartModeDisabled(callingUid, callingPackage)) {
+ Slog.w(TAG, "Not setting alarm from " + callingUid + ":" + a
+ + " -- package not allowed to start");
+ return;
+ }
+ } catch (RemoteException e) {
+ }
+ removeLocked(operation, directReceiver);
+ incrementAlarmCount(a.uid);
+ setImplLocked(a, false, doValidate);
+ }
+
+ /**
+ * Returns the maximum alarms that an app in the specified bucket can receive in a rolling time
+ * window given by {@link Constants#APP_STANDBY_WINDOW}
+ */
+ @VisibleForTesting
+ int getQuotaForBucketLocked(int bucket) {
+ final int index;
+ if (bucket <= UsageStatsManager.STANDBY_BUCKET_ACTIVE) {
+ index = ACTIVE_INDEX;
+ } else if (bucket <= UsageStatsManager.STANDBY_BUCKET_WORKING_SET) {
+ index = WORKING_INDEX;
+ } else if (bucket <= UsageStatsManager.STANDBY_BUCKET_FREQUENT) {
+ index = FREQUENT_INDEX;
+ } else if (bucket < UsageStatsManager.STANDBY_BUCKET_NEVER) {
+ index = RARE_INDEX;
+ } else {
+ index = NEVER_INDEX;
+ }
+ return mConstants.APP_STANDBY_QUOTAS[index];
+ }
+
+ /**
+ * Adjusts the alarm delivery time based on the current app standby bucket.
+ * @param alarm The alarm to adjust
+ * @return true if the alarm delivery time was updated.
+ */
+ private boolean adjustDeliveryTimeBasedOnBucketLocked(Alarm alarm) {
+ if (isExemptFromAppStandby(alarm)) {
+ return false;
+ }
+ if (mAppStandbyParole) {
+ if (alarm.whenElapsed > alarm.expectedWhenElapsed) {
+ // We did defer this alarm earlier, restore original requirements
+ alarm.whenElapsed = alarm.expectedWhenElapsed;
+ alarm.maxWhenElapsed = alarm.expectedMaxWhenElapsed;
+ return true;
+ }
+ return false;
+ }
+ final long oldWhenElapsed = alarm.whenElapsed;
+ final long oldMaxWhenElapsed = alarm.maxWhenElapsed;
+
+ final String sourcePackage = alarm.sourcePackage;
+ final int sourceUserId = UserHandle.getUserId(alarm.creatorUid);
+ final int standbyBucket = mUsageStatsManagerInternal.getAppStandbyBucket(
+ sourcePackage, sourceUserId, mInjector.getElapsedRealtime());
+
+ // Quota deferring implementation:
+ boolean deferred = false;
+ final int wakeupsInWindow = mAppWakeupHistory.getTotalWakeupsInWindow(sourcePackage,
+ sourceUserId);
+ if (standbyBucket == UsageStatsManager.STANDBY_BUCKET_RESTRICTED) {
+ // Special case because it's 1/day instead of 1/hour.
+ // AppWakeupHistory doesn't delete old wakeup times until a new one is logged, so we
+ // should always have the last wakeup available.
+ if (wakeupsInWindow > 0) {
+ final long lastWakeupTime = mAppWakeupHistory.getNthLastWakeupForPackage(
+ sourcePackage, sourceUserId, mConstants.APP_STANDBY_RESTRICTED_QUOTA);
+ if (mInjector.getElapsedRealtime() - lastWakeupTime
+ < mConstants.APP_STANDBY_RESTRICTED_WINDOW) {
+ final long minElapsed =
+ lastWakeupTime + mConstants.APP_STANDBY_RESTRICTED_WINDOW;
+ if (alarm.expectedWhenElapsed < minElapsed) {
+ alarm.whenElapsed = alarm.maxWhenElapsed = minElapsed;
+ deferred = true;
+ }
+ }
+ }
+ } else {
+ final int quotaForBucket = getQuotaForBucketLocked(standbyBucket);
+ if (wakeupsInWindow >= quotaForBucket) {
+ final long minElapsed;
+ if (quotaForBucket <= 0) {
+ // Just keep deferring for a day till the quota changes
+ minElapsed = mInjector.getElapsedRealtime() + MILLIS_IN_DAY;
+ } else {
+ // Suppose the quota for window was q, and the qth last delivery time for this
+ // package was t(q) then the next delivery must be after t(q) + <window_size>
+ final long t = mAppWakeupHistory.getNthLastWakeupForPackage(
+ sourcePackage, sourceUserId, quotaForBucket);
+ minElapsed = t + mConstants.APP_STANDBY_WINDOW;
+ }
+ if (alarm.expectedWhenElapsed < minElapsed) {
+ alarm.whenElapsed = alarm.maxWhenElapsed = minElapsed;
+ deferred = true;
+ }
+ }
+ }
+ if (!deferred) {
+ // Restore original requirements in case they were changed earlier.
+ alarm.whenElapsed = alarm.expectedWhenElapsed;
+ alarm.maxWhenElapsed = alarm.expectedMaxWhenElapsed;
+ }
+
+ return (oldWhenElapsed != alarm.whenElapsed || oldMaxWhenElapsed != alarm.maxWhenElapsed);
+ }
+
+ private void setImplLocked(Alarm a, boolean rebatching, boolean doValidate) {
+ if ((a.flags&AlarmManager.FLAG_IDLE_UNTIL) != 0) {
+ // This is a special alarm that will put the system into idle until it goes off.
+ // The caller has given the time they want this to happen at, however we need
+ // to pull that earlier if there are existing alarms that have requested to
+ // bring us out of idle at an earlier time.
+ if (mNextWakeFromIdle != null && a.whenElapsed > mNextWakeFromIdle.whenElapsed) {
+ a.when = a.whenElapsed = a.maxWhenElapsed = mNextWakeFromIdle.whenElapsed;
+ }
+ // Add fuzz to make the alarm go off some time before the actual desired time.
+ final long nowElapsed = mInjector.getElapsedRealtime();
+ final int fuzz = fuzzForDuration(a.whenElapsed-nowElapsed);
+ if (fuzz > 0) {
+ if (mRandom == null) {
+ mRandom = new Random();
+ }
+ final int delta = mRandom.nextInt(fuzz);
+ a.whenElapsed -= delta;
+ if (false) {
+ Slog.d(TAG, "Alarm when: " + a.whenElapsed);
+ Slog.d(TAG, "Delta until alarm: " + (a.whenElapsed-nowElapsed));
+ Slog.d(TAG, "Applied fuzz: " + fuzz);
+ Slog.d(TAG, "Final delta: " + delta);
+ Slog.d(TAG, "Final when: " + a.whenElapsed);
+ }
+ a.when = a.maxWhenElapsed = a.whenElapsed;
+ }
+
+ } else if (mPendingIdleUntil != null) {
+ // We currently have an idle until alarm scheduled; if the new alarm has
+ // not explicitly stated it wants to run while idle, then put it on hold.
+ if ((a.flags&(AlarmManager.FLAG_ALLOW_WHILE_IDLE
+ | AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED
+ | AlarmManager.FLAG_WAKE_FROM_IDLE))
+ == 0) {
+ mPendingWhileIdleAlarms.add(a);
+ return;
+ }
+ }
+ if (RECORD_DEVICE_IDLE_ALARMS) {
+ if ((a.flags & AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0) {
+ IdleDispatchEntry ent = new IdleDispatchEntry();
+ ent.uid = a.uid;
+ ent.pkg = a.operation.getCreatorPackage();
+ ent.tag = a.operation.getTag("");
+ ent.op = "SET";
+ ent.elapsedRealtime = mInjector.getElapsedRealtime();
+ ent.argRealtime = a.whenElapsed;
+ mAllowWhileIdleDispatches.add(ent);
+ }
+ }
+ adjustDeliveryTimeBasedOnBucketLocked(a);
+ insertAndBatchAlarmLocked(a);
+
+ if (a.alarmClock != null) {
+ mNextAlarmClockMayChange = true;
+ }
+
+ boolean needRebatch = false;
+
+ if ((a.flags&AlarmManager.FLAG_IDLE_UNTIL) != 0) {
+ if (RECORD_DEVICE_IDLE_ALARMS) {
+ if (mPendingIdleUntil == null) {
+ IdleDispatchEntry ent = new IdleDispatchEntry();
+ ent.uid = 0;
+ ent.pkg = "START IDLE";
+ ent.elapsedRealtime = mInjector.getElapsedRealtime();
+ mAllowWhileIdleDispatches.add(ent);
+ }
+ }
+ if ((mPendingIdleUntil != a) && (mPendingIdleUntil != null)) {
+ Slog.wtfStack(TAG, "setImplLocked: idle until changed from " + mPendingIdleUntil
+ + " to " + a);
+ }
+
+ mPendingIdleUntil = a;
+ needRebatch = true;
+ } else if ((a.flags&AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) {
+ if (mNextWakeFromIdle == null || mNextWakeFromIdle.whenElapsed > a.whenElapsed) {
+ mNextWakeFromIdle = a;
+ // If this wake from idle is earlier than whatever was previously scheduled,
+ // and we are currently idling, then we need to rebatch alarms in case the idle
+ // until time needs to be updated.
+ if (mPendingIdleUntil != null) {
+ needRebatch = true;
+ }
+ }
+ }
+
+ if (!rebatching) {
+ if (DEBUG_VALIDATE) {
+ if (doValidate && !validateConsistencyLocked()) {
+ Slog.v(TAG, "Tipping-point operation: type=" + a.type + " when=" + a.when
+ + " when(hex)=" + Long.toHexString(a.when)
+ + " whenElapsed=" + a.whenElapsed
+ + " maxWhenElapsed=" + a.maxWhenElapsed
+ + " interval=" + a.repeatInterval + " op=" + a.operation
+ + " flags=0x" + Integer.toHexString(a.flags));
+ rebatchAllAlarmsLocked(false);
+ needRebatch = false;
+ }
+ }
+
+ if (needRebatch) {
+ rebatchAllAlarmsLocked(false);
+ }
+
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ }
+ }
+
+ /**
+ * System-process internal API
+ */
+ private final class LocalService implements AlarmManagerInternal {
+ @Override
+ public boolean isIdling() {
+ return isIdlingImpl();
+ }
+
+ @Override
+ public void removeAlarmsForUid(int uid) {
+ synchronized (mLock) {
+ removeLocked(uid);
+ }
+ }
+
+ @Override
+ public void remove(PendingIntent pi) {
+ mHandler.obtainMessage(AlarmHandler.REMOVE_FOR_CANCELED, pi).sendToTarget();
+ }
+
+ @Override
+ public void registerInFlightListener(InFlightListener callback) {
+ synchronized (mLock) {
+ mInFlightListeners.add(callback);
+ }
+ }
+ }
+
+ /**
+ * Public-facing binder interface
+ */
+ private final IBinder mService = new IAlarmManager.Stub() {
+ @Override
+ public void set(String callingPackage,
+ int type, long triggerAtTime, long windowLength, long interval, int flags,
+ PendingIntent operation, IAlarmListener directReceiver, String listenerTag,
+ WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock) {
+ final int callingUid = Binder.getCallingUid();
+
+ // make sure the caller is not lying about which package should be blamed for
+ // wakelock time spent in alarm delivery
+ mAppOps.checkPackage(callingUid, callingPackage);
+
+ // Repeating alarms must use PendingIntent, not direct listener
+ if (interval != 0) {
+ if (directReceiver != null) {
+ throw new IllegalArgumentException("Repeating alarms cannot use AlarmReceivers");
+ }
+ }
+
+ if (workSource != null) {
+ getContext().enforcePermission(
+ android.Manifest.permission.UPDATE_DEVICE_STATS,
+ Binder.getCallingPid(), callingUid, "AlarmManager.set");
+ }
+
+ // No incoming callers can request either WAKE_FROM_IDLE or
+ // ALLOW_WHILE_IDLE_UNRESTRICTED -- we will apply those later as appropriate.
+ flags &= ~(AlarmManager.FLAG_WAKE_FROM_IDLE
+ | AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED);
+
+ // Only the system can use FLAG_IDLE_UNTIL -- this is used to tell the alarm
+ // manager when to come out of idle mode, which is only for DeviceIdleController.
+ if (callingUid != Process.SYSTEM_UID) {
+ flags &= ~AlarmManager.FLAG_IDLE_UNTIL;
+ }
+
+ // If this is an exact time alarm, then it can't be batched with other alarms.
+ if (windowLength == AlarmManager.WINDOW_EXACT) {
+ flags |= AlarmManager.FLAG_STANDALONE;
+ }
+
+ // If this alarm is for an alarm clock, then it must be standalone and we will
+ // use it to wake early from idle if needed.
+ if (alarmClock != null) {
+ flags |= AlarmManager.FLAG_WAKE_FROM_IDLE | AlarmManager.FLAG_STANDALONE;
+
+ // If the caller is a core system component or on the user's whitelist, and not calling
+ // to do work on behalf of someone else, then always set ALLOW_WHILE_IDLE_UNRESTRICTED.
+ // This means we will allow these alarms to go off as normal even while idle, with no
+ // timing restrictions.
+ } else if (workSource == null && (callingUid < Process.FIRST_APPLICATION_UID
+ || UserHandle.isSameApp(callingUid, mSystemUiUid)
+ || ((mAppStateTracker != null)
+ && mAppStateTracker.isUidPowerSaveUserWhitelisted(callingUid)))) {
+ flags |= AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
+ flags &= ~AlarmManager.FLAG_ALLOW_WHILE_IDLE;
+ }
+
+ setImpl(type, triggerAtTime, windowLength, interval, operation, directReceiver,
+ listenerTag, flags, workSource, alarmClock, callingUid, callingPackage);
+ }
+
+ @Override
+ public boolean setTime(long millis) {
+ getContext().enforceCallingOrSelfPermission(
+ "android.permission.SET_TIME",
+ "setTime");
+
+ return setTimeImpl(millis);
+ }
+
+ @Override
+ public void setTimeZone(String tz) {
+ getContext().enforceCallingOrSelfPermission(
+ "android.permission.SET_TIME_ZONE",
+ "setTimeZone");
+
+ final long oldId = Binder.clearCallingIdentity();
+ try {
+ setTimeZoneImpl(tz);
+ } finally {
+ Binder.restoreCallingIdentity(oldId);
+ }
+ }
+
+ @Override
+ public void remove(PendingIntent operation, IAlarmListener listener) {
+ if (operation == null && listener == null) {
+ Slog.w(TAG, "remove() with no intent or listener");
+ return;
+ }
+ synchronized (mLock) {
+ removeLocked(operation, listener);
+ }
+ }
+
+ @Override
+ public long getNextWakeFromIdleTime() {
+ return getNextWakeFromIdleTimeImpl();
+ }
+
+ @Override
+ public AlarmManager.AlarmClockInfo getNextAlarmClock(int userId) {
+ userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, false /* allowAll */, false /* requireFull */,
+ "getNextAlarmClock", null);
+
+ return getNextAlarmClockImpl(userId);
+ }
+
+ @Override
+ public long currentNetworkTimeMillis() {
+ final NtpTrustedTime time = NtpTrustedTime.getInstance(getContext());
+ NtpTrustedTime.TimeResult ntpResult = time.getCachedTimeResult();
+ if (ntpResult != null) {
+ return ntpResult.currentTimeMillis();
+ } else {
+ throw new ParcelableException(new DateTimeException("Missing NTP fix"));
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, pw)) return;
+
+ if (args.length > 0 && "--proto".equals(args[0])) {
+ dumpProto(fd);
+ } else {
+ dumpImpl(pw);
+ }
+ }
+
+ @Override
+ public void onShellCommand(FileDescriptor in, FileDescriptor out,
+ FileDescriptor err, String[] args, ShellCallback callback,
+ ResultReceiver resultReceiver) {
+ (new ShellCmd()).exec(this, in, out, err, args, callback, resultReceiver);
+ }
+ };
+
+ void dumpImpl(PrintWriter pw) {
+ synchronized (mLock) {
+ pw.println("Current Alarm Manager state:");
+ mConstants.dump(pw, " ");
+ pw.println();
+
+ if (mAppStateTracker != null) {
+ mAppStateTracker.dump(pw, " ");
+ pw.println();
+ }
+
+ pw.println(" App Standby Parole: " + mAppStandbyParole);
+ pw.println();
+
+ final long nowELAPSED = mInjector.getElapsedRealtime();
+ final long nowUPTIME = SystemClock.uptimeMillis();
+ final long nowRTC = mInjector.getCurrentTimeMillis();
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+
+ pw.print(" nowRTC="); pw.print(nowRTC);
+ pw.print("="); pw.print(sdf.format(new Date(nowRTC)));
+ pw.print(" nowELAPSED="); pw.print(nowELAPSED);
+ pw.println();
+ pw.print(" mLastTimeChangeClockTime="); pw.print(mLastTimeChangeClockTime);
+ pw.print("="); pw.println(sdf.format(new Date(mLastTimeChangeClockTime)));
+ pw.print(" mLastTimeChangeRealtime="); pw.println(mLastTimeChangeRealtime);
+ pw.print(" mLastTickReceived="); pw.println(sdf.format(new Date(mLastTickReceived)));
+ pw.print(" mLastTickSet="); pw.println(sdf.format(new Date(mLastTickSet)));
+ pw.print(" mLastTickAdded="); pw.println(sdf.format(new Date(mLastTickAdded)));
+ pw.print(" mLastTickRemoved="); pw.println(sdf.format(new Date(mLastTickRemoved)));
+
+ if (RECORD_ALARMS_IN_HISTORY) {
+ pw.println();
+ pw.println(" Recent TIME_TICK history:");
+ int i = mNextTickHistory;
+ do {
+ i--;
+ if (i < 0) i = TICK_HISTORY_DEPTH - 1;
+ final long time = mTickHistory[i];
+ pw.print(" ");
+ pw.println((time > 0)
+ ? sdf.format(new Date(nowRTC - (nowELAPSED - time)))
+ : "-");
+ } while (i != mNextTickHistory);
+ }
+
+ SystemServiceManager ssm = LocalServices.getService(SystemServiceManager.class);
+ if (ssm != null) {
+ pw.println();
+ pw.print(" RuntimeStarted=");
+ pw.print(sdf.format(
+ new Date(nowRTC - nowELAPSED + ssm.getRuntimeStartElapsedTime())));
+ if (ssm.isRuntimeRestarted()) {
+ pw.print(" (Runtime restarted)");
+ }
+ pw.println();
+ pw.print(" Runtime uptime (elapsed): ");
+ TimeUtils.formatDuration(nowELAPSED, ssm.getRuntimeStartElapsedTime(), pw);
+ pw.println();
+ pw.print(" Runtime uptime (uptime): ");
+ TimeUtils.formatDuration(nowUPTIME, ssm.getRuntimeStartUptime(), pw);
+ pw.println();
+ }
+
+ pw.println();
+ if (!mInteractive) {
+ pw.print(" Time since non-interactive: ");
+ TimeUtils.formatDuration(nowELAPSED - mNonInteractiveStartTime, pw);
+ pw.println();
+ }
+ pw.print(" Max wakeup delay: ");
+ TimeUtils.formatDuration(currentNonWakeupFuzzLocked(nowELAPSED), pw);
+ pw.println();
+ pw.print(" Time since last dispatch: ");
+ TimeUtils.formatDuration(nowELAPSED - mLastAlarmDeliveryTime, pw);
+ pw.println();
+ pw.print(" Next non-wakeup delivery time: ");
+ TimeUtils.formatDuration(mNextNonWakeupDeliveryTime, nowELAPSED, pw);
+ pw.println();
+
+ long nextWakeupRTC = mNextWakeup + (nowRTC - nowELAPSED);
+ long nextNonWakeupRTC = mNextNonWakeup + (nowRTC - nowELAPSED);
+ pw.print(" Next non-wakeup alarm: ");
+ TimeUtils.formatDuration(mNextNonWakeup, nowELAPSED, pw);
+ pw.print(" = "); pw.print(mNextNonWakeup);
+ pw.print(" = "); pw.println(sdf.format(new Date(nextNonWakeupRTC)));
+ pw.print(" set at "); TimeUtils.formatDuration(mNextNonWakeUpSetAt, nowELAPSED, pw);
+ pw.println();
+ pw.print(" Next wakeup alarm: "); TimeUtils.formatDuration(mNextWakeup, nowELAPSED, pw);
+ pw.print(" = "); pw.print(mNextWakeup);
+ pw.print(" = "); pw.println(sdf.format(new Date(nextWakeupRTC)));
+ pw.print(" set at "); TimeUtils.formatDuration(mNextWakeUpSetAt, nowELAPSED, pw);
+ pw.println();
+
+ pw.print(" Next kernel non-wakeup alarm: ");
+ TimeUtils.formatDuration(mInjector.getNextAlarm(ELAPSED_REALTIME), pw);
+ pw.println();
+ pw.print(" Next kernel wakeup alarm: ");
+ TimeUtils.formatDuration(mInjector.getNextAlarm(ELAPSED_REALTIME_WAKEUP), pw);
+ pw.println();
+
+ pw.print(" Last wakeup: "); TimeUtils.formatDuration(mLastWakeup, nowELAPSED, pw);
+ pw.print(" = "); pw.println(mLastWakeup);
+ pw.print(" Last trigger: "); TimeUtils.formatDuration(mLastTrigger, nowELAPSED, pw);
+ pw.print(" = "); pw.println(mLastTrigger);
+ pw.print(" Num time change events: "); pw.println(mNumTimeChanged);
+
+ pw.println();
+ pw.println(" Next alarm clock information: ");
+ final TreeSet<Integer> users = new TreeSet<>();
+ for (int i = 0; i < mNextAlarmClockForUser.size(); i++) {
+ users.add(mNextAlarmClockForUser.keyAt(i));
+ }
+ for (int i = 0; i < mPendingSendNextAlarmClockChangedForUser.size(); i++) {
+ users.add(mPendingSendNextAlarmClockChangedForUser.keyAt(i));
+ }
+ for (int user : users) {
+ final AlarmManager.AlarmClockInfo next = mNextAlarmClockForUser.get(user);
+ final long time = next != null ? next.getTriggerTime() : 0;
+ final boolean pendingSend = mPendingSendNextAlarmClockChangedForUser.get(user);
+ pw.print(" user:"); pw.print(user);
+ pw.print(" pendingSend:"); pw.print(pendingSend);
+ pw.print(" time:"); pw.print(time);
+ if (time > 0) {
+ pw.print(" = "); pw.print(sdf.format(new Date(time)));
+ pw.print(" = "); TimeUtils.formatDuration(time, nowRTC, pw);
+ }
+ pw.println();
+ }
+ if (mAlarmBatches.size() > 0) {
+ pw.println();
+ pw.print(" Pending alarm batches: ");
+ pw.println(mAlarmBatches.size());
+ for (Batch b : mAlarmBatches) {
+ pw.print(b); pw.println(':');
+ dumpAlarmList(pw, b.alarms, " ", nowELAPSED, nowRTC, sdf);
+ }
+ }
+ pw.println();
+ pw.println(" Pending user blocked background alarms: ");
+ boolean blocked = false;
+ for (int i = 0; i < mPendingBackgroundAlarms.size(); i++) {
+ final ArrayList<Alarm> blockedAlarms = mPendingBackgroundAlarms.valueAt(i);
+ if (blockedAlarms != null && blockedAlarms.size() > 0) {
+ blocked = true;
+ dumpAlarmList(pw, blockedAlarms, " ", nowELAPSED, nowRTC, sdf);
+ }
+ }
+ if (!blocked) {
+ pw.println(" none");
+ }
+ pw.println();
+ pw.print(" Pending alarms per uid: [");
+ for (int i = 0; i < mAlarmsPerUid.size(); i++) {
+ if (i > 0) {
+ pw.print(", ");
+ }
+ UserHandle.formatUid(pw, mAlarmsPerUid.keyAt(i));
+ pw.print(":");
+ pw.print(mAlarmsPerUid.valueAt(i));
+ }
+ pw.println("]");
+ pw.println();
+
+ mAppWakeupHistory.dump(pw, " ", nowELAPSED);
+
+ if (mPendingIdleUntil != null || mPendingWhileIdleAlarms.size() > 0) {
+ pw.println();
+ pw.println(" Idle mode state:");
+ pw.print(" Idling until: ");
+ if (mPendingIdleUntil != null) {
+ pw.println(mPendingIdleUntil);
+ mPendingIdleUntil.dump(pw, " ", nowELAPSED, nowRTC, sdf);
+ } else {
+ pw.println("null");
+ }
+ pw.println(" Pending alarms:");
+ dumpAlarmList(pw, mPendingWhileIdleAlarms, " ", nowELAPSED, nowRTC, sdf);
+ }
+ if (mNextWakeFromIdle != null) {
+ pw.println();
+ pw.print(" Next wake from idle: "); pw.println(mNextWakeFromIdle);
+ mNextWakeFromIdle.dump(pw, " ", nowELAPSED, nowRTC, sdf);
+ }
+
+ pw.println();
+ pw.print(" Past-due non-wakeup alarms: ");
+ if (mPendingNonWakeupAlarms.size() > 0) {
+ pw.println(mPendingNonWakeupAlarms.size());
+ dumpAlarmList(pw, mPendingNonWakeupAlarms, " ", nowELAPSED, nowRTC, sdf);
+ } else {
+ pw.println("(none)");
+ }
+ pw.print(" Number of delayed alarms: "); pw.print(mNumDelayedAlarms);
+ pw.print(", total delay time: "); TimeUtils.formatDuration(mTotalDelayTime, pw);
+ pw.println();
+ pw.print(" Max delay time: "); TimeUtils.formatDuration(mMaxDelayTime, pw);
+ pw.print(", max non-interactive time: ");
+ TimeUtils.formatDuration(mNonInteractiveTime, pw);
+ pw.println();
+
+ pw.println();
+ pw.print(" Broadcast ref count: "); pw.println(mBroadcastRefCount);
+ pw.print(" PendingIntent send count: "); pw.println(mSendCount);
+ pw.print(" PendingIntent finish count: "); pw.println(mSendFinishCount);
+ pw.print(" Listener send count: "); pw.println(mListenerCount);
+ pw.print(" Listener finish count: "); pw.println(mListenerFinishCount);
+ pw.println();
+
+ if (mInFlight.size() > 0) {
+ pw.println("Outstanding deliveries:");
+ for (int i = 0; i < mInFlight.size(); i++) {
+ pw.print(" #"); pw.print(i); pw.print(": ");
+ pw.println(mInFlight.get(i));
+ }
+ pw.println();
+ }
+
+ if (mLastAllowWhileIdleDispatch.size() > 0) {
+ pw.println(" Last allow while idle dispatch times:");
+ for (int i=0; i<mLastAllowWhileIdleDispatch.size(); i++) {
+ pw.print(" UID ");
+ final int uid = mLastAllowWhileIdleDispatch.keyAt(i);
+ UserHandle.formatUid(pw, uid);
+ pw.print(": ");
+ final long lastTime = mLastAllowWhileIdleDispatch.valueAt(i);
+ TimeUtils.formatDuration(lastTime, nowELAPSED, pw);
+
+ final long minInterval = getWhileIdleMinIntervalLocked(uid);
+ pw.print(" Next allowed:");
+ TimeUtils.formatDuration(lastTime + minInterval, nowELAPSED, pw);
+ pw.print(" (");
+ TimeUtils.formatDuration(minInterval, 0, pw);
+ pw.print(")");
+
+ pw.println();
+ }
+ }
+
+ pw.print(" mUseAllowWhileIdleShortTime: [");
+ for (int i = 0; i < mUseAllowWhileIdleShortTime.size(); i++) {
+ if (mUseAllowWhileIdleShortTime.valueAt(i)) {
+ UserHandle.formatUid(pw, mUseAllowWhileIdleShortTime.keyAt(i));
+ pw.print(" ");
+ }
+ }
+ pw.println("]");
+ pw.println();
+
+ if (mLog.dump(pw, " Recent problems", " ")) {
+ pw.println();
+ }
+
+ final FilterStats[] topFilters = new FilterStats[10];
+ final Comparator<FilterStats> comparator = new Comparator<FilterStats>() {
+ @Override
+ public int compare(FilterStats lhs, FilterStats rhs) {
+ if (lhs.aggregateTime < rhs.aggregateTime) {
+ return 1;
+ } else if (lhs.aggregateTime > rhs.aggregateTime) {
+ return -1;
+ }
+ return 0;
+ }
+ };
+ int len = 0;
+ // Get the top 10 FilterStats, ordered by aggregateTime.
+ for (int iu=0; iu<mBroadcastStats.size(); iu++) {
+ ArrayMap<String, BroadcastStats> uidStats = mBroadcastStats.valueAt(iu);
+ for (int ip=0; ip<uidStats.size(); ip++) {
+ BroadcastStats bs = uidStats.valueAt(ip);
+ for (int is=0; is<bs.filterStats.size(); is++) {
+ FilterStats fs = bs.filterStats.valueAt(is);
+ int pos = len > 0
+ ? Arrays.binarySearch(topFilters, 0, len, fs, comparator) : 0;
+ if (pos < 0) {
+ pos = -pos - 1;
+ }
+ if (pos < topFilters.length) {
+ int copylen = topFilters.length - pos - 1;
+ if (copylen > 0) {
+ System.arraycopy(topFilters, pos, topFilters, pos+1, copylen);
+ }
+ topFilters[pos] = fs;
+ if (len < topFilters.length) {
+ len++;
+ }
+ }
+ }
+ }
+ }
+ if (len > 0) {
+ pw.println(" Top Alarms:");
+ for (int i=0; i<len; i++) {
+ FilterStats fs = topFilters[i];
+ pw.print(" ");
+ if (fs.nesting > 0) pw.print("*ACTIVE* ");
+ TimeUtils.formatDuration(fs.aggregateTime, pw);
+ pw.print(" running, "); pw.print(fs.numWakeup);
+ pw.print(" wakeups, "); pw.print(fs.count);
+ pw.print(" alarms: "); UserHandle.formatUid(pw, fs.mBroadcastStats.mUid);
+ pw.print(":"); pw.print(fs.mBroadcastStats.mPackageName);
+ pw.println();
+ pw.print(" "); pw.print(fs.mTag);
+ pw.println();
+ }
+ }
+
+ pw.println(" ");
+ pw.println(" Alarm Stats:");
+ final ArrayList<FilterStats> tmpFilters = new ArrayList<FilterStats>();
+ for (int iu=0; iu<mBroadcastStats.size(); iu++) {
+ ArrayMap<String, BroadcastStats> uidStats = mBroadcastStats.valueAt(iu);
+ for (int ip=0; ip<uidStats.size(); ip++) {
+ BroadcastStats bs = uidStats.valueAt(ip);
+ pw.print(" ");
+ if (bs.nesting > 0) pw.print("*ACTIVE* ");
+ UserHandle.formatUid(pw, bs.mUid);
+ pw.print(":");
+ pw.print(bs.mPackageName);
+ pw.print(" "); TimeUtils.formatDuration(bs.aggregateTime, pw);
+ pw.print(" running, "); pw.print(bs.numWakeup);
+ pw.println(" wakeups:");
+ tmpFilters.clear();
+ for (int is=0; is<bs.filterStats.size(); is++) {
+ tmpFilters.add(bs.filterStats.valueAt(is));
+ }
+ Collections.sort(tmpFilters, comparator);
+ for (int i=0; i<tmpFilters.size(); i++) {
+ FilterStats fs = tmpFilters.get(i);
+ pw.print(" ");
+ if (fs.nesting > 0) pw.print("*ACTIVE* ");
+ TimeUtils.formatDuration(fs.aggregateTime, pw);
+ pw.print(" "); pw.print(fs.numWakeup);
+ pw.print(" wakes " ); pw.print(fs.count);
+ pw.print(" alarms, last ");
+ TimeUtils.formatDuration(fs.lastTime, nowELAPSED, pw);
+ pw.println(":");
+ pw.print(" ");
+ pw.print(fs.mTag);
+ pw.println();
+ }
+ }
+ }
+ pw.println();
+ mStatLogger.dump(pw, " ");
+
+ if (RECORD_DEVICE_IDLE_ALARMS) {
+ pw.println();
+ pw.println(" Allow while idle dispatches:");
+ for (int i = 0; i < mAllowWhileIdleDispatches.size(); i++) {
+ IdleDispatchEntry ent = mAllowWhileIdleDispatches.get(i);
+ pw.print(" ");
+ TimeUtils.formatDuration(ent.elapsedRealtime, nowELAPSED, pw);
+ pw.print(": ");
+ UserHandle.formatUid(pw, ent.uid);
+ pw.print(":");
+ pw.println(ent.pkg);
+ if (ent.op != null) {
+ pw.print(" ");
+ pw.print(ent.op);
+ pw.print(" / ");
+ pw.print(ent.tag);
+ if (ent.argRealtime != 0) {
+ pw.print(" (");
+ TimeUtils.formatDuration(ent.argRealtime, nowELAPSED, pw);
+ pw.print(")");
+ }
+ pw.println();
+ }
+ }
+ }
+
+ if (WAKEUP_STATS) {
+ pw.println();
+ pw.println(" Recent Wakeup History:");
+ long last = -1;
+ for (WakeupEvent event : mRecentWakeups) {
+ pw.print(" "); pw.print(sdf.format(new Date(event.when)));
+ pw.print('|');
+ if (last < 0) {
+ pw.print('0');
+ } else {
+ pw.print(event.when - last);
+ }
+ last = event.when;
+ pw.print('|'); pw.print(event.uid);
+ pw.print('|'); pw.print(event.action);
+ pw.println();
+ }
+ pw.println();
+ }
+ }
+ }
+
+ void dumpProto(FileDescriptor fd) {
+ final ProtoOutputStream proto = new ProtoOutputStream(fd);
+
+ synchronized (mLock) {
+ final long nowRTC = mInjector.getCurrentTimeMillis();
+ final long nowElapsed = mInjector.getElapsedRealtime();
+ proto.write(AlarmManagerServiceDumpProto.CURRENT_TIME, nowRTC);
+ proto.write(AlarmManagerServiceDumpProto.ELAPSED_REALTIME, nowElapsed);
+ proto.write(AlarmManagerServiceDumpProto.LAST_TIME_CHANGE_CLOCK_TIME,
+ mLastTimeChangeClockTime);
+ proto.write(AlarmManagerServiceDumpProto.LAST_TIME_CHANGE_REALTIME,
+ mLastTimeChangeRealtime);
+
+ mConstants.dumpProto(proto, AlarmManagerServiceDumpProto.SETTINGS);
+
+ if (mAppStateTracker != null) {
+ mAppStateTracker.dumpProto(proto, AlarmManagerServiceDumpProto.APP_STATE_TRACKER);
+ }
+
+ proto.write(AlarmManagerServiceDumpProto.IS_INTERACTIVE, mInteractive);
+ if (!mInteractive) {
+ // Durations
+ proto.write(AlarmManagerServiceDumpProto.TIME_SINCE_NON_INTERACTIVE_MS,
+ nowElapsed - mNonInteractiveStartTime);
+ proto.write(AlarmManagerServiceDumpProto.MAX_WAKEUP_DELAY_MS,
+ currentNonWakeupFuzzLocked(nowElapsed));
+ proto.write(AlarmManagerServiceDumpProto.TIME_SINCE_LAST_DISPATCH_MS,
+ nowElapsed - mLastAlarmDeliveryTime);
+ proto.write(AlarmManagerServiceDumpProto.TIME_UNTIL_NEXT_NON_WAKEUP_DELIVERY_MS,
+ nowElapsed - mNextNonWakeupDeliveryTime);
+ }
+
+ proto.write(AlarmManagerServiceDumpProto.TIME_UNTIL_NEXT_NON_WAKEUP_ALARM_MS,
+ mNextNonWakeup - nowElapsed);
+ proto.write(AlarmManagerServiceDumpProto.TIME_UNTIL_NEXT_WAKEUP_MS,
+ mNextWakeup - nowElapsed);
+ proto.write(AlarmManagerServiceDumpProto.TIME_SINCE_LAST_WAKEUP_MS,
+ nowElapsed - mLastWakeup);
+ proto.write(AlarmManagerServiceDumpProto.TIME_SINCE_LAST_WAKEUP_SET_MS,
+ nowElapsed - mNextWakeUpSetAt);
+ proto.write(AlarmManagerServiceDumpProto.TIME_CHANGE_EVENT_COUNT, mNumTimeChanged);
+
+ final TreeSet<Integer> users = new TreeSet<>();
+ final int nextAlarmClockForUserSize = mNextAlarmClockForUser.size();
+ for (int i = 0; i < nextAlarmClockForUserSize; i++) {
+ users.add(mNextAlarmClockForUser.keyAt(i));
+ }
+ final int pendingSendNextAlarmClockChangedForUserSize =
+ mPendingSendNextAlarmClockChangedForUser.size();
+ for (int i = 0; i < pendingSendNextAlarmClockChangedForUserSize; i++) {
+ users.add(mPendingSendNextAlarmClockChangedForUser.keyAt(i));
+ }
+ for (int user : users) {
+ final AlarmManager.AlarmClockInfo next = mNextAlarmClockForUser.get(user);
+ final long time = next != null ? next.getTriggerTime() : 0;
+ final boolean pendingSend = mPendingSendNextAlarmClockChangedForUser.get(user);
+ final long aToken = proto.start(AlarmManagerServiceDumpProto.NEXT_ALARM_CLOCK_METADATA);
+ proto.write(AlarmClockMetadataProto.USER, user);
+ proto.write(AlarmClockMetadataProto.IS_PENDING_SEND, pendingSend);
+ proto.write(AlarmClockMetadataProto.TRIGGER_TIME_MS, time);
+ proto.end(aToken);
+ }
+ for (Batch b : mAlarmBatches) {
+ b.dumpDebug(proto, AlarmManagerServiceDumpProto.PENDING_ALARM_BATCHES,
+ nowElapsed, nowRTC);
+ }
+ for (int i = 0; i < mPendingBackgroundAlarms.size(); i++) {
+ final ArrayList<Alarm> blockedAlarms = mPendingBackgroundAlarms.valueAt(i);
+ if (blockedAlarms != null) {
+ for (Alarm a : blockedAlarms) {
+ a.dumpDebug(proto,
+ AlarmManagerServiceDumpProto.PENDING_USER_BLOCKED_BACKGROUND_ALARMS,
+ nowElapsed, nowRTC);
+ }
+ }
+ }
+ if (mPendingIdleUntil != null) {
+ mPendingIdleUntil.dumpDebug(
+ proto, AlarmManagerServiceDumpProto.PENDING_IDLE_UNTIL, nowElapsed, nowRTC);
+ }
+ for (Alarm a : mPendingWhileIdleAlarms) {
+ a.dumpDebug(proto, AlarmManagerServiceDumpProto.PENDING_WHILE_IDLE_ALARMS,
+ nowElapsed, nowRTC);
+ }
+ if (mNextWakeFromIdle != null) {
+ mNextWakeFromIdle.dumpDebug(proto, AlarmManagerServiceDumpProto.NEXT_WAKE_FROM_IDLE,
+ nowElapsed, nowRTC);
+ }
+
+ for (Alarm a : mPendingNonWakeupAlarms) {
+ a.dumpDebug(proto, AlarmManagerServiceDumpProto.PAST_DUE_NON_WAKEUP_ALARMS,
+ nowElapsed, nowRTC);
+ }
+
+ proto.write(AlarmManagerServiceDumpProto.DELAYED_ALARM_COUNT, mNumDelayedAlarms);
+ proto.write(AlarmManagerServiceDumpProto.TOTAL_DELAY_TIME_MS, mTotalDelayTime);
+ proto.write(AlarmManagerServiceDumpProto.MAX_DELAY_DURATION_MS, mMaxDelayTime);
+ proto.write(AlarmManagerServiceDumpProto.MAX_NON_INTERACTIVE_DURATION_MS,
+ mNonInteractiveTime);
+
+ proto.write(AlarmManagerServiceDumpProto.BROADCAST_REF_COUNT, mBroadcastRefCount);
+ proto.write(AlarmManagerServiceDumpProto.PENDING_INTENT_SEND_COUNT, mSendCount);
+ proto.write(AlarmManagerServiceDumpProto.PENDING_INTENT_FINISH_COUNT, mSendFinishCount);
+ proto.write(AlarmManagerServiceDumpProto.LISTENER_SEND_COUNT, mListenerCount);
+ proto.write(AlarmManagerServiceDumpProto.LISTENER_FINISH_COUNT, mListenerFinishCount);
+
+ for (InFlight f : mInFlight) {
+ f.dumpDebug(proto, AlarmManagerServiceDumpProto.OUTSTANDING_DELIVERIES);
+ }
+
+ for (int i = 0; i < mLastAllowWhileIdleDispatch.size(); ++i) {
+ final long token = proto.start(
+ AlarmManagerServiceDumpProto.LAST_ALLOW_WHILE_IDLE_DISPATCH_TIMES);
+ final int uid = mLastAllowWhileIdleDispatch.keyAt(i);
+ final long lastTime = mLastAllowWhileIdleDispatch.valueAt(i);
+
+ proto.write(AlarmManagerServiceDumpProto.LastAllowWhileIdleDispatch.UID, uid);
+ proto.write(AlarmManagerServiceDumpProto.LastAllowWhileIdleDispatch.TIME_MS, lastTime);
+ proto.write(AlarmManagerServiceDumpProto.LastAllowWhileIdleDispatch.NEXT_ALLOWED_MS,
+ lastTime + getWhileIdleMinIntervalLocked(uid));
+ proto.end(token);
+ }
+
+ for (int i = 0; i < mUseAllowWhileIdleShortTime.size(); i++) {
+ if (mUseAllowWhileIdleShortTime.valueAt(i)) {
+ proto.write(AlarmManagerServiceDumpProto.USE_ALLOW_WHILE_IDLE_SHORT_TIME,
+ mUseAllowWhileIdleShortTime.keyAt(i));
+ }
+ }
+
+ mLog.dumpDebug(proto, AlarmManagerServiceDumpProto.RECENT_PROBLEMS);
+
+ final FilterStats[] topFilters = new FilterStats[10];
+ final Comparator<FilterStats> comparator = new Comparator<FilterStats>() {
+ @Override
+ public int compare(FilterStats lhs, FilterStats rhs) {
+ if (lhs.aggregateTime < rhs.aggregateTime) {
+ return 1;
+ } else if (lhs.aggregateTime > rhs.aggregateTime) {
+ return -1;
+ }
+ return 0;
+ }
+ };
+ int len = 0;
+ // Get the top 10 FilterStats, ordered by aggregateTime.
+ for (int iu = 0; iu < mBroadcastStats.size(); ++iu) {
+ ArrayMap<String, BroadcastStats> uidStats = mBroadcastStats.valueAt(iu);
+ for (int ip = 0; ip < uidStats.size(); ++ip) {
+ BroadcastStats bs = uidStats.valueAt(ip);
+ for (int is = 0; is < bs.filterStats.size(); ++is) {
+ FilterStats fs = bs.filterStats.valueAt(is);
+ int pos = len > 0
+ ? Arrays.binarySearch(topFilters, 0, len, fs, comparator) : 0;
+ if (pos < 0) {
+ pos = -pos - 1;
+ }
+ if (pos < topFilters.length) {
+ int copylen = topFilters.length - pos - 1;
+ if (copylen > 0) {
+ System.arraycopy(topFilters, pos, topFilters, pos+1, copylen);
+ }
+ topFilters[pos] = fs;
+ if (len < topFilters.length) {
+ len++;
+ }
+ }
+ }
+ }
+ }
+ for (int i = 0; i < len; ++i) {
+ final long token = proto.start(AlarmManagerServiceDumpProto.TOP_ALARMS);
+ FilterStats fs = topFilters[i];
+
+ proto.write(AlarmManagerServiceDumpProto.TopAlarm.UID, fs.mBroadcastStats.mUid);
+ proto.write(AlarmManagerServiceDumpProto.TopAlarm.PACKAGE_NAME,
+ fs.mBroadcastStats.mPackageName);
+ fs.dumpDebug(proto, AlarmManagerServiceDumpProto.TopAlarm.FILTER);
+
+ proto.end(token);
+ }
+
+ final ArrayList<FilterStats> tmpFilters = new ArrayList<FilterStats>();
+ for (int iu = 0; iu < mBroadcastStats.size(); ++iu) {
+ ArrayMap<String, BroadcastStats> uidStats = mBroadcastStats.valueAt(iu);
+ for (int ip = 0; ip < uidStats.size(); ++ip) {
+ final long token = proto.start(AlarmManagerServiceDumpProto.ALARM_STATS);
+
+ BroadcastStats bs = uidStats.valueAt(ip);
+ bs.dumpDebug(proto, AlarmManagerServiceDumpProto.AlarmStat.BROADCAST);
+
+ // uidStats is an ArrayMap, which we can't sort.
+ tmpFilters.clear();
+ for (int is = 0; is < bs.filterStats.size(); ++is) {
+ tmpFilters.add(bs.filterStats.valueAt(is));
+ }
+ Collections.sort(tmpFilters, comparator);
+ for (FilterStats fs : tmpFilters) {
+ fs.dumpDebug(proto, AlarmManagerServiceDumpProto.AlarmStat.FILTERS);
+ }
+
+ proto.end(token);
+ }
+ }
+
+ if (RECORD_DEVICE_IDLE_ALARMS) {
+ for (int i = 0; i < mAllowWhileIdleDispatches.size(); i++) {
+ IdleDispatchEntry ent = mAllowWhileIdleDispatches.get(i);
+ final long token = proto.start(
+ AlarmManagerServiceDumpProto.ALLOW_WHILE_IDLE_DISPATCHES);
+
+ proto.write(IdleDispatchEntryProto.UID, ent.uid);
+ proto.write(IdleDispatchEntryProto.PKG, ent.pkg);
+ proto.write(IdleDispatchEntryProto.TAG, ent.tag);
+ proto.write(IdleDispatchEntryProto.OP, ent.op);
+ proto.write(IdleDispatchEntryProto.ENTRY_CREATION_REALTIME,
+ ent.elapsedRealtime);
+ proto.write(IdleDispatchEntryProto.ARG_REALTIME, ent.argRealtime);
+
+ proto.end(token);
+ }
+ }
+
+ if (WAKEUP_STATS) {
+ for (WakeupEvent event : mRecentWakeups) {
+ final long token = proto.start(AlarmManagerServiceDumpProto.RECENT_WAKEUP_HISTORY);
+ proto.write(WakeupEventProto.UID, event.uid);
+ proto.write(WakeupEventProto.ACTION, event.action);
+ proto.write(WakeupEventProto.WHEN, event.when);
+ proto.end(token);
+ }
+ }
+ }
+
+ proto.flush();
+ }
+
+ private void logBatchesLocked(SimpleDateFormat sdf) {
+ ByteArrayOutputStream bs = new ByteArrayOutputStream(2048);
+ PrintWriter pw = new PrintWriter(bs);
+ final long nowRTC = mInjector.getCurrentTimeMillis();
+ final long nowELAPSED = mInjector.getElapsedRealtime();
+ final int NZ = mAlarmBatches.size();
+ for (int iz = 0; iz < NZ; iz++) {
+ Batch bz = mAlarmBatches.get(iz);
+ pw.append("Batch "); pw.print(iz); pw.append(": "); pw.println(bz);
+ dumpAlarmList(pw, bz.alarms, " ", nowELAPSED, nowRTC, sdf);
+ pw.flush();
+ Slog.v(TAG, bs.toString());
+ bs.reset();
+ }
+ }
+
+ private boolean validateConsistencyLocked() {
+ if (DEBUG_VALIDATE) {
+ long lastTime = Long.MIN_VALUE;
+ final int N = mAlarmBatches.size();
+ for (int i = 0; i < N; i++) {
+ Batch b = mAlarmBatches.get(i);
+ if (b.start >= lastTime) {
+ // duplicate start times are okay because of standalone batches
+ lastTime = b.start;
+ } else {
+ Slog.e(TAG, "CONSISTENCY FAILURE: Batch " + i + " is out of order");
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ logBatchesLocked(sdf);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private Batch findFirstWakeupBatchLocked() {
+ final int N = mAlarmBatches.size();
+ for (int i = 0; i < N; i++) {
+ Batch b = mAlarmBatches.get(i);
+ if (b.hasWakeups()) {
+ return b;
+ }
+ }
+ return null;
+ }
+
+ long getNextWakeFromIdleTimeImpl() {
+ synchronized (mLock) {
+ return mNextWakeFromIdle != null ? mNextWakeFromIdle.whenElapsed : Long.MAX_VALUE;
+ }
+ }
+
+ private boolean isIdlingImpl() {
+ synchronized (mLock) {
+ return mPendingIdleUntil != null;
+ }
+ }
+
+ AlarmManager.AlarmClockInfo getNextAlarmClockImpl(int userId) {
+ synchronized (mLock) {
+ return mNextAlarmClockForUser.get(userId);
+ }
+ }
+
+ /**
+ * Recomputes the next alarm clock for all users.
+ */
+ private void updateNextAlarmClockLocked() {
+ if (!mNextAlarmClockMayChange) {
+ return;
+ }
+ mNextAlarmClockMayChange = false;
+
+ SparseArray<AlarmManager.AlarmClockInfo> nextForUser = mTmpSparseAlarmClockArray;
+ nextForUser.clear();
+
+ final int N = mAlarmBatches.size();
+ for (int i = 0; i < N; i++) {
+ ArrayList<Alarm> alarms = mAlarmBatches.get(i).alarms;
+ final int M = alarms.size();
+
+ for (int j = 0; j < M; j++) {
+ Alarm a = alarms.get(j);
+ if (a.alarmClock != null) {
+ final int userId = UserHandle.getUserId(a.uid);
+ AlarmManager.AlarmClockInfo current = mNextAlarmClockForUser.get(userId);
+
+ if (DEBUG_ALARM_CLOCK) {
+ Log.v(TAG, "Found AlarmClockInfo " + a.alarmClock + " at " +
+ formatNextAlarm(getContext(), a.alarmClock, userId) +
+ " for user " + userId);
+ }
+
+ // Alarms and batches are sorted by time, no need to compare times here.
+ if (nextForUser.get(userId) == null) {
+ nextForUser.put(userId, a.alarmClock);
+ } else if (a.alarmClock.equals(current)
+ && current.getTriggerTime() <= nextForUser.get(userId).getTriggerTime()) {
+ // same/earlier time and it's the one we cited before, so stick with it
+ nextForUser.put(userId, current);
+ }
+ }
+ }
+ }
+
+ // Update mNextAlarmForUser with new values.
+ final int NN = nextForUser.size();
+ for (int i = 0; i < NN; i++) {
+ AlarmManager.AlarmClockInfo newAlarm = nextForUser.valueAt(i);
+ int userId = nextForUser.keyAt(i);
+ AlarmManager.AlarmClockInfo currentAlarm = mNextAlarmClockForUser.get(userId);
+ if (!newAlarm.equals(currentAlarm)) {
+ updateNextAlarmInfoForUserLocked(userId, newAlarm);
+ }
+ }
+
+ // Remove users without any alarm clocks scheduled.
+ final int NNN = mNextAlarmClockForUser.size();
+ for (int i = NNN - 1; i >= 0; i--) {
+ int userId = mNextAlarmClockForUser.keyAt(i);
+ if (nextForUser.get(userId) == null) {
+ updateNextAlarmInfoForUserLocked(userId, null);
+ }
+ }
+ }
+
+ private void updateNextAlarmInfoForUserLocked(int userId,
+ AlarmManager.AlarmClockInfo alarmClock) {
+ if (alarmClock != null) {
+ if (DEBUG_ALARM_CLOCK) {
+ Log.v(TAG, "Next AlarmClockInfoForUser(" + userId + "): " +
+ formatNextAlarm(getContext(), alarmClock, userId));
+ }
+ mNextAlarmClockForUser.put(userId, alarmClock);
+ } else {
+ if (DEBUG_ALARM_CLOCK) {
+ Log.v(TAG, "Next AlarmClockInfoForUser(" + userId + "): None");
+ }
+ mNextAlarmClockForUser.remove(userId);
+ }
+
+ mPendingSendNextAlarmClockChangedForUser.put(userId, true);
+ mHandler.removeMessages(AlarmHandler.SEND_NEXT_ALARM_CLOCK_CHANGED);
+ mHandler.sendEmptyMessage(AlarmHandler.SEND_NEXT_ALARM_CLOCK_CHANGED);
+ }
+
+ /**
+ * Updates NEXT_ALARM_FORMATTED and sends NEXT_ALARM_CLOCK_CHANGED_INTENT for all users
+ * for which alarm clocks have changed since the last call to this.
+ *
+ * Do not call with a lock held. Only call from mHandler's thread.
+ *
+ * @see AlarmHandler#SEND_NEXT_ALARM_CLOCK_CHANGED
+ */
+ private void sendNextAlarmClockChanged() {
+ SparseArray<AlarmManager.AlarmClockInfo> pendingUsers = mHandlerSparseAlarmClockArray;
+ pendingUsers.clear();
+
+ synchronized (mLock) {
+ final int N = mPendingSendNextAlarmClockChangedForUser.size();
+ for (int i = 0; i < N; i++) {
+ int userId = mPendingSendNextAlarmClockChangedForUser.keyAt(i);
+ pendingUsers.append(userId, mNextAlarmClockForUser.get(userId));
+ }
+ mPendingSendNextAlarmClockChangedForUser.clear();
+ }
+
+ final int N = pendingUsers.size();
+ for (int i = 0; i < N; i++) {
+ int userId = pendingUsers.keyAt(i);
+ AlarmManager.AlarmClockInfo alarmClock = pendingUsers.valueAt(i);
+ Settings.System.putStringForUser(getContext().getContentResolver(),
+ Settings.System.NEXT_ALARM_FORMATTED,
+ formatNextAlarm(getContext(), alarmClock, userId),
+ userId);
+
+ getContext().sendBroadcastAsUser(NEXT_ALARM_CLOCK_CHANGED_INTENT,
+ new UserHandle(userId));
+ }
+ }
+
+ /**
+ * Formats an alarm like platform/packages/apps/DeskClock used to.
+ */
+ private static String formatNextAlarm(final Context context, AlarmManager.AlarmClockInfo info,
+ int userId) {
+ String skeleton = DateFormat.is24HourFormat(context, userId) ? "EHm" : "Ehma";
+ String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
+ return (info == null) ? "" :
+ DateFormat.format(pattern, info.getTriggerTime()).toString();
+ }
+
+ void rescheduleKernelAlarmsLocked() {
+ // Schedule the next upcoming wakeup alarm. If there is a deliverable batch
+ // prior to that which contains no wakeups, we schedule that as well.
+ final long nowElapsed = mInjector.getElapsedRealtime();
+ long nextNonWakeup = 0;
+ if (mAlarmBatches.size() > 0) {
+ final Batch firstWakeup = findFirstWakeupBatchLocked();
+ final Batch firstBatch = mAlarmBatches.get(0);
+ if (firstWakeup != null) {
+ mNextWakeup = firstWakeup.start;
+ mNextWakeUpSetAt = nowElapsed;
+ setLocked(ELAPSED_REALTIME_WAKEUP, firstWakeup.start);
+ }
+ if (firstBatch != firstWakeup) {
+ nextNonWakeup = firstBatch.start;
+ }
+ }
+ if (mPendingNonWakeupAlarms.size() > 0) {
+ if (nextNonWakeup == 0 || mNextNonWakeupDeliveryTime < nextNonWakeup) {
+ nextNonWakeup = mNextNonWakeupDeliveryTime;
+ }
+ }
+ if (nextNonWakeup != 0) {
+ mNextNonWakeup = nextNonWakeup;
+ mNextNonWakeUpSetAt = nowElapsed;
+ setLocked(ELAPSED_REALTIME, nextNonWakeup);
+ }
+ }
+
+ void removeLocked(PendingIntent operation, IAlarmListener directReceiver) {
+ if (operation == null && directReceiver == null) {
+ if (localLOGV) {
+ Slog.w(TAG, "requested remove() of null operation",
+ new RuntimeException("here"));
+ }
+ return;
+ }
+
+ boolean didRemove = false;
+ final Predicate<Alarm> whichAlarms = (Alarm a) -> a.matches(operation, directReceiver);
+ for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
+ Batch b = mAlarmBatches.get(i);
+ didRemove |= b.remove(whichAlarms, false);
+ if (b.size() == 0) {
+ mAlarmBatches.remove(i);
+ }
+ }
+ for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {
+ final Alarm alarm = mPendingWhileIdleAlarms.get(i);
+ if (alarm.matches(operation, directReceiver)) {
+ // Don't set didRemove, since this doesn't impact the scheduled alarms.
+ mPendingWhileIdleAlarms.remove(i);
+ decrementAlarmCount(alarm.uid, 1);
+ }
+ }
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+ final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
+ for (int j = alarmsForUid.size() - 1; j >= 0; j--) {
+ final Alarm alarm = alarmsForUid.get(j);
+ if (alarm.matches(operation, directReceiver)) {
+ // Don't set didRemove, since this doesn't impact the scheduled alarms.
+ alarmsForUid.remove(j);
+ decrementAlarmCount(alarm.uid, 1);
+ }
+ }
+ if (alarmsForUid.size() == 0) {
+ mPendingBackgroundAlarms.removeAt(i);
+ }
+ }
+ if (didRemove) {
+ if (DEBUG_BATCH) {
+ Slog.v(TAG, "remove(operation) changed bounds; rebatching");
+ }
+ boolean restorePending = false;
+ if (mPendingIdleUntil != null && mPendingIdleUntil.matches(operation, directReceiver)) {
+ mPendingIdleUntil = null;
+ restorePending = true;
+ }
+ if (mNextWakeFromIdle != null && mNextWakeFromIdle.matches(operation, directReceiver)) {
+ mNextWakeFromIdle = null;
+ }
+ rebatchAllAlarmsLocked(true);
+ if (restorePending) {
+ restorePendingWhileIdleAlarmsLocked();
+ }
+ updateNextAlarmClockLocked();
+ }
+ }
+
+ void removeLocked(final int uid) {
+ if (uid == Process.SYSTEM_UID) {
+ // If a force-stop occurs for a system-uid package, ignore it.
+ return;
+ }
+ boolean didRemove = false;
+ final Predicate<Alarm> whichAlarms = (Alarm a) -> a.uid == uid;
+ for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
+ Batch b = mAlarmBatches.get(i);
+ didRemove |= b.remove(whichAlarms, false);
+ if (b.size() == 0) {
+ mAlarmBatches.remove(i);
+ }
+ }
+ for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {
+ final Alarm a = mPendingWhileIdleAlarms.get(i);
+ if (a.uid == uid) {
+ // Don't set didRemove, since this doesn't impact the scheduled alarms.
+ mPendingWhileIdleAlarms.remove(i);
+ decrementAlarmCount(uid, 1);
+ }
+ }
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i --) {
+ final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
+ for (int j = alarmsForUid.size() - 1; j >= 0; j--) {
+ if (alarmsForUid.get(j).uid == uid) {
+ alarmsForUid.remove(j);
+ decrementAlarmCount(uid, 1);
+ }
+ }
+ if (alarmsForUid.size() == 0) {
+ mPendingBackgroundAlarms.removeAt(i);
+ }
+ }
+ // If we're currently keying off of this app's alarms for doze transitions,
+ // make sure to reset to other triggers.
+ if (mNextWakeFromIdle != null && mNextWakeFromIdle.uid == uid) {
+ mNextWakeFromIdle = null;
+ }
+ if (mPendingIdleUntil != null && mPendingIdleUntil.uid == uid) {
+ // Should never happen - only the system uid is allowed to set idle-until alarms
+ Slog.wtf(TAG, "Removed app uid " + uid + " set idle-until alarm!");
+ mPendingIdleUntil = null;
+ }
+ if (didRemove) {
+ if (DEBUG_BATCH) {
+ Slog.v(TAG, "remove(uid) changed bounds; rebatching");
+ }
+ rebatchAllAlarmsLocked(true);
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ }
+ }
+
+ void removeLocked(final String packageName) {
+ if (packageName == null) {
+ if (localLOGV) {
+ Slog.w(TAG, "requested remove() of null packageName",
+ new RuntimeException("here"));
+ }
+ return;
+ }
+
+ boolean didRemove = false;
+ final MutableBoolean removedNextWakeFromIdle = new MutableBoolean(false);
+ final Predicate<Alarm> whichAlarms = (Alarm a) -> {
+ final boolean didMatch = a.matches(packageName);
+ if (didMatch && a == mNextWakeFromIdle) {
+ removedNextWakeFromIdle.value = true;
+ }
+ return didMatch;
+ };
+ final boolean oldHasTick = haveBatchesTimeTickAlarm(mAlarmBatches);
+ for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
+ Batch b = mAlarmBatches.get(i);
+ didRemove |= b.remove(whichAlarms, false);
+ if (b.size() == 0) {
+ mAlarmBatches.remove(i);
+ }
+ }
+ final boolean newHasTick = haveBatchesTimeTickAlarm(mAlarmBatches);
+ if (oldHasTick != newHasTick) {
+ Slog.wtf(TAG, "removeLocked: hasTick changed from " + oldHasTick + " to " + newHasTick);
+ }
+
+ for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {
+ final Alarm a = mPendingWhileIdleAlarms.get(i);
+ if (a.matches(packageName)) {
+ // Don't set didRemove, since this doesn't impact the scheduled alarms.
+ mPendingWhileIdleAlarms.remove(i);
+ decrementAlarmCount(a.uid, 1);
+ }
+ }
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i --) {
+ final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
+ for (int j = alarmsForUid.size() - 1; j >= 0; j--) {
+ final Alarm alarm = alarmsForUid.get(j);
+ if (alarm.matches(packageName)) {
+ alarmsForUid.remove(j);
+ decrementAlarmCount(alarm.uid, 1);
+ }
+ }
+ if (alarmsForUid.size() == 0) {
+ mPendingBackgroundAlarms.removeAt(i);
+ }
+ }
+ // If we're currently keying off of this app's alarms for doze transitions,
+ // make sure to reset to other triggers.
+ if (removedNextWakeFromIdle.value) {
+ mNextWakeFromIdle = null;
+ }
+ if (didRemove) {
+ if (DEBUG_BATCH) {
+ Slog.v(TAG, "remove(package) changed bounds; rebatching");
+ }
+ rebatchAllAlarmsLocked(true);
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ }
+ }
+
+ // Only called for ephemeral apps
+ void removeForStoppedLocked(final int uid) {
+ if (uid == Process.SYSTEM_UID) {
+ // If a force-stop occurs for a system-uid package, ignore it.
+ return;
+ }
+ boolean didRemove = false;
+ final Predicate<Alarm> whichAlarms = (Alarm a) -> {
+ try {
+ if (a.uid == uid && ActivityManager.getService().isAppStartModeDisabled(
+ uid, a.packageName)) {
+ return true;
+ }
+ } catch (RemoteException e) { /* fall through */}
+ return false;
+ };
+ for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
+ Batch b = mAlarmBatches.get(i);
+ didRemove |= b.remove(whichAlarms, false);
+ if (b.size() == 0) {
+ mAlarmBatches.remove(i);
+ }
+ }
+ for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {
+ final Alarm a = mPendingWhileIdleAlarms.get(i);
+ if (a.uid == uid) {
+ // Don't set didRemove, since this doesn't impact the scheduled alarms.
+ mPendingWhileIdleAlarms.remove(i);
+ decrementAlarmCount(uid, 1);
+ }
+ }
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+ if (mPendingBackgroundAlarms.keyAt(i) == uid) {
+ final ArrayList<Alarm> toRemove = mPendingBackgroundAlarms.valueAt(i);
+ if (toRemove != null) {
+ decrementAlarmCount(uid, toRemove.size());
+ }
+ mPendingBackgroundAlarms.removeAt(i);
+ }
+ }
+ if (didRemove) {
+ if (DEBUG_BATCH) {
+ Slog.v(TAG, "remove(package) changed bounds; rebatching");
+ }
+ rebatchAllAlarmsLocked(true);
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ }
+ }
+
+ void removeUserLocked(int userHandle) {
+ if (userHandle == USER_SYSTEM) {
+ // If we're told we're removing the system user, ignore it.
+ return;
+ }
+ boolean didRemove = false;
+ final Predicate<Alarm> whichAlarms =
+ (Alarm a) -> UserHandle.getUserId(a.creatorUid) == userHandle;
+ for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
+ Batch b = mAlarmBatches.get(i);
+ didRemove |= b.remove(whichAlarms, false);
+ if (b.size() == 0) {
+ mAlarmBatches.remove(i);
+ }
+ }
+ for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {
+ if (UserHandle.getUserId(mPendingWhileIdleAlarms.get(i).creatorUid)
+ == userHandle) {
+ // Don't set didRemove, since this doesn't impact the scheduled alarms.
+ final Alarm removed = mPendingWhileIdleAlarms.remove(i);
+ decrementAlarmCount(removed.uid, 1);
+ }
+ }
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+ if (UserHandle.getUserId(mPendingBackgroundAlarms.keyAt(i)) == userHandle) {
+ final ArrayList<Alarm> toRemove = mPendingBackgroundAlarms.valueAt(i);
+ if (toRemove != null) {
+ for (int j = 0; j < toRemove.size(); j++) {
+ decrementAlarmCount(toRemove.get(j).uid, 1);
+ }
+ }
+ mPendingBackgroundAlarms.removeAt(i);
+ }
+ }
+ for (int i = mLastAllowWhileIdleDispatch.size() - 1; i >= 0; i--) {
+ if (UserHandle.getUserId(mLastAllowWhileIdleDispatch.keyAt(i)) == userHandle) {
+ mLastAllowWhileIdleDispatch.removeAt(i);
+ }
+ }
+
+ if (didRemove) {
+ if (DEBUG_BATCH) {
+ Slog.v(TAG, "remove(user) changed bounds; rebatching");
+ }
+ rebatchAllAlarmsLocked(true);
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ }
+ }
+
+ void interactiveStateChangedLocked(boolean interactive) {
+ if (mInteractive != interactive) {
+ mInteractive = interactive;
+ final long nowELAPSED = mInjector.getElapsedRealtime();
+ if (interactive) {
+ if (mPendingNonWakeupAlarms.size() > 0) {
+ final long thisDelayTime = nowELAPSED - mStartCurrentDelayTime;
+ mTotalDelayTime += thisDelayTime;
+ if (mMaxDelayTime < thisDelayTime) {
+ mMaxDelayTime = thisDelayTime;
+ }
+ deliverAlarmsLocked(mPendingNonWakeupAlarms, nowELAPSED);
+ mPendingNonWakeupAlarms.clear();
+ }
+ if (mNonInteractiveStartTime > 0) {
+ long dur = nowELAPSED - mNonInteractiveStartTime;
+ if (dur > mNonInteractiveTime) {
+ mNonInteractiveTime = dur;
+ }
+ }
+ // And send a TIME_TICK right now, since it is important to get the UI updated.
+ mHandler.post(() ->
+ getContext().sendBroadcastAsUser(mTimeTickIntent, UserHandle.ALL));
+ } else {
+ mNonInteractiveStartTime = nowELAPSED;
+ }
+ }
+ }
+
+ boolean lookForPackageLocked(String packageName) {
+ for (int i = 0; i < mAlarmBatches.size(); i++) {
+ Batch b = mAlarmBatches.get(i);
+ if (b.hasPackage(packageName)) {
+ return true;
+ }
+ }
+ for (int i = 0; i < mPendingWhileIdleAlarms.size(); i++) {
+ final Alarm a = mPendingWhileIdleAlarms.get(i);
+ if (a.matches(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void setLocked(int type, long when) {
+ if (mInjector.isAlarmDriverPresent()) {
+ mInjector.setAlarm(type, when);
+ } else {
+ Message msg = Message.obtain();
+ msg.what = AlarmHandler.ALARM_EVENT;
+
+ mHandler.removeMessages(msg.what);
+ mHandler.sendMessageAtTime(msg, when);
+ }
+ }
+
+ private static final void dumpAlarmList(PrintWriter pw, ArrayList<Alarm> list,
+ String prefix, String label, long nowELAPSED, long nowRTC, SimpleDateFormat sdf) {
+ for (int i=list.size()-1; i>=0; i--) {
+ Alarm a = list.get(i);
+ pw.print(prefix); pw.print(label); pw.print(" #"); pw.print(i);
+ pw.print(": "); pw.println(a);
+ a.dump(pw, prefix + " ", nowELAPSED, nowRTC, sdf);
+ }
+ }
+
+ private static final String labelForType(int type) {
+ switch (type) {
+ case RTC: return "RTC";
+ case RTC_WAKEUP : return "RTC_WAKEUP";
+ case ELAPSED_REALTIME : return "ELAPSED";
+ case ELAPSED_REALTIME_WAKEUP: return "ELAPSED_WAKEUP";
+ }
+ return "--unknown--";
+ }
+
+ private static final void dumpAlarmList(PrintWriter pw, ArrayList<Alarm> list,
+ String prefix, long nowELAPSED, long nowRTC, SimpleDateFormat sdf) {
+ for (int i=list.size()-1; i>=0; i--) {
+ Alarm a = list.get(i);
+ final String label = labelForType(a.type);
+ pw.print(prefix); pw.print(label); pw.print(" #"); pw.print(i);
+ pw.print(": "); pw.println(a);
+ a.dump(pw, prefix + " ", nowELAPSED, nowRTC, sdf);
+ }
+ }
+
+ private boolean isBackgroundRestricted(Alarm alarm) {
+ boolean exemptOnBatterySaver = (alarm.flags & FLAG_ALLOW_WHILE_IDLE) != 0;
+ if (alarm.alarmClock != null) {
+ // Don't defer alarm clocks
+ return false;
+ }
+ if (alarm.operation != null) {
+ if (alarm.operation.isActivity()) {
+ // Don't defer starting actual UI
+ return false;
+ }
+ if (alarm.operation.isForegroundService()) {
+ // FG service alarms are nearly as important; consult AST policy
+ exemptOnBatterySaver = true;
+ }
+ }
+ final String sourcePackage = alarm.sourcePackage;
+ final int sourceUid = alarm.creatorUid;
+ return (mAppStateTracker != null) &&
+ mAppStateTracker.areAlarmsRestricted(sourceUid, sourcePackage,
+ exemptOnBatterySaver);
+ }
+
+ private static native long init();
+ private static native void close(long nativeData);
+ private static native int set(long nativeData, int type, long seconds, long nanoseconds);
+ private static native int waitForAlarm(long nativeData);
+ private static native int setKernelTime(long nativeData, long millis);
+ private static native int setKernelTimezone(long nativeData, int minuteswest);
+ private static native long getNextAlarm(long nativeData, int type);
+
+ private long getWhileIdleMinIntervalLocked(int uid) {
+ final boolean dozing = mPendingIdleUntil != null;
+ final boolean ebs = (mAppStateTracker != null)
+ && mAppStateTracker.isForceAllAppsStandbyEnabled();
+ if (!dozing && !ebs) {
+ return mConstants.ALLOW_WHILE_IDLE_SHORT_TIME;
+ }
+ if (dozing) {
+ return mConstants.ALLOW_WHILE_IDLE_LONG_TIME;
+ }
+ if (mUseAllowWhileIdleShortTime.get(uid)) {
+ // if the last allow-while-idle went off while uid was fg, or the uid
+ // recently came into fg, don't block the alarm for long.
+ return mConstants.ALLOW_WHILE_IDLE_SHORT_TIME;
+ }
+ return mConstants.ALLOW_WHILE_IDLE_LONG_TIME;
+ }
+
+ boolean triggerAlarmsLocked(ArrayList<Alarm> triggerList, final long nowELAPSED) {
+ boolean hasWakeup = false;
+ // batches are temporally sorted, so we need only pull from the
+ // start of the list until we either empty it or hit a batch
+ // that is not yet deliverable
+ while (mAlarmBatches.size() > 0) {
+ Batch batch = mAlarmBatches.get(0);
+ if (batch.start > nowELAPSED) {
+ // Everything else is scheduled for the future
+ break;
+ }
+
+ // We will (re)schedule some alarms now; don't let that interfere
+ // with delivery of this current batch
+ mAlarmBatches.remove(0);
+
+ final int N = batch.size();
+ for (int i = 0; i < N; i++) {
+ Alarm alarm = batch.get(i);
+
+ if ((alarm.flags&AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0) {
+ // If this is an ALLOW_WHILE_IDLE alarm, we constrain how frequently the app can
+ // schedule such alarms. The first such alarm from an app is always delivered.
+ final long lastTime = mLastAllowWhileIdleDispatch.get(alarm.creatorUid, -1);
+ final long minTime = lastTime + getWhileIdleMinIntervalLocked(alarm.creatorUid);
+ if (lastTime >= 0 && nowELAPSED < minTime) {
+ // Whoops, it hasn't been long enough since the last ALLOW_WHILE_IDLE
+ // alarm went off for this app. Reschedule the alarm to be in the
+ // correct time period.
+ alarm.expectedWhenElapsed = alarm.whenElapsed = minTime;
+ if (alarm.maxWhenElapsed < minTime) {
+ alarm.maxWhenElapsed = minTime;
+ }
+ alarm.expectedMaxWhenElapsed = alarm.maxWhenElapsed;
+ if (RECORD_DEVICE_IDLE_ALARMS) {
+ IdleDispatchEntry ent = new IdleDispatchEntry();
+ ent.uid = alarm.uid;
+ ent.pkg = alarm.operation.getCreatorPackage();
+ ent.tag = alarm.operation.getTag("");
+ ent.op = "RESCHEDULE";
+ ent.elapsedRealtime = nowELAPSED;
+ ent.argRealtime = lastTime;
+ mAllowWhileIdleDispatches.add(ent);
+ }
+ setImplLocked(alarm, true, false);
+ continue;
+ }
+ }
+ if (isBackgroundRestricted(alarm)) {
+ // Alarms with FLAG_WAKE_FROM_IDLE or mPendingIdleUntil alarm are not deferred
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG, "Deferring alarm " + alarm + " due to user forced app standby");
+ }
+ ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.get(alarm.creatorUid);
+ if (alarmsForUid == null) {
+ alarmsForUid = new ArrayList<>();
+ mPendingBackgroundAlarms.put(alarm.creatorUid, alarmsForUid);
+ }
+ alarmsForUid.add(alarm);
+ continue;
+ }
+
+ alarm.count = 1;
+ triggerList.add(alarm);
+ if ((alarm.flags&AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) {
+ EventLogTags.writeDeviceIdleWakeFromIdle(mPendingIdleUntil != null ? 1 : 0,
+ alarm.statsTag);
+ }
+ if (mPendingIdleUntil == alarm) {
+ mPendingIdleUntil = null;
+ rebatchAllAlarmsLocked(false);
+ restorePendingWhileIdleAlarmsLocked();
+ }
+ if (mNextWakeFromIdle == alarm) {
+ mNextWakeFromIdle = null;
+ rebatchAllAlarmsLocked(false);
+ }
+
+ // Recurring alarms may have passed several alarm intervals while the
+ // phone was asleep or off, so pass a trigger count when sending them.
+ if (alarm.repeatInterval > 0) {
+ // this adjustment will be zero if we're late by
+ // less than one full repeat interval
+ alarm.count += (nowELAPSED - alarm.expectedWhenElapsed) / alarm.repeatInterval;
+ // Also schedule its next recurrence
+ final long delta = alarm.count * alarm.repeatInterval;
+ final long nextElapsed = alarm.expectedWhenElapsed + delta;
+ setImplLocked(alarm.type, alarm.when + delta, nextElapsed, alarm.windowLength,
+ maxTriggerTime(nowELAPSED, nextElapsed, alarm.repeatInterval),
+ alarm.repeatInterval, alarm.operation, null, null, alarm.flags, true,
+ alarm.workSource, alarm.alarmClock, alarm.uid, alarm.packageName);
+ }
+
+ if (alarm.wakeup) {
+ hasWakeup = true;
+ }
+
+ // We removed an alarm clock. Let the caller recompute the next alarm clock.
+ if (alarm.alarmClock != null) {
+ mNextAlarmClockMayChange = true;
+ }
+ }
+ }
+
+ // This is a new alarm delivery set; bump the sequence number to indicate that
+ // all apps' alarm delivery classes should be recalculated.
+ mCurrentSeq++;
+ calculateDeliveryPriorities(triggerList);
+ Collections.sort(triggerList, mAlarmDispatchComparator);
+
+ if (localLOGV) {
+ for (int i=0; i<triggerList.size(); i++) {
+ Slog.v(TAG, "Triggering alarm #" + i + ": " + triggerList.get(i));
+ }
+ }
+
+ return hasWakeup;
+ }
+
+ /**
+ * This Comparator sorts Alarms into increasing time order.
+ */
+ public static class IncreasingTimeOrder implements Comparator<Alarm> {
+ public int compare(Alarm a1, Alarm a2) {
+ long when1 = a1.whenElapsed;
+ long when2 = a2.whenElapsed;
+ if (when1 > when2) {
+ return 1;
+ }
+ if (when1 < when2) {
+ return -1;
+ }
+ return 0;
+ }
+ }
+
+ @VisibleForTesting
+ static class Alarm {
+ public final int type;
+ public final long origWhen;
+ public final boolean wakeup;
+ public final PendingIntent operation;
+ public final IAlarmListener listener;
+ public final String listenerTag;
+ public final String statsTag;
+ public final WorkSource workSource;
+ public final int flags;
+ public final AlarmManager.AlarmClockInfo alarmClock;
+ public final int uid;
+ public final int creatorUid;
+ public final String packageName;
+ public final String sourcePackage;
+ public int count;
+ public long when;
+ public long windowLength;
+ public long whenElapsed; // 'when' in the elapsed time base
+ public long maxWhenElapsed; // also in the elapsed time base
+ // Expected alarm expiry time before app standby deferring is applied.
+ public long expectedWhenElapsed;
+ public long expectedMaxWhenElapsed;
+ public long repeatInterval;
+ public PriorityClass priorityClass;
+
+ public Alarm(int _type, long _when, long _whenElapsed, long _windowLength, long _maxWhen,
+ long _interval, PendingIntent _op, IAlarmListener _rec, String _listenerTag,
+ WorkSource _ws, int _flags, AlarmManager.AlarmClockInfo _info,
+ int _uid, String _pkgName) {
+ type = _type;
+ origWhen = _when;
+ wakeup = _type == AlarmManager.ELAPSED_REALTIME_WAKEUP
+ || _type == AlarmManager.RTC_WAKEUP;
+ when = _when;
+ whenElapsed = _whenElapsed;
+ expectedWhenElapsed = _whenElapsed;
+ windowLength = _windowLength;
+ maxWhenElapsed = expectedMaxWhenElapsed = clampPositive(_maxWhen);
+ repeatInterval = _interval;
+ operation = _op;
+ listener = _rec;
+ listenerTag = _listenerTag;
+ statsTag = makeTag(_op, _listenerTag, _type);
+ workSource = _ws;
+ flags = _flags;
+ alarmClock = _info;
+ uid = _uid;
+ packageName = _pkgName;
+ sourcePackage = (operation != null) ? operation.getCreatorPackage() : packageName;
+ creatorUid = (operation != null) ? operation.getCreatorUid() : uid;
+ }
+
+ public static String makeTag(PendingIntent pi, String tag, int type) {
+ final String alarmString = type == ELAPSED_REALTIME_WAKEUP || type == RTC_WAKEUP
+ ? "*walarm*:" : "*alarm*:";
+ return (pi != null) ? pi.getTag(alarmString) : (alarmString + tag);
+ }
+
+ public WakeupEvent makeWakeupEvent(long nowRTC) {
+ return new WakeupEvent(nowRTC, creatorUid,
+ (operation != null)
+ ? operation.getIntent().getAction()
+ : ("<listener>:" + listenerTag));
+ }
+
+ // Returns true if either matches
+ public boolean matches(PendingIntent pi, IAlarmListener rec) {
+ return (operation != null)
+ ? operation.equals(pi)
+ : rec != null && listener.asBinder().equals(rec.asBinder());
+ }
+
+ public boolean matches(String packageName) {
+ return packageName.equals(sourcePackage);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("Alarm{");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(" type ");
+ sb.append(type);
+ sb.append(" when ");
+ sb.append(when);
+ sb.append(" ");
+ sb.append(sourcePackage);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ public void dump(PrintWriter pw, String prefix, long nowELAPSED, long nowRTC,
+ SimpleDateFormat sdf) {
+ final boolean isRtc = (type == RTC || type == RTC_WAKEUP);
+ pw.print(prefix); pw.print("tag="); pw.println(statsTag);
+ pw.print(prefix); pw.print("type="); pw.print(type);
+ pw.print(" expectedWhenElapsed="); TimeUtils.formatDuration(
+ expectedWhenElapsed, nowELAPSED, pw);
+ pw.print(" expectedMaxWhenElapsed="); TimeUtils.formatDuration(
+ expectedMaxWhenElapsed, nowELAPSED, pw);
+ pw.print(" whenElapsed="); TimeUtils.formatDuration(whenElapsed,
+ nowELAPSED, pw);
+ pw.print(" maxWhenElapsed="); TimeUtils.formatDuration(maxWhenElapsed,
+ nowELAPSED, pw);
+ pw.print(" when=");
+ if (isRtc) {
+ pw.print(sdf.format(new Date(when)));
+ } else {
+ TimeUtils.formatDuration(when, nowELAPSED, pw);
+ }
+ pw.println();
+ pw.print(prefix); pw.print("window="); TimeUtils.formatDuration(windowLength, pw);
+ pw.print(" repeatInterval="); pw.print(repeatInterval);
+ pw.print(" count="); pw.print(count);
+ pw.print(" flags=0x"); pw.println(Integer.toHexString(flags));
+ if (alarmClock != null) {
+ pw.print(prefix); pw.println("Alarm clock:");
+ pw.print(prefix); pw.print(" triggerTime=");
+ pw.println(sdf.format(new Date(alarmClock.getTriggerTime())));
+ pw.print(prefix); pw.print(" showIntent="); pw.println(alarmClock.getShowIntent());
+ }
+ pw.print(prefix); pw.print("operation="); pw.println(operation);
+ if (listener != null) {
+ pw.print(prefix); pw.print("listener="); pw.println(listener.asBinder());
+ }
+ }
+
+ public void dumpDebug(ProtoOutputStream proto, long fieldId, long nowElapsed,
+ long nowRTC) {
+ final long token = proto.start(fieldId);
+
+ proto.write(AlarmProto.TAG, statsTag);
+ proto.write(AlarmProto.TYPE, type);
+ proto.write(AlarmProto.TIME_UNTIL_WHEN_ELAPSED_MS, whenElapsed - nowElapsed);
+ proto.write(AlarmProto.WINDOW_LENGTH_MS, windowLength);
+ proto.write(AlarmProto.REPEAT_INTERVAL_MS, repeatInterval);
+ proto.write(AlarmProto.COUNT, count);
+ proto.write(AlarmProto.FLAGS, flags);
+ if (alarmClock != null) {
+ alarmClock.dumpDebug(proto, AlarmProto.ALARM_CLOCK);
+ }
+ if (operation != null) {
+ operation.dumpDebug(proto, AlarmProto.OPERATION);
+ }
+ if (listener != null) {
+ proto.write(AlarmProto.LISTENER, listener.asBinder().toString());
+ }
+
+ proto.end(token);
+ }
+ }
+
+ void recordWakeupAlarms(ArrayList<Batch> batches, long nowELAPSED, long nowRTC) {
+ final int numBatches = batches.size();
+ for (int nextBatch = 0; nextBatch < numBatches; nextBatch++) {
+ Batch b = batches.get(nextBatch);
+ if (b.start > nowELAPSED) {
+ break;
+ }
+
+ final int numAlarms = b.alarms.size();
+ for (int nextAlarm = 0; nextAlarm < numAlarms; nextAlarm++) {
+ Alarm a = b.alarms.get(nextAlarm);
+ mRecentWakeups.add(a.makeWakeupEvent(nowRTC));
+ }
+ }
+ }
+
+ long currentNonWakeupFuzzLocked(long nowELAPSED) {
+ long timeSinceOn = nowELAPSED - mNonInteractiveStartTime;
+ if (timeSinceOn < 5*60*1000) {
+ // If the screen has been off for 5 minutes, only delay by at most two minutes.
+ return 2*60*1000;
+ } else if (timeSinceOn < 30*60*1000) {
+ // If the screen has been off for 30 minutes, only delay by at most 15 minutes.
+ return 15*60*1000;
+ } else {
+ // Otherwise, we will delay by at most an hour.
+ return 60*60*1000;
+ }
+ }
+
+ static int fuzzForDuration(long duration) {
+ if (duration < 15*60*1000) {
+ // If the duration until the time is less than 15 minutes, the maximum fuzz
+ // is the duration.
+ return (int)duration;
+ } else if (duration < 90*60*1000) {
+ // If duration is less than 1 1/2 hours, the maximum fuzz is 15 minutes,
+ return 15*60*1000;
+ } else {
+ // Otherwise, we will fuzz by at most half an hour.
+ return 30*60*1000;
+ }
+ }
+
+ boolean checkAllowNonWakeupDelayLocked(long nowELAPSED) {
+ if (mInteractive) {
+ return false;
+ }
+ if (mLastAlarmDeliveryTime <= 0) {
+ return false;
+ }
+ if (mPendingNonWakeupAlarms.size() > 0 && mNextNonWakeupDeliveryTime < nowELAPSED) {
+ // This is just a little paranoia, if somehow we have pending non-wakeup alarms
+ // and the next delivery time is in the past, then just deliver them all. This
+ // avoids bugs where we get stuck in a loop trying to poll for alarms.
+ return false;
+ }
+ long timeSinceLast = nowELAPSED - mLastAlarmDeliveryTime;
+ return timeSinceLast <= currentNonWakeupFuzzLocked(nowELAPSED);
+ }
+
+ void deliverAlarmsLocked(ArrayList<Alarm> triggerList, long nowELAPSED) {
+ mLastAlarmDeliveryTime = nowELAPSED;
+ for (int i=0; i<triggerList.size(); i++) {
+ Alarm alarm = triggerList.get(i);
+ final boolean allowWhileIdle = (alarm.flags&AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0;
+ if (alarm.wakeup) {
+ Trace.traceBegin(Trace.TRACE_TAG_POWER, "Dispatch wakeup alarm to " + alarm.packageName);
+ } else {
+ Trace.traceBegin(Trace.TRACE_TAG_POWER, "Dispatch non-wakeup alarm to " + alarm.packageName);
+ }
+ try {
+ if (localLOGV) {
+ Slog.v(TAG, "sending alarm " + alarm);
+ }
+ if (RECORD_ALARMS_IN_HISTORY) {
+ ActivityManager.noteAlarmStart(alarm.operation, alarm.workSource, alarm.uid,
+ alarm.statsTag);
+ }
+ mDeliveryTracker.deliverLocked(alarm, nowELAPSED, allowWhileIdle);
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Failure sending alarm.", e);
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_POWER);
+ decrementAlarmCount(alarm.uid, 1);
+ }
+ }
+
+ private boolean isExemptFromAppStandby(Alarm a) {
+ return a.alarmClock != null || UserHandle.isCore(a.creatorUid)
+ || (a.flags & FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED) != 0;
+ }
+
+ @VisibleForTesting
+ static class Injector {
+ private long mNativeData;
+ private Context mContext;
+
+ Injector(Context context) {
+ mContext = context;
+ }
+
+ void init() {
+ mNativeData = AlarmManagerService.init();
+ }
+
+ int waitForAlarm() {
+ return AlarmManagerService.waitForAlarm(mNativeData);
+ }
+
+ boolean isAlarmDriverPresent() {
+ return mNativeData != 0;
+ }
+
+ void setAlarm(int type, long millis) {
+ // The kernel never triggers alarms with negative wakeup times
+ // so we ensure they are positive.
+ final long alarmSeconds, alarmNanoseconds;
+ if (millis < 0) {
+ alarmSeconds = 0;
+ alarmNanoseconds = 0;
+ } else {
+ alarmSeconds = millis / 1000;
+ alarmNanoseconds = (millis % 1000) * 1000 * 1000;
+ }
+
+ final int result = AlarmManagerService.set(mNativeData, type, alarmSeconds,
+ alarmNanoseconds);
+ if (result != 0) {
+ final long nowElapsed = SystemClock.elapsedRealtime();
+ Slog.wtf(TAG, "Unable to set kernel alarm, now=" + nowElapsed
+ + " type=" + type + " @ (" + alarmSeconds + "," + alarmNanoseconds
+ + "), ret = " + result + " = " + Os.strerror(result));
+ }
+ }
+
+ long getNextAlarm(int type) {
+ return AlarmManagerService.getNextAlarm(mNativeData, type);
+ }
+
+ void setKernelTimezone(int minutesWest) {
+ AlarmManagerService.setKernelTimezone(mNativeData, minutesWest);
+ }
+
+ void setKernelTime(long millis) {
+ if (mNativeData != 0) {
+ AlarmManagerService.setKernelTime(mNativeData, millis);
+ }
+ }
+
+ void close() {
+ AlarmManagerService.close(mNativeData);
+ }
+
+ long getElapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ long getCurrentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ PowerManager.WakeLock getAlarmWakeLock() {
+ final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ return pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*alarm*");
+ }
+
+ int getSystemUiUid() {
+ PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
+ return pm.getPackageUid(pm.getSystemUiServiceComponent().getPackageName(),
+ MATCH_SYSTEM_ONLY, USER_SYSTEM);
+ }
+
+ ClockReceiver getClockReceiver(AlarmManagerService service) {
+ return service.new ClockReceiver();
+ }
+ }
+
+ private class AlarmThread extends Thread
+ {
+ private int mFalseWakeups;
+ private int mWtfThreshold;
+ public AlarmThread()
+ {
+ super("AlarmManager");
+ mFalseWakeups = 0;
+ mWtfThreshold = 100;
+ }
+
+ public void run()
+ {
+ ArrayList<Alarm> triggerList = new ArrayList<Alarm>();
+
+ while (true)
+ {
+ int result = mInjector.waitForAlarm();
+ final long nowRTC = mInjector.getCurrentTimeMillis();
+ final long nowELAPSED = mInjector.getElapsedRealtime();
+ synchronized (mLock) {
+ mLastWakeup = nowELAPSED;
+ }
+ if (result == 0) {
+ Slog.wtf(TAG, "waitForAlarm returned 0, nowRTC = " + nowRTC
+ + ", nowElapsed = " + nowELAPSED);
+ }
+ triggerList.clear();
+
+ if ((result & TIME_CHANGED_MASK) != 0) {
+ // The kernel can give us spurious time change notifications due to
+ // small adjustments it makes internally; we want to filter those out.
+ final long lastTimeChangeClockTime;
+ final long expectedClockTime;
+ synchronized (mLock) {
+ lastTimeChangeClockTime = mLastTimeChangeClockTime;
+ expectedClockTime = lastTimeChangeClockTime
+ + (nowELAPSED - mLastTimeChangeRealtime);
+ }
+ if (lastTimeChangeClockTime == 0 || nowRTC < (expectedClockTime-1000)
+ || nowRTC > (expectedClockTime+1000)) {
+ // The change is by at least +/- 1000 ms (or this is the first change),
+ // let's do it!
+ if (DEBUG_BATCH) {
+ Slog.v(TAG, "Time changed notification from kernel; rebatching");
+ }
+ // StatsLog requires currentTimeMillis(), which == nowRTC to within usecs.
+ FrameworkStatsLog.write(FrameworkStatsLog.WALL_CLOCK_TIME_SHIFTED, nowRTC);
+ removeImpl(null, mTimeTickTrigger);
+ removeImpl(mDateChangeSender, null);
+ rebatchAllAlarms();
+ mClockReceiver.scheduleTimeTickEvent();
+ mClockReceiver.scheduleDateChangedEvent();
+ synchronized (mLock) {
+ mNumTimeChanged++;
+ mLastTimeChangeClockTime = nowRTC;
+ mLastTimeChangeRealtime = nowELAPSED;
+ }
+ Intent intent = new Intent(Intent.ACTION_TIME_CHANGED);
+ intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+ | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+ | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
+ | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
+ getContext().sendBroadcastAsUser(intent, UserHandle.ALL);
+
+ // The world has changed on us, so we need to re-evaluate alarms
+ // regardless of whether the kernel has told us one went off.
+ result |= IS_WAKEUP_MASK;
+ }
+ }
+
+ if (result != TIME_CHANGED_MASK) {
+ // If this was anything besides just a time change, then figure what if
+ // anything to do about alarms.
+ synchronized (mLock) {
+ if (localLOGV) Slog.v(
+ TAG, "Checking for alarms... rtc=" + nowRTC
+ + ", elapsed=" + nowELAPSED);
+
+ if (WAKEUP_STATS) {
+ if ((result & IS_WAKEUP_MASK) != 0) {
+ long newEarliest = nowRTC - RECENT_WAKEUP_PERIOD;
+ int n = 0;
+ for (WakeupEvent event : mRecentWakeups) {
+ if (event.when > newEarliest) break;
+ n++; // number of now-stale entries at the list head
+ }
+ for (int i = 0; i < n; i++) {
+ mRecentWakeups.remove();
+ }
+
+ recordWakeupAlarms(mAlarmBatches, nowELAPSED, nowRTC);
+ }
+ }
+
+ mLastTrigger = nowELAPSED;
+ boolean hasWakeup = triggerAlarmsLocked(triggerList, nowELAPSED);
+ if (!hasWakeup && checkAllowNonWakeupDelayLocked(nowELAPSED)) {
+ // if there are no wakeup alarms and the screen is off, we can
+ // delay what we have so far until the future.
+ if (mPendingNonWakeupAlarms.size() == 0) {
+ mStartCurrentDelayTime = nowELAPSED;
+ mNextNonWakeupDeliveryTime = nowELAPSED
+ + ((currentNonWakeupFuzzLocked(nowELAPSED)*3)/2);
+ }
+ mPendingNonWakeupAlarms.addAll(triggerList);
+ mNumDelayedAlarms += triggerList.size();
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ } else {
+ // now deliver the alarm intents; if there are pending non-wakeup
+ // alarms, we need to merge them in to the list. note we don't
+ // just deliver them first because we generally want non-wakeup
+ // alarms delivered after wakeup alarms.
+ if (mPendingNonWakeupAlarms.size() > 0) {
+ calculateDeliveryPriorities(mPendingNonWakeupAlarms);
+ triggerList.addAll(mPendingNonWakeupAlarms);
+ Collections.sort(triggerList, mAlarmDispatchComparator);
+ final long thisDelayTime = nowELAPSED - mStartCurrentDelayTime;
+ mTotalDelayTime += thisDelayTime;
+ if (mMaxDelayTime < thisDelayTime) {
+ mMaxDelayTime = thisDelayTime;
+ }
+ mPendingNonWakeupAlarms.clear();
+ }
+ if (mLastTimeChangeRealtime != nowELAPSED && triggerList.isEmpty()) {
+ if (++mFalseWakeups >= mWtfThreshold) {
+ Slog.wtf(TAG, "Too many (" + mFalseWakeups
+ + ") false wakeups, nowElapsed=" + nowELAPSED);
+ if (mWtfThreshold < 100_000) {
+ mWtfThreshold *= 10;
+ } else {
+ mFalseWakeups = 0;
+ }
+ }
+ }
+ final ArraySet<Pair<String, Integer>> triggerPackages =
+ new ArraySet<>();
+ for (int i = 0; i < triggerList.size(); i++) {
+ final Alarm a = triggerList.get(i);
+ if (!isExemptFromAppStandby(a)) {
+ triggerPackages.add(Pair.create(
+ a.sourcePackage, UserHandle.getUserId(a.creatorUid)));
+ }
+ }
+ deliverAlarmsLocked(triggerList, nowELAPSED);
+ reorderAlarmsBasedOnStandbyBuckets(triggerPackages);
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ }
+ }
+
+ } else {
+ // Just in case -- even though no wakeup flag was set, make sure
+ // we have updated the kernel to the next alarm time.
+ synchronized (mLock) {
+ rescheduleKernelAlarmsLocked();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Attribute blame for a WakeLock.
+ * @param ws WorkSource to attribute blame.
+ * @param knownUid attribution uid; < 0 values are ignored.
+ */
+ void setWakelockWorkSource(WorkSource ws, int knownUid, String tag, boolean first) {
+ try {
+ mWakeLock.setHistoryTag(first ? tag : null);
+
+ if (ws != null) {
+ mWakeLock.setWorkSource(ws);
+ return;
+ }
+
+ if (knownUid >= 0) {
+ mWakeLock.setWorkSource(new WorkSource(knownUid));
+ return;
+ }
+ } catch (Exception e) {
+ }
+
+ // Something went wrong; fall back to attributing the lock to the OS
+ mWakeLock.setWorkSource(null);
+ }
+
+ private static int getAlarmAttributionUid(Alarm alarm) {
+ if (alarm.workSource != null && !alarm.workSource.isEmpty()) {
+ return alarm.workSource.getAttributionUid();
+ }
+
+ return alarm.creatorUid;
+ }
+
+ @VisibleForTesting
+ class AlarmHandler extends Handler {
+ public static final int ALARM_EVENT = 1;
+ public static final int SEND_NEXT_ALARM_CLOCK_CHANGED = 2;
+ public static final int LISTENER_TIMEOUT = 3;
+ public static final int REPORT_ALARMS_ACTIVE = 4;
+ public static final int APP_STANDBY_BUCKET_CHANGED = 5;
+ public static final int CHARGING_STATUS_CHANGED = 6;
+ public static final int REMOVE_FOR_STOPPED = 7;
+ public static final int REMOVE_FOR_CANCELED = 8;
+
+ AlarmHandler() {
+ super(Looper.myLooper());
+ }
+
+ public void postRemoveForStopped(int uid) {
+ obtainMessage(REMOVE_FOR_STOPPED, uid, 0).sendToTarget();
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case ALARM_EVENT: {
+ // This code is used when the kernel timer driver is not available, which
+ // shouldn't happen. Here, we try our best to simulate it, which may be useful
+ // when porting Android to a new device. Note that we can't wake up a device
+ // this way, so WAKE_UP alarms will be delivered only when the device is awake.
+ ArrayList<Alarm> triggerList = new ArrayList<Alarm>();
+ synchronized (mLock) {
+ final long nowELAPSED = mInjector.getElapsedRealtime();
+ triggerAlarmsLocked(triggerList, nowELAPSED);
+ updateNextAlarmClockLocked();
+ }
+
+ // now trigger the alarms without the lock held
+ for (int i=0; i<triggerList.size(); i++) {
+ Alarm alarm = triggerList.get(i);
+ try {
+ alarm.operation.send();
+ } catch (PendingIntent.CanceledException e) {
+ if (alarm.repeatInterval > 0) {
+ // This IntentSender is no longer valid, but this
+ // is a repeating alarm, so toss the hoser.
+ removeImpl(alarm.operation, null);
+ }
+ }
+ decrementAlarmCount(alarm.uid, 1);
+ }
+ break;
+ }
+
+ case SEND_NEXT_ALARM_CLOCK_CHANGED:
+ sendNextAlarmClockChanged();
+ break;
+
+ case LISTENER_TIMEOUT:
+ mDeliveryTracker.alarmTimedOut((IBinder) msg.obj);
+ break;
+
+ case REPORT_ALARMS_ACTIVE:
+ if (mLocalDeviceIdleController != null) {
+ mLocalDeviceIdleController.setAlarmsActive(msg.arg1 != 0);
+ }
+ break;
+
+ case CHARGING_STATUS_CHANGED:
+ synchronized (mLock) {
+ mAppStandbyParole = (Boolean) msg.obj;
+ if (reorderAlarmsBasedOnStandbyBuckets(null)) {
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ }
+ }
+ break;
+
+ case APP_STANDBY_BUCKET_CHANGED:
+ synchronized (mLock) {
+ final ArraySet<Pair<String, Integer>> filterPackages = new ArraySet<>();
+ filterPackages.add(Pair.create((String) msg.obj, msg.arg1));
+ if (reorderAlarmsBasedOnStandbyBuckets(filterPackages)) {
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ }
+ }
+ break;
+
+ case REMOVE_FOR_STOPPED:
+ synchronized (mLock) {
+ removeForStoppedLocked(msg.arg1);
+ }
+ break;
+
+ case REMOVE_FOR_CANCELED:
+ final PendingIntent operation = (PendingIntent) msg.obj;
+ synchronized (mLock) {
+ removeLocked(operation, null);
+ }
+ break;
+
+ default:
+ // nope, just ignore it
+ break;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ class ChargingReceiver extends BroadcastReceiver {
+ ChargingReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BatteryManager.ACTION_CHARGING);
+ filter.addAction(BatteryManager.ACTION_DISCHARGING);
+ getContext().registerReceiver(this, filter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ final boolean charging;
+ if (BatteryManager.ACTION_CHARGING.equals(action)) {
+ if (DEBUG_STANDBY) {
+ Slog.d(TAG, "Device is charging.");
+ }
+ charging = true;
+ } else {
+ if (DEBUG_STANDBY) {
+ Slog.d(TAG, "Disconnected from power.");
+ }
+ charging = false;
+ }
+ mHandler.removeMessages(AlarmHandler.CHARGING_STATUS_CHANGED);
+ mHandler.obtainMessage(AlarmHandler.CHARGING_STATUS_CHANGED, charging)
+ .sendToTarget();
+ }
+ }
+
+ @VisibleForTesting
+ class ClockReceiver extends BroadcastReceiver {
+ public ClockReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_DATE_CHANGED);
+ getContext().registerReceiver(this, filter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_DATE_CHANGED)) {
+ // Since the kernel does not keep track of DST, we need to
+ // reset the TZ information at the beginning of each day
+ // based off of the current Zone gmt offset + userspace tracked
+ // daylight savings information.
+ TimeZone zone = TimeZone.getTimeZone(SystemProperties.get(TIMEZONE_PROPERTY));
+ int gmtOffset = zone.getOffset(mInjector.getCurrentTimeMillis());
+ mInjector.setKernelTimezone(-(gmtOffset / 60000));
+ scheduleDateChangedEvent();
+ }
+ }
+
+ public void scheduleTimeTickEvent() {
+ final long currentTime = mInjector.getCurrentTimeMillis();
+ final long nextTime = 60000 * ((currentTime / 60000) + 1);
+
+ // Schedule this event for the amount of time that it would take to get to
+ // the top of the next minute.
+ final long tickEventDelay = nextTime - currentTime;
+
+ final WorkSource workSource = null; // Let system take blame for time tick events.
+ setImpl(ELAPSED_REALTIME, mInjector.getElapsedRealtime() + tickEventDelay, 0,
+ 0, null, mTimeTickTrigger, "TIME_TICK", AlarmManager.FLAG_STANDALONE,
+ workSource, null, Process.myUid(), "android");
+
+ // Finally, remember when we set the tick alarm
+ synchronized (mLock) {
+ mLastTickSet = currentTime;
+ }
+ }
+
+ public void scheduleDateChangedEvent() {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(mInjector.getCurrentTimeMillis());
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.SECOND, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+ calendar.add(Calendar.DAY_OF_MONTH, 1);
+
+ final WorkSource workSource = null; // Let system take blame for date change events.
+ setImpl(RTC, calendar.getTimeInMillis(), 0, 0, mDateChangeSender, null, null,
+ AlarmManager.FLAG_STANDALONE, workSource, null,
+ Process.myUid(), "android");
+ }
+ }
+
+ class InteractiveStateReceiver extends BroadcastReceiver {
+ public InteractiveStateReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ getContext().registerReceiver(this, filter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ synchronized (mLock) {
+ interactiveStateChangedLocked(Intent.ACTION_SCREEN_ON.equals(intent.getAction()));
+ }
+ }
+ }
+
+ class UninstallReceiver extends BroadcastReceiver {
+ public UninstallReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+ filter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART);
+ filter.addDataScheme("package");
+ getContext().registerReceiver(this, filter);
+ // Register for events related to sdcard installation.
+ IntentFilter sdFilter = new IntentFilter();
+ sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+ sdFilter.addAction(Intent.ACTION_USER_STOPPED);
+ sdFilter.addAction(Intent.ACTION_UID_REMOVED);
+ getContext().registerReceiver(this, sdFilter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ synchronized (mLock) {
+ String pkgList[] = null;
+ switch (intent.getAction()) {
+ case Intent.ACTION_QUERY_PACKAGE_RESTART:
+ pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
+ for (String packageName : pkgList) {
+ if (lookForPackageLocked(packageName)) {
+ setResultCode(Activity.RESULT_OK);
+ return;
+ }
+ }
+ return;
+ case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE:
+ pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+ break;
+ case Intent.ACTION_USER_STOPPED:
+ final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (userHandle >= 0) {
+ removeUserLocked(userHandle);
+ mAppWakeupHistory.removeForUser(userHandle);
+ }
+ return;
+ case Intent.ACTION_UID_REMOVED:
+ if (uid >= 0) {
+ mLastAllowWhileIdleDispatch.delete(uid);
+ mUseAllowWhileIdleShortTime.delete(uid);
+ }
+ return;
+ case Intent.ACTION_PACKAGE_REMOVED:
+ if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ // This package is being updated; don't kill its alarms.
+ return;
+ }
+ // Intentional fall-through.
+ case Intent.ACTION_PACKAGE_RESTARTED:
+ final Uri data = intent.getData();
+ if (data != null) {
+ final String pkg = data.getSchemeSpecificPart();
+ if (pkg != null) {
+ pkgList = new String[]{pkg};
+ }
+ }
+ break;
+ }
+ if (pkgList != null && (pkgList.length > 0)) {
+ for (String pkg : pkgList) {
+ if (uid >= 0) {
+ // package-removed and package-restarted case
+ mAppWakeupHistory.removeForPackage(pkg, UserHandle.getUserId(uid));
+ removeLocked(uid);
+ } else {
+ // external-applications-unavailable case
+ removeLocked(pkg);
+ }
+ mPriorities.remove(pkg);
+ for (int i=mBroadcastStats.size()-1; i>=0; i--) {
+ ArrayMap<String, BroadcastStats> uidStats = mBroadcastStats.valueAt(i);
+ if (uidStats.remove(pkg) != null) {
+ if (uidStats.size() <= 0) {
+ mBroadcastStats.removeAt(i);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ final class UidObserver extends IUidObserver.Stub {
+ @Override public void onUidStateChanged(int uid, int procState, long procStateSeq,
+ int capability) {
+ }
+
+ @Override public void onUidGone(int uid, boolean disabled) {
+ if (disabled) {
+ mHandler.postRemoveForStopped(uid);
+ }
+ }
+
+ @Override public void onUidActive(int uid) {
+ }
+
+ @Override public void onUidIdle(int uid, boolean disabled) {
+ if (disabled) {
+ mHandler.postRemoveForStopped(uid);
+ }
+ }
+
+ @Override public void onUidCachedChanged(int uid, boolean cached) {
+ }
+ }
+
+ /**
+ * Tracking of app assignments to standby buckets
+ */
+ private final class AppStandbyTracker extends
+ AppIdleStateChangeListener {
+ @Override
+ public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId,
+ boolean idle, int bucket, int reason) {
+ if (DEBUG_STANDBY) {
+ Slog.d(TAG, "Package " + packageName + " for user " + userId + " now in bucket " +
+ bucket);
+ }
+ mHandler.removeMessages(AlarmHandler.APP_STANDBY_BUCKET_CHANGED);
+ mHandler.obtainMessage(AlarmHandler.APP_STANDBY_BUCKET_CHANGED, userId, -1, packageName)
+ .sendToTarget();
+ }
+ }
+
+ private final Listener mForceAppStandbyListener = new Listener() {
+ @Override
+ public void unblockAllUnrestrictedAlarms() {
+ synchronized (mLock) {
+ sendAllUnrestrictedPendingBackgroundAlarmsLocked();
+ }
+ }
+
+ @Override
+ public void unblockAlarmsForUid(int uid) {
+ synchronized (mLock) {
+ sendPendingBackgroundAlarmsLocked(uid, null);
+ }
+ }
+
+ @Override
+ public void unblockAlarmsForUidPackage(int uid, String packageName) {
+ synchronized (mLock) {
+ sendPendingBackgroundAlarmsLocked(uid, packageName);
+ }
+ }
+
+ @Override
+ public void onUidForeground(int uid, boolean foreground) {
+ synchronized (mLock) {
+ if (foreground) {
+ mUseAllowWhileIdleShortTime.put(uid, true);
+
+ // Note we don't have to drain the pending while-idle alarms here, because
+ // this event should coincide with unblockAlarmsForUid().
+ }
+ }
+ }
+ };
+
+ private final BroadcastStats getStatsLocked(PendingIntent pi) {
+ String pkg = pi.getCreatorPackage();
+ int uid = pi.getCreatorUid();
+ return getStatsLocked(uid, pkg);
+ }
+
+ private final BroadcastStats getStatsLocked(int uid, String pkgName) {
+ ArrayMap<String, BroadcastStats> uidStats = mBroadcastStats.get(uid);
+ if (uidStats == null) {
+ uidStats = new ArrayMap<String, BroadcastStats>();
+ mBroadcastStats.put(uid, uidStats);
+ }
+ BroadcastStats bs = uidStats.get(pkgName);
+ if (bs == null) {
+ bs = new BroadcastStats(uid, pkgName);
+ uidStats.put(pkgName, bs);
+ }
+ return bs;
+ }
+
+ /**
+ * Canonical count of (operation.send() - onSendFinished()) and
+ * listener send/complete/timeout invocations.
+ * Guarded by the usual lock.
+ */
+ @GuardedBy("mLock")
+ private int mSendCount = 0;
+ @GuardedBy("mLock")
+ private int mSendFinishCount = 0;
+ @GuardedBy("mLock")
+ private int mListenerCount = 0;
+ @GuardedBy("mLock")
+ private int mListenerFinishCount = 0;
+
+ class DeliveryTracker extends IAlarmCompleteListener.Stub implements PendingIntent.OnFinished {
+
+ private InFlight removeLocked(PendingIntent pi, Intent intent) {
+ for (int i = 0; i < mInFlight.size(); i++) {
+ final InFlight inflight = mInFlight.get(i);
+ if (inflight.mPendingIntent == pi) {
+ if (pi.isBroadcast()) {
+ notifyBroadcastAlarmCompleteLocked(inflight.mUid);
+ }
+ return mInFlight.remove(i);
+ }
+ }
+ mLog.w("No in-flight alarm for " + pi + " " + intent);
+ return null;
+ }
+
+ private InFlight removeLocked(IBinder listener) {
+ for (int i = 0; i < mInFlight.size(); i++) {
+ if (mInFlight.get(i).mListener == listener) {
+ return mInFlight.remove(i);
+ }
+ }
+ mLog.w("No in-flight alarm for listener " + listener);
+ return null;
+ }
+
+ private void updateStatsLocked(InFlight inflight) {
+ final long nowELAPSED = mInjector.getElapsedRealtime();
+ BroadcastStats bs = inflight.mBroadcastStats;
+ bs.nesting--;
+ if (bs.nesting <= 0) {
+ bs.nesting = 0;
+ bs.aggregateTime += nowELAPSED - bs.startTime;
+ }
+ FilterStats fs = inflight.mFilterStats;
+ fs.nesting--;
+ if (fs.nesting <= 0) {
+ fs.nesting = 0;
+ fs.aggregateTime += nowELAPSED - fs.startTime;
+ }
+ if (RECORD_ALARMS_IN_HISTORY) {
+ ActivityManager.noteAlarmFinish(inflight.mPendingIntent, inflight.mWorkSource,
+ inflight.mUid, inflight.mTag);
+ }
+ }
+
+ private void updateTrackingLocked(InFlight inflight) {
+ if (inflight != null) {
+ updateStatsLocked(inflight);
+ }
+ mBroadcastRefCount--;
+ if (DEBUG_WAKELOCK) {
+ Slog.d(TAG, "mBroadcastRefCount -> " + mBroadcastRefCount);
+ }
+ if (mBroadcastRefCount == 0) {
+ mHandler.obtainMessage(AlarmHandler.REPORT_ALARMS_ACTIVE, 0).sendToTarget();
+ mWakeLock.release();
+ if (mInFlight.size() > 0) {
+ mLog.w("Finished all dispatches with " + mInFlight.size()
+ + " remaining inflights");
+ for (int i=0; i<mInFlight.size(); i++) {
+ mLog.w(" Remaining #" + i + ": " + mInFlight.get(i));
+ }
+ mInFlight.clear();
+ }
+ } else {
+ // the next of our alarms is now in flight. reattribute the wakelock.
+ if (mInFlight.size() > 0) {
+ InFlight inFlight = mInFlight.get(0);
+ setWakelockWorkSource(inFlight.mWorkSource, inFlight.mCreatorUid, inFlight.mTag,
+ false);
+ } else {
+ // should never happen
+ mLog.w("Alarm wakelock still held but sent queue empty");
+ mWakeLock.setWorkSource(null);
+ }
+ }
+ }
+
+ /**
+ * Callback that arrives when a direct-call alarm reports that delivery has finished
+ */
+ @Override
+ public void alarmComplete(IBinder who) {
+ if (who == null) {
+ mLog.w("Invalid alarmComplete: uid=" + Binder.getCallingUid()
+ + " pid=" + Binder.getCallingPid());
+ return;
+ }
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ mHandler.removeMessages(AlarmHandler.LISTENER_TIMEOUT, who);
+ InFlight inflight = removeLocked(who);
+ if (inflight != null) {
+ if (DEBUG_LISTENER_CALLBACK) {
+ Slog.i(TAG, "alarmComplete() from " + who);
+ }
+ updateTrackingLocked(inflight);
+ mListenerFinishCount++;
+ } else {
+ // Delivery timed out, and the timeout handling already took care of
+ // updating our tracking here, so we needn't do anything further.
+ if (DEBUG_LISTENER_CALLBACK) {
+ Slog.i(TAG, "Late alarmComplete() from " + who);
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * Callback that arrives when a PendingIntent alarm has finished delivery
+ */
+ @Override
+ public void onSendFinished(PendingIntent pi, Intent intent, int resultCode,
+ String resultData, Bundle resultExtras) {
+ synchronized (mLock) {
+ mSendFinishCount++;
+ updateTrackingLocked(removeLocked(pi, intent));
+ }
+ }
+
+ /**
+ * Timeout of a direct-call alarm delivery
+ */
+ public void alarmTimedOut(IBinder who) {
+ synchronized (mLock) {
+ InFlight inflight = removeLocked(who);
+ if (inflight != null) {
+ // TODO: implement ANR policy for the target
+ if (DEBUG_LISTENER_CALLBACK) {
+ Slog.i(TAG, "Alarm listener " + who + " timed out in delivery");
+ }
+ updateTrackingLocked(inflight);
+ mListenerFinishCount++;
+ } else {
+ if (DEBUG_LISTENER_CALLBACK) {
+ Slog.i(TAG, "Spurious timeout of listener " + who);
+ }
+ mLog.w("Spurious timeout of listener " + who);
+ }
+ }
+ }
+
+ /**
+ * Deliver an alarm and set up the post-delivery handling appropriately
+ */
+ @GuardedBy("mLock")
+ public void deliverLocked(Alarm alarm, long nowELAPSED, boolean allowWhileIdle) {
+ final long workSourceToken = ThreadLocalWorkSource.setUid(
+ getAlarmAttributionUid(alarm));
+ try {
+ if (alarm.operation != null) {
+ // PendingIntent alarm
+ mSendCount++;
+
+ try {
+ alarm.operation.send(getContext(), 0,
+ mBackgroundIntent.putExtra(
+ Intent.EXTRA_ALARM_COUNT, alarm.count),
+ mDeliveryTracker, mHandler, null,
+ allowWhileIdle ? mIdleOptions : null);
+ } catch (PendingIntent.CanceledException e) {
+ if (alarm.repeatInterval > 0) {
+ // This IntentSender is no longer valid, but this
+ // is a repeating alarm, so toss it
+ removeImpl(alarm.operation, null);
+ }
+ // No actual delivery was possible, so the delivery tracker's
+ // 'finished' callback won't be invoked. We also don't need
+ // to do any wakelock or stats tracking, so we have nothing
+ // left to do here but go on to the next thing.
+ mSendFinishCount++;
+ return;
+ }
+ } else {
+ // Direct listener callback alarm
+ mListenerCount++;
+
+ if (RECORD_ALARMS_IN_HISTORY) {
+ if (alarm.listener == mTimeTickTrigger) {
+ mTickHistory[mNextTickHistory++] = nowELAPSED;
+ if (mNextTickHistory >= TICK_HISTORY_DEPTH) {
+ mNextTickHistory = 0;
+ }
+ }
+ }
+
+ try {
+ if (DEBUG_LISTENER_CALLBACK) {
+ Slog.v(TAG, "Alarm to uid=" + alarm.uid
+ + " listener=" + alarm.listener.asBinder());
+ }
+ alarm.listener.doAlarm(this);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(AlarmHandler.LISTENER_TIMEOUT,
+ alarm.listener.asBinder()),
+ mConstants.LISTENER_TIMEOUT);
+ } catch (Exception e) {
+ if (DEBUG_LISTENER_CALLBACK) {
+ Slog.i(TAG, "Alarm undeliverable to listener "
+ + alarm.listener.asBinder(), e);
+ }
+ // As in the PendingIntent.CanceledException case, delivery of the
+ // alarm was not possible, so we have no wakelock or timeout or
+ // stats management to do. It threw before we posted the delayed
+ // timeout message, so we're done here.
+ mListenerFinishCount++;
+ return;
+ }
+ }
+ } finally {
+ ThreadLocalWorkSource.restore(workSourceToken);
+ }
+
+ // The alarm is now in flight; now arrange wakelock and stats tracking
+ if (DEBUG_WAKELOCK) {
+ Slog.d(TAG, "mBroadcastRefCount -> " + (mBroadcastRefCount + 1));
+ }
+ if (mBroadcastRefCount == 0) {
+ setWakelockWorkSource(alarm.workSource, alarm.creatorUid, alarm.statsTag, true);
+ mWakeLock.acquire();
+ mHandler.obtainMessage(AlarmHandler.REPORT_ALARMS_ACTIVE, 1).sendToTarget();
+ }
+ final InFlight inflight = new InFlight(AlarmManagerService.this, alarm, nowELAPSED);
+ mInFlight.add(inflight);
+ mBroadcastRefCount++;
+ if (inflight.isBroadcast()) {
+ notifyBroadcastAlarmPendingLocked(alarm.uid);
+ }
+ if (allowWhileIdle) {
+ // Record the last time this uid handled an ALLOW_WHILE_IDLE alarm.
+ mLastAllowWhileIdleDispatch.put(alarm.creatorUid, nowELAPSED);
+ if ((mAppStateTracker == null)
+ || mAppStateTracker.isUidInForeground(alarm.creatorUid)) {
+ mUseAllowWhileIdleShortTime.put(alarm.creatorUid, true);
+ } else {
+ mUseAllowWhileIdleShortTime.put(alarm.creatorUid, false);
+ }
+ if (RECORD_DEVICE_IDLE_ALARMS) {
+ IdleDispatchEntry ent = new IdleDispatchEntry();
+ ent.uid = alarm.uid;
+ ent.pkg = alarm.packageName;
+ ent.tag = alarm.statsTag;
+ ent.op = "DELIVER";
+ ent.elapsedRealtime = nowELAPSED;
+ mAllowWhileIdleDispatches.add(ent);
+ }
+ }
+ if (!isExemptFromAppStandby(alarm)) {
+ final Pair<String, Integer> packageUser = Pair.create(alarm.sourcePackage,
+ UserHandle.getUserId(alarm.creatorUid));
+ mAppWakeupHistory.recordAlarmForPackage(alarm.sourcePackage,
+ UserHandle.getUserId(alarm.creatorUid), nowELAPSED);
+ }
+ final BroadcastStats bs = inflight.mBroadcastStats;
+ bs.count++;
+ if (bs.nesting == 0) {
+ bs.nesting = 1;
+ bs.startTime = nowELAPSED;
+ } else {
+ bs.nesting++;
+ }
+ final FilterStats fs = inflight.mFilterStats;
+ fs.count++;
+ if (fs.nesting == 0) {
+ fs.nesting = 1;
+ fs.startTime = nowELAPSED;
+ } else {
+ fs.nesting++;
+ }
+ if (alarm.type == ELAPSED_REALTIME_WAKEUP
+ || alarm.type == RTC_WAKEUP) {
+ bs.numWakeup++;
+ fs.numWakeup++;
+ ActivityManager.noteWakeupAlarm(
+ alarm.operation, alarm.workSource, alarm.uid, alarm.packageName,
+ alarm.statsTag);
+ }
+ }
+ }
+
+ private void incrementAlarmCount(int uid) {
+ final int uidIndex = mAlarmsPerUid.indexOfKey(uid);
+ if (uidIndex >= 0) {
+ mAlarmsPerUid.setValueAt(uidIndex, mAlarmsPerUid.valueAt(uidIndex) + 1);
+ } else {
+ mAlarmsPerUid.put(uid, 1);
+ }
+ }
+
+ private void decrementAlarmCount(int uid, int decrement) {
+ int oldCount = 0;
+ final int uidIndex = mAlarmsPerUid.indexOfKey(uid);
+ if (uidIndex >= 0) {
+ oldCount = mAlarmsPerUid.valueAt(uidIndex);
+ if (oldCount > decrement) {
+ mAlarmsPerUid.setValueAt(uidIndex, oldCount - decrement);
+ } else {
+ mAlarmsPerUid.removeAt(uidIndex);
+ }
+ }
+ if (oldCount < decrement) {
+ Slog.wtf(TAG, "Attempt to decrement existing alarm count " + oldCount + " by "
+ + decrement + " for uid " + uid);
+ }
+ }
+
+ private class ShellCmd extends ShellCommand {
+
+ IAlarmManager getBinderService() {
+ return IAlarmManager.Stub.asInterface(mService);
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+
+ final PrintWriter pw = getOutPrintWriter();
+ try {
+ switch (cmd) {
+ case "set-time":
+ final long millis = Long.parseLong(getNextArgRequired());
+ return (getBinderService().setTime(millis)) ? 0 : -1;
+ case "set-timezone":
+ final String tz = getNextArgRequired();
+ getBinderService().setTimeZone(tz);
+ return 0;
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ } catch (Exception e) {
+ pw.println(e);
+ }
+ return -1;
+ }
+
+ @Override
+ public void onHelp() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("Alarm manager service (alarm) commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println(" set-time TIME");
+ pw.println(" Set the system clock time to TIME where TIME is milliseconds");
+ pw.println(" since the Epoch.");
+ pw.println(" set-timezone TZ");
+ pw.println(" Set the system timezone to TZ where TZ is an Olson id.");
+ }
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/alarm/TEST_MAPPING
new file mode 100644
index 000000000000..d76ce7449e43
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/TEST_MAPPING
@@ -0,0 +1,24 @@
+{
+ "presubmit": [
+ {
+ "name": "FrameworksMockingServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.alarm"
+ },
+ {
+ "include-annotation": "android.platform.test.annotations.Presubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ],
+
+ "postsubmit": [
+ {
+ "name": "CtsAlarmManagerTestCases"
+ }
+ ]
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index 435384dd2319..a8906ac9d7a5 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -25,6 +25,7 @@ import android.content.IntentFilter;
import android.os.Handler;
import android.os.PowerManager;
import android.os.RemoteException;
+import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
@@ -33,7 +34,6 @@ 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;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index e88865161dfa..a1c13a9ad51c 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -70,6 +70,7 @@ import android.os.WorkSource;
import android.provider.Settings;
import android.text.format.DateUtils;
import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
import android.util.KeyValueListParser;
import android.util.Log;
import android.util.Slog;
@@ -84,7 +85,6 @@ 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;
@@ -684,12 +684,12 @@ public class JobSchedulerService extends com.android.server.SystemService
void dump(IndentingPrintWriter pw) {
pw.println("Settings:");
pw.increaseIndent();
- pw.printPair(KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT,
+ pw.print(KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT,
MIN_READY_NON_ACTIVE_JOBS_COUNT).println();
- pw.printPair(KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS,
+ pw.print(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();
+ pw.print(KEY_HEAVY_USE_FACTOR, HEAVY_USE_FACTOR).println();
+ pw.print(KEY_MODERATE_USE_FACTOR, MODERATE_USE_FACTOR).println();
MAX_JOB_COUNTS_SCREEN_ON.normal.dump(pw, "");
MAX_JOB_COUNTS_SCREEN_ON.moderate.dump(pw, "");
@@ -703,15 +703,15 @@ public class JobSchedulerService extends com.android.server.SystemService
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.print(KEY_MIN_LINEAR_BACKOFF_TIME, MIN_LINEAR_BACKOFF_TIME).println();
+ pw.print(KEY_MIN_EXP_BACKOFF_TIME, MIN_EXP_BACKOFF_TIME).println();
+ pw.print(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println();
+ pw.print(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,
+ pw.print(KEY_ENABLE_API_QUOTAS, ENABLE_API_QUOTAS).println();
+ pw.print(KEY_API_QUOTA_SCHEDULE_COUNT, API_QUOTA_SCHEDULE_COUNT).println();
+ pw.print(KEY_API_QUOTA_SCHEDULE_WINDOW_MS, API_QUOTA_SCHEDULE_WINDOW_MS).println();
+ pw.print(KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION,
API_QUOTA_SCHEDULE_THROW_EXCEPTION).println();
pw.decreaseIndent();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
index 2f5f555817ec..f2a55805d70a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -36,6 +36,7 @@ import android.util.AtomicFile;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SystemConfigFileCommitEventLogger;
import android.util.Xml;
import com.android.internal.annotations.GuardedBy;
@@ -102,6 +103,7 @@ public final class JobStore {
private boolean mWriteInProgress;
private static final Object sSingletonLock = new Object();
+ private final SystemConfigFileCommitEventLogger mEventLogger;
private final AtomicFile mJobsFile;
/** Handler backed by IoThread for writing to disk. */
private final Handler mIoHandler = IoThread.getHandler();
@@ -141,7 +143,8 @@ public final class JobStore {
File systemDir = new File(dataDir, "system");
File jobDir = new File(systemDir, "job");
jobDir.mkdirs();
- mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), "jobs");
+ mEventLogger = new SystemConfigFileCommitEventLogger("jobs");
+ mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), mEventLogger);
mJobSet = new JobSet();
@@ -426,7 +429,7 @@ public final class JobStore {
int numSystemJobs = 0;
int numSyncJobs = 0;
try {
- final long startTime = SystemClock.uptimeMillis();
+ mEventLogger.setStartTime(SystemClock.uptimeMillis());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
XmlSerializer out = new FastXmlSerializer();
out.setOutput(baos, StandardCharsets.UTF_8.name());
@@ -459,7 +462,7 @@ public final class JobStore {
out.endDocument();
// Write out to disk in one fell swoop.
- FileOutputStream fos = mJobsFile.startWrite(startTime);
+ FileOutputStream fos = mJobsFile.startWrite();
fos.write(baos.toByteArray());
mJobsFile.finishWrite(fos);
} catch (IOException e) {
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
index 1645bcb928c1..fd26b72ef9d9 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
@@ -20,11 +20,11 @@ import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.util.IndentingPrintWriter;
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;
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
index 461ef21af7ee..28269c89d13b 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
@@ -26,12 +26,12 @@ import android.os.BatteryManager;
import android.os.BatteryManagerInternal;
import android.os.UserHandle;
import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
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;
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
index bb94275fc409..f1c624d1d9f5 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -39,6 +39,7 @@ import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.DataUnit;
+import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
@@ -46,7 +47,6 @@ 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;
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
index 5fcd774189ac..50723c7c2841 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
@@ -24,13 +24,13 @@ import android.os.Handler;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
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;
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
index 01f5fa62f889..04b41646b48b 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
@@ -27,13 +27,13 @@ import android.os.Message;
import android.os.PowerManager;
import android.os.UserHandle;
import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
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;
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
index c0b3204192d6..2fe827e338e9 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
@@ -20,9 +20,9 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
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;
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
index d108f0b698f7..bc4e396a4fe1 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -52,6 +52,7 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
import android.util.KeyValueListParser;
import android.util.Log;
import android.util.Pair;
@@ -64,7 +65,6 @@ 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;
@@ -2496,32 +2496,32 @@ public final class QuotaController extends StateController {
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,
+ pw.print(KEY_ALLOWED_TIME_PER_PERIOD_MS, ALLOWED_TIME_PER_PERIOD_MS).println();
+ pw.print(KEY_IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS).println();
+ pw.print(KEY_WINDOW_SIZE_ACTIVE_MS, WINDOW_SIZE_ACTIVE_MS).println();
+ pw.print(KEY_WINDOW_SIZE_WORKING_MS, WINDOW_SIZE_WORKING_MS).println();
+ pw.print(KEY_WINDOW_SIZE_FREQUENT_MS, WINDOW_SIZE_FREQUENT_MS).println();
+ pw.print(KEY_WINDOW_SIZE_RARE_MS, WINDOW_SIZE_RARE_MS).println();
+ pw.print(KEY_WINDOW_SIZE_RESTRICTED_MS, WINDOW_SIZE_RESTRICTED_MS).println();
+ pw.print(KEY_MAX_EXECUTION_TIME_MS, MAX_EXECUTION_TIME_MS).println();
+ pw.print(KEY_MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE).println();
+ pw.print(KEY_MAX_JOB_COUNT_WORKING, MAX_JOB_COUNT_WORKING).println();
+ pw.print(KEY_MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT).println();
+ pw.print(KEY_MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE).println();
+ pw.print(KEY_MAX_JOB_COUNT_RESTRICTED, MAX_JOB_COUNT_RESTRICTED).println();
+ pw.print(KEY_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS).println();
+ pw.print(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,
+ pw.print(KEY_MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE).println();
+ pw.print(KEY_MAX_SESSION_COUNT_WORKING, MAX_SESSION_COUNT_WORKING).println();
+ pw.print(KEY_MAX_SESSION_COUNT_FREQUENT, MAX_SESSION_COUNT_FREQUENT).println();
+ pw.print(KEY_MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE).println();
+ pw.print(KEY_MAX_SESSION_COUNT_RESTRICTED, MAX_SESSION_COUNT_RESTRICTED).println();
+ pw.print(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW).println();
- pw.printPair(KEY_TIMING_SESSION_COALESCING_DURATION_MS,
+ pw.print(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.print(KEY_MIN_QUOTA_CHECK_DELAY_MS, MIN_QUOTA_CHECK_DELAY_MS).println();
pw.decreaseIndent();
}
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
index 51be38be990d..71c759931f57 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
@@ -19,10 +19,10 @@ package com.android.server.job.controllers;
import static com.android.server.job.JobSchedulerService.DEBUG;
import android.content.Context;
+import android.util.IndentingPrintWriter;
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;
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
index 51187dff4d59..0731918d83a1 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java
@@ -24,12 +24,12 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserHandle;
import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
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;
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
index 1bb9e967c025..361ebe55ccd8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java
@@ -31,6 +31,7 @@ import android.os.Process;
import android.os.UserHandle;
import android.os.WorkSource;
import android.provider.Settings;
+import android.util.IndentingPrintWriter;
import android.util.KeyValueListParser;
import android.util.Log;
import android.util.Slog;
@@ -38,7 +39,6 @@ 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;
@@ -498,7 +498,7 @@ public final class TimeController extends StateController {
pw.println();
pw.println("TimeController:");
pw.increaseIndent();
- pw.printPair(KEY_USE_NON_WAKEUP_ALARM_FOR_DELAY,
+ pw.print(KEY_USE_NON_WAKEUP_ALARM_FOR_DELAY,
USE_NON_WAKEUP_ALARM_FOR_DELAY).println();
pw.decreaseIndent();
}
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
index e180c55e1bf2..ac59f9542e99 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
@@ -17,9 +17,9 @@
package com.android.server.job.restrictions;
import android.app.job.JobInfo;
+import android.util.IndentingPrintWriter;
import android.util.proto.ProtoOutputStream;
-import com.android.internal.util.IndentingPrintWriter;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.controllers.JobStatus;
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
index aa7696df6dbd..40c8ce0d5c89 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
@@ -19,9 +19,9 @@ package com.android.server.job.restrictions;
import android.app.job.JobParameters;
import android.os.PowerManager;
import android.os.PowerManager.OnThermalStatusChangedListener;
+import android.util.IndentingPrintWriter;
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;
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
index 372ec981df02..4b8d6f3e0e03 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
@@ -36,6 +36,7 @@ import android.app.usage.UsageStatsManager;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.AtomicFile;
+import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TimeUtils;
@@ -45,7 +46,6 @@ 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;
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 280a6870a5e1..32bf9e48bdb6 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -95,6 +95,7 @@ import android.provider.Settings.Global;
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
import android.util.KeyValueListParser;
import android.util.Slog;
import android.util.SparseArray;
@@ -107,10 +108,8 @@ 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;
@@ -325,9 +324,30 @@ public class AppStandbyController implements AppStandbyInternal {
private PackageManager mPackageManager;
Injector mInjector;
- static final ArrayList<StandbyUpdateRecord> sStandbyUpdatePool = new ArrayList<>(4);
+ private static class Pool<T> {
+ private final T[] mArray;
+ private int mSize = 0;
+
+ Pool(T[] array) {
+ mArray = array;
+ }
+
+ @Nullable
+ synchronized T obtain() {
+ return mSize > 0 ? mArray[--mSize] : null;
+ }
+
+ synchronized void recycle(T instance) {
+ if (mSize < mArray.length) {
+ mArray[mSize++] = instance;
+ }
+ }
+ }
+
+ private static class StandbyUpdateRecord {
+ private static final Pool<StandbyUpdateRecord> sPool =
+ new Pool<>(new StandbyUpdateRecord[10]);
- public static class StandbyUpdateRecord {
// Identity of the app whose standby state has changed
String packageName;
int userId;
@@ -341,36 +361,48 @@ public class AppStandbyController implements AppStandbyInternal {
// 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;
+ StandbyUpdateRecord r = sPool.obtain();
+ if (r == null) {
+ r = new StandbyUpdateRecord();
}
+ 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);
+ sPool.recycle(this);
+ }
+ }
+
+ private static class ContentProviderUsageRecord {
+ private static final Pool<ContentProviderUsageRecord> sPool =
+ new Pool<>(new ContentProviderUsageRecord[10]);
+
+ public String name;
+ public String packageName;
+ public int userId;
+
+ public static ContentProviderUsageRecord obtain(String name, String packageName,
+ int userId) {
+ ContentProviderUsageRecord r = sPool.obtain();
+ if (r == null) {
+ r = new ContentProviderUsageRecord();
}
+ r.name = name;
+ r.packageName = packageName;
+ r.userId = userId;
+ return r;
+ }
+
+ public void recycle() {
+ sPool.recycle(this);
}
}
@@ -415,7 +447,6 @@ public class AppStandbyController implements AppStandbyInternal {
}
}
}
-
}
@Override
@@ -1760,11 +1791,9 @@ public class AppStandbyController implements AppStandbyInternal {
@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)
+ ContentProviderUsageRecord record = ContentProviderUsageRecord.obtain(name, packageName,
+ userId);
+ mHandler.obtainMessage(MSG_REPORT_CONTENT_PROVIDER_USAGE, record)
.sendToTarget();
}
@@ -2074,11 +2103,9 @@ public class AppStandbyController implements AppStandbyInternal {
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();
+ ContentProviderUsageRecord record = (ContentProviderUsageRecord) msg.obj;
+ reportContentProviderUsage(record.name, record.packageName, record.userId);
+ record.recycle();
break;
case MSG_PAROLE_STATE_CHANGED:
diff --git a/apex/media/OWNERS b/apex/media/OWNERS
index 9b853c5dd7d8..e83ea3a5087a 100644
--- a/apex/media/OWNERS
+++ b/apex/media/OWNERS
@@ -1,4 +1,7 @@
andrewlewis@google.com
aquilescanta@google.com
+chz@google.com
+hkuang@google.com
+lnilsson@google.com
marcone@google.com
sungsoo@google.com