diff options
Diffstat (limited to 'apex')
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 Binary files differnew file mode 100644 index 000000000000..4e5acae9c1e4 --- /dev/null +++ b/apex/appsearch/com.android.appsearch.avbpubkey 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 Binary files differnew file mode 100644 index 000000000000..77e98b20877b --- /dev/null +++ b/apex/appsearch/com.android.appsearch.pk8 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}<{@link AppSearchResult}<{@link Void}>>. + * 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}<byte[]> of serialized DocumentProtos. + * @param callback + * {@link AndroidFuture}<{@link AppSearchBatchResult}<{@link String}, {@link Void}>>. + * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise, + * {@code callback} will be completed with an + * {@link AppSearchBatchResult}<{@link String}, {@link Void}> + * 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}<{@link AppSearchBatchResult}<{@link String}, {@link byte[]}>>. + * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise, + * {@code callback} will be completed with an + * {@link AppSearchBatchResult}<{@link String}, {@link byte[]}> + * 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}<{@link AppSearchResult}<{@link byte[]}>> + * 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}<{@link AppSearchBatchResult}<{@link String}, {@link Void}>>. + * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise, + * {@code callback} will be completed with an + * {@link AppSearchBatchResult}<{@link String}, {@link Void}> + * 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}<{@link AppSearchBatchResult}<{@link String}, {@link Void}>>. + * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise, + * {@code callback} will be completed with an + * {@link AppSearchBatchResult}<{@link String}, {@link Void}> + * 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}<{@link AppSearchResult}<{@link Void}>>. + * 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 <receiver> 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 |