diff options
author | Xin Li <delphij@google.com> | 2020-09-10 17:22:01 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2020-09-10 17:22:01 +0000 |
commit | 8ac6741e47c76bde065f868ea64d2f04541487b9 (patch) | |
tree | 1a679458fdbd8d370692d56791e2bf83acee35b5 /apex | |
parent | 3de940cc40b1e3fdf8224e18a8308a16768cbfa8 (diff) | |
parent | c64112eb974e9aa7638aead998f07a868acfb5a7 (diff) |
Merge "Merge Android R"
Diffstat (limited to 'apex')
216 files changed, 50664 insertions, 1 deletions
diff --git a/apex/Android.bp b/apex/Android.bp index 74db82825e6a..a5e2b4a5b707 100644 --- a/apex/Android.bp +++ b/apex/Android.bp @@ -106,7 +106,10 @@ java_defaults { stubs_library_visibility: ["//visibility:public"], // Hide impl library and stub sources - impl_library_visibility: [":__package__"], + impl_library_visibility: [ + ":__package__", + "//frameworks/base", // For framework-all + ], stubs_source_visibility: ["//visibility:private"], // Collates API usages from each module for further analysis. @@ -158,6 +161,9 @@ stubs_defaults { api_file: "api/current.txt", removed_api_file: "api/removed.txt", }, + api_lint: { + enabled: true, + }, }, dist: { targets: ["sdk", "win_sdk"], @@ -181,6 +187,9 @@ stubs_defaults { api_file: "api/system-current.txt", removed_api_file: "api/system-removed.txt", }, + api_lint: { + enabled: true, + }, }, dist: { targets: ["sdk", "win_sdk"], @@ -193,6 +202,7 @@ java_defaults { installable: false, sdk_version: "module_current", libs: [ "stub-annotations" ], + java_version: "1.8", dist: { targets: ["sdk", "win_sdk"], dir: "apistubs/android/public", @@ -204,6 +214,7 @@ java_defaults { installable: false, sdk_version: "module_current", libs: [ "stub-annotations" ], + java_version: "1.8", dist: { targets: ["sdk", "win_sdk"], dir: "apistubs/android/system", @@ -215,6 +226,7 @@ java_defaults { installable: false, sdk_version: "module_current", libs: [ "stub-annotations" ], + java_version: "1.8", dist: { targets: ["sdk", "win_sdk"], dir: "apistubs/android/module-lib", @@ -246,6 +258,9 @@ stubs_defaults { api_file: "api/module-lib-current.txt", removed_api_file: "api/module-lib-removed.txt", }, + api_lint: { + enabled: true, + }, }, dist: { targets: ["sdk", "win_sdk"], @@ -280,6 +295,9 @@ stubs_defaults { api_file: "api/current.txt", removed_api_file: "api/removed.txt", }, + api_lint: { + enabled: true, + }, }, dist: { targets: ["sdk", "win_sdk"], diff --git a/apex/blobstore/OWNERS b/apex/blobstore/OWNERS new file mode 100644 index 000000000000..8e04399196e2 --- /dev/null +++ b/apex/blobstore/OWNERS @@ -0,0 +1,4 @@ +set noparent + +sudheersai@google.com +yamasani@google.com diff --git a/apex/blobstore/TEST_MAPPING b/apex/blobstore/TEST_MAPPING new file mode 100644 index 000000000000..6d3c0d73f77f --- /dev/null +++ b/apex/blobstore/TEST_MAPPING @@ -0,0 +1,18 @@ +{ + "presubmit": [ + { + "name": "CtsBlobStoreTestCases" + }, + { + "name": "CtsBlobStoreHostTestCases" + }, + { + "name": "FrameworksMockingServicesTests", + "options": [ + { + "include-filter": "com.android.server.blob" + } + ] + } + ] +} diff --git a/apex/blobstore/framework/Android.bp b/apex/blobstore/framework/Android.bp new file mode 100644 index 000000000000..24693511117c --- /dev/null +++ b/apex/blobstore/framework/Android.bp @@ -0,0 +1,40 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +filegroup { + name: "framework-blobstore-sources", + srcs: [ + "java/**/*.java", + "java/**/*.aidl" + ], + path: "java", +} + +java_library { + name: "blobstore-framework", + installable: false, + compile_dex: true, + sdk_version: "core_platform", + srcs: [ + ":framework-blobstore-sources", + ], + aidl: { + export_include_dirs: [ + "java", + ], + }, + libs: [ + "framework-minus-apex", + ], +} diff --git a/apex/blobstore/framework/java/android/app/blob/BlobHandle.aidl b/apex/blobstore/framework/java/android/app/blob/BlobHandle.aidl new file mode 100644 index 000000000000..02d0740a2ce0 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/BlobHandle.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.blob; + +/** {@hide} */ +parcelable BlobHandle;
\ No newline at end of file diff --git a/apex/blobstore/framework/java/android/app/blob/BlobHandle.java b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java new file mode 100644 index 000000000000..113f8fe9e248 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java @@ -0,0 +1,305 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.blob; + +import static android.app.blob.XmlTags.ATTR_ALGO; +import static android.app.blob.XmlTags.ATTR_DIGEST; +import static android.app.blob.XmlTags.ATTR_EXPIRY_TIME; +import static android.app.blob.XmlTags.ATTR_LABEL; +import static android.app.blob.XmlTags.ATTR_TAG; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Base64; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.Preconditions; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +/** + * An identifier to represent a blob. + */ +// TODO: use datagen tool? +public final class BlobHandle implements Parcelable { + /** @hide */ + public static final String ALGO_SHA_256 = "SHA-256"; + + private static final String[] SUPPORTED_ALGOS = { + ALGO_SHA_256 + }; + + private static final int LIMIT_BLOB_TAG_LENGTH = 128; // characters + private static final int LIMIT_BLOB_LABEL_LENGTH = 100; // characters + + /** + * Cyrptographically secure hash algorithm used to generate hash of the blob this handle is + * representing. + * + * @hide + */ + @NonNull public final String algorithm; + + /** + * Hash of the blob this handle is representing using {@link #algorithm}. + * + * @hide + */ + @NonNull public final byte[] digest; + + /** + * Label of the blob that can be surfaced to the user. + * @hide + */ + @NonNull public final CharSequence label; + + /** + * Time in milliseconds after which the blob should be invalidated and not + * allowed to be accessed by any other app, in {@link System#currentTimeMillis()} timebase. + * + * @hide + */ + @CurrentTimeMillisLong public final long expiryTimeMillis; + + /** + * An opaque {@link String} associated with the blob. + * + * @hide + */ + @NonNull public final String tag; + + private BlobHandle(String algorithm, byte[] digest, CharSequence label, long expiryTimeMillis, + String tag) { + this.algorithm = algorithm; + this.digest = digest; + this.label = label; + this.expiryTimeMillis = expiryTimeMillis; + this.tag = tag; + } + + private BlobHandle(Parcel in) { + this.algorithm = in.readString(); + this.digest = in.createByteArray(); + this.label = in.readCharSequence(); + this.expiryTimeMillis = in.readLong(); + this.tag = in.readString(); + } + + /** @hide */ + public static @NonNull BlobHandle create(@NonNull String algorithm, @NonNull byte[] digest, + @NonNull CharSequence label, @CurrentTimeMillisLong long expiryTimeMillis, + @NonNull String tag) { + final BlobHandle handle = new BlobHandle(algorithm, digest, label, expiryTimeMillis, tag); + handle.assertIsValid(); + return handle; + } + + /** + * Create a new blob identifier. + * + * <p> For two objects of {@link BlobHandle} to be considered equal, the following arguments + * must be equal: + * <ul> + * <li> {@code digest} + * <li> {@code label} + * <li> {@code expiryTimeMillis} + * <li> {@code tag} + * </ul> + * + * @param digest the SHA-256 hash of the blob this is representing. + * @param label a label indicating what the blob is, that can be surfaced to the user. + * The length of the label cannot be more than 100 characters. It is recommended + * to keep this brief. This may be truncated and ellipsized if it is too long + * to be displayed to the user. + * @param expiryTimeMillis the time in secs after which the blob should be invalidated and not + * allowed to be accessed by any other app, + * in {@link System#currentTimeMillis()} timebase or {@code 0} to + * indicate that there is no expiry time associated with this blob. + * @param tag an opaque {@link String} associated with the blob. The length of the tag + * cannot be more than 128 characters. + * + * @return a new instance of {@link BlobHandle} object. + */ + public static @NonNull BlobHandle createWithSha256(@NonNull byte[] digest, + @NonNull CharSequence label, @CurrentTimeMillisLong long expiryTimeMillis, + @NonNull String tag) { + return create(ALGO_SHA_256, digest, label, expiryTimeMillis, tag); + } + + /** + * Returns the SHA-256 hash of the blob that this object is representing. + * + * @see #createWithSha256(byte[], CharSequence, long, String) + */ + public @NonNull byte[] getSha256Digest() { + return digest; + } + + /** + * Returns the label associated with the blob that this object is representing. + * + * @see #createWithSha256(byte[], CharSequence, long, String) + */ + public @NonNull CharSequence getLabel() { + return label; + } + + /** + * Returns the expiry time in milliseconds of the blob that this object is representing, in + * {@link System#currentTimeMillis()} timebase. + * + * @see #createWithSha256(byte[], CharSequence, long, String) + */ + public @CurrentTimeMillisLong long getExpiryTimeMillis() { + return expiryTimeMillis; + } + + /** + * Returns the opaque {@link String} associated with the blob this object is representing. + * + * @see #createWithSha256(byte[], CharSequence, long, String) + */ + public @NonNull String getTag() { + return tag; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(algorithm); + dest.writeByteArray(digest); + dest.writeCharSequence(label); + dest.writeLong(expiryTimeMillis); + dest.writeString(tag); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || !(obj instanceof BlobHandle)) { + return false; + } + final BlobHandle other = (BlobHandle) obj; + return this.algorithm.equals(other.algorithm) + && Arrays.equals(this.digest, other.digest) + && this.label.toString().equals(other.label.toString()) + && this.expiryTimeMillis == other.expiryTimeMillis + && this.tag.equals(other.tag); + } + + @Override + public int hashCode() { + return Objects.hash(algorithm, Arrays.hashCode(digest), label, expiryTimeMillis, tag); + } + + /** @hide */ + public void dump(IndentingPrintWriter fout, boolean dumpFull) { + if (dumpFull) { + fout.println("algo: " + algorithm); + fout.println("digest: " + (dumpFull ? encodeDigest(digest) : safeDigest(digest))); + fout.println("label: " + label); + fout.println("expiryMs: " + expiryTimeMillis); + fout.println("tag: " + tag); + } else { + fout.println(toString()); + } + } + + /** @hide */ + public void assertIsValid() { + Preconditions.checkArgumentIsSupported(SUPPORTED_ALGOS, algorithm); + Preconditions.checkByteArrayNotEmpty(digest, "digest"); + Preconditions.checkStringNotEmpty(label, "label must not be null"); + Preconditions.checkArgument(label.length() <= LIMIT_BLOB_LABEL_LENGTH, "label too long"); + Preconditions.checkArgumentNonnegative(expiryTimeMillis, + "expiryTimeMillis must not be negative"); + Preconditions.checkStringNotEmpty(tag, "tag must not be null"); + Preconditions.checkArgument(tag.length() <= LIMIT_BLOB_TAG_LENGTH, "tag too long"); + } + + @Override + public String toString() { + return "BlobHandle {" + + "algo:" + algorithm + "," + + "digest:" + safeDigest(digest) + "," + + "label:" + label + "," + + "expiryMs:" + expiryTimeMillis + "," + + "tag:" + tag + + "}"; + } + + /** @hide */ + public static String safeDigest(@NonNull byte[] digest) { + final String digestStr = encodeDigest(digest); + return digestStr.substring(0, 2) + ".." + digestStr.substring(digestStr.length() - 2); + } + + private static String encodeDigest(@NonNull byte[] digest) { + return Base64.encodeToString(digest, Base64.NO_WRAP); + } + + /** @hide */ + public boolean isExpired() { + return expiryTimeMillis != 0 && expiryTimeMillis < System.currentTimeMillis(); + } + + public static final @NonNull Creator<BlobHandle> CREATOR = new Creator<BlobHandle>() { + @Override + public @NonNull BlobHandle createFromParcel(@NonNull Parcel source) { + return new BlobHandle(source); + } + + @Override + public @NonNull BlobHandle[] newArray(int size) { + return new BlobHandle[size]; + } + }; + + /** @hide */ + public void writeToXml(@NonNull XmlSerializer out) throws IOException { + XmlUtils.writeStringAttribute(out, ATTR_ALGO, algorithm); + XmlUtils.writeByteArrayAttribute(out, ATTR_DIGEST, digest); + XmlUtils.writeStringAttribute(out, ATTR_LABEL, label); + XmlUtils.writeLongAttribute(out, ATTR_EXPIRY_TIME, expiryTimeMillis); + XmlUtils.writeStringAttribute(out, ATTR_TAG, tag); + } + + /** @hide */ + @NonNull + public static BlobHandle createFromXml(@NonNull XmlPullParser in) throws IOException { + final String algo = XmlUtils.readStringAttribute(in, ATTR_ALGO); + final byte[] digest = XmlUtils.readByteArrayAttribute(in, ATTR_DIGEST); + final CharSequence label = XmlUtils.readStringAttribute(in, ATTR_LABEL); + final long expiryTimeMs = XmlUtils.readLongAttribute(in, ATTR_EXPIRY_TIME); + final String tag = XmlUtils.readStringAttribute(in, ATTR_TAG); + + return BlobHandle.create(algo, digest, label, expiryTimeMs, tag); + } +} diff --git a/apex/blobstore/framework/java/android/app/blob/BlobInfo.aidl b/apex/blobstore/framework/java/android/app/blob/BlobInfo.aidl new file mode 100644 index 000000000000..25497738f685 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/BlobInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.blob; + +/** {@hide} */ +parcelable BlobInfo;
\ No newline at end of file diff --git a/apex/blobstore/framework/java/android/app/blob/BlobInfo.java b/apex/blobstore/framework/java/android/app/blob/BlobInfo.java new file mode 100644 index 000000000000..ba92d95b483e --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/BlobInfo.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.blob; + +import static android.text.format.Formatter.FLAG_IEC_UNITS; + +import android.annotation.NonNull; +import android.app.AppGlobals; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.format.Formatter; + +import java.util.Collections; +import java.util.List; + +/** + * Class to provide information about a shared blob. + * + * @hide + */ +public final class BlobInfo implements Parcelable { + private final long mId; + private final long mExpiryTimeMs; + private final CharSequence mLabel; + private final long mSizeBytes; + private final List<LeaseInfo> mLeaseInfos; + + public BlobInfo(long id, long expiryTimeMs, CharSequence label, long sizeBytes, + List<LeaseInfo> leaseInfos) { + mId = id; + mExpiryTimeMs = expiryTimeMs; + mLabel = label; + mSizeBytes = sizeBytes; + mLeaseInfos = leaseInfos; + } + + private BlobInfo(Parcel in) { + mId = in.readLong(); + mExpiryTimeMs = in.readLong(); + mLabel = in.readCharSequence(); + mSizeBytes = in.readLong(); + mLeaseInfos = in.readArrayList(null /* classloader */); + } + + public long getId() { + return mId; + } + + public long getExpiryTimeMs() { + return mExpiryTimeMs; + } + + public CharSequence getLabel() { + return mLabel; + } + + public long getSizeBytes() { + return mSizeBytes; + } + + public List<LeaseInfo> getLeases() { + return Collections.unmodifiableList(mLeaseInfos); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeLong(mId); + dest.writeLong(mExpiryTimeMs); + dest.writeCharSequence(mLabel); + dest.writeLong(mSizeBytes); + dest.writeList(mLeaseInfos); + } + + @Override + public String toString() { + return toShortString(); + } + + private String toShortString() { + return "BlobInfo {" + + "id: " + mId + "," + + "expiryMs: " + mExpiryTimeMs + "," + + "label: " + mLabel + "," + + "size: " + formatBlobSize(mSizeBytes) + "," + + "leases: " + LeaseInfo.toShortString(mLeaseInfos) + "," + + "}"; + } + + private static String formatBlobSize(long sizeBytes) { + return Formatter.formatFileSize(AppGlobals.getInitialApplication(), + sizeBytes, FLAG_IEC_UNITS); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Creator<BlobInfo> CREATOR = new Creator<BlobInfo>() { + @Override + @NonNull + public BlobInfo createFromParcel(Parcel source) { + return new BlobInfo(source); + } + + @Override + @NonNull + public BlobInfo[] newArray(int size) { + return new BlobInfo[size]; + } + }; +} diff --git a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java new file mode 100644 index 000000000000..39f7526560a9 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java @@ -0,0 +1,943 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.blob; + +import android.annotation.BytesLong; +import android.annotation.CallbackExecutor; +import android.annotation.CurrentTimeMillisLong; +import android.annotation.IdRes; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemService; +import android.annotation.TestApi; +import android.content.Context; +import android.os.LimitExceededException; +import android.os.ParcelFileDescriptor; +import android.os.ParcelableException; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.os.UserHandle; + +import com.android.internal.util.function.pooled.PooledLambda; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +/** + * This class provides access to the blob store managed by the system. + * + * <p> Apps can publish and access a data blob using a {@link BlobHandle} object which can + * be created with {@link BlobHandle#createWithSha256(byte[], CharSequence, long, String)}. + * This {@link BlobHandle} object encapsulates the following pieces of information used for + * identifying the blobs: + * <ul> + * <li> {@link BlobHandle#getSha256Digest()} + * <li> {@link BlobHandle#getLabel()} + * <li> {@link BlobHandle#getExpiryTimeMillis()} + * <li> {@link BlobHandle#getTag()} + * </ul> + * For two {@link BlobHandle} objects to be considered identical, all these pieces of information + * must be equal. + * + * <p> For contributing a new data blob, an app needs to create a session using + * {@link BlobStoreManager#createSession(BlobHandle)} and then open this session for writing using + * {@link BlobStoreManager#openSession(long)}. + * + * <p> The following code snippet shows how to create and open a session for writing: + * <pre class="prettyprint"> + * final long sessionId = blobStoreManager.createSession(blobHandle); + * try (BlobStoreManager.Session session = blobStoreManager.openSession(sessionId)) { + * try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream( + * session.openWrite(offsetBytes, lengthBytes))) { + * writeData(out); + * } + * } + * </pre> + * + * <p> If all the data could not be written in a single attempt, apps can close this session + * and re-open it again using the session id obtained via + * {@link BlobStoreManager#createSession(BlobHandle)}. Note that the session data is persisted + * and can be re-opened for completing the data contribution, even across device reboots. + * + * <p> After the data is written to the session, it can be committed using + * {@link Session#commit(Executor, Consumer)}. Until the session is committed, data written + * to the session will not be shared with any app. + * + * <p class="note"> Once a session is committed using {@link Session#commit(Executor, Consumer)}, + * any data written as part of this session is sealed and cannot be modified anymore. + * + * <p> Before committing the session, apps can indicate which apps are allowed to access the + * contributed data using one or more of the following access modes: + * <ul> + * <li> {@link Session#allowPackageAccess(String, byte[])} which will allow whitelisting + * specific packages to access the blobs. + * <li> {@link Session#allowSameSignatureAccess()} which will allow only apps which are signed + * with the same certificate as the app which contributed the blob to access it. + * <li> {@link Session#allowPublicAccess()} which will allow any app on the device to access + * the blob. + * </ul> + * + * <p> The following code snippet shows how to specify the access mode and commit the session: + * <pre class="prettyprint"> + * try (BlobStoreManager.Session session = blobStoreManager.openSession(sessionId)) { + * try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream( + * session.openWrite(offsetBytes, lengthBytes))) { + * writeData(out); + * } + * session.allowSameSignatureAccess(); + * session.allowPackageAccess(packageName, certificate); + * session.commit(executor, callback); + * } + * </pre> + * + * <p> Apps that satisfy at least one of the access mode constraints specified by the publisher + * of the data blob will be able to access it. + * + * <p> A data blob published without specifying any of + * these access modes will be considered private and only the app that contributed the data + * blob will be allowed to access it. This is still useful for overall device system health as + * the System can try to keep one copy of data blob on disk when multiple apps contribute the + * same data. + * + * <p class="note"> It is strongly recommended that apps use one of + * {@link Session#allowPackageAccess(String, byte[])} or {@link Session#allowSameSignatureAccess()} + * when they know, ahead of time, the set of apps they would like to share the blobs with. + * {@link Session#allowPublicAccess()} is meant for publicly available data committed from + * libraries and SDKs. + * + * <p> Once a data blob is committed with {@link Session#commit(Executor, Consumer)}, it + * can be accessed using {@link BlobStoreManager#openBlob(BlobHandle)}, assuming the caller + * satisfies constraints of any of the access modes associated with that data blob. An app may + * acquire a lease on a blob with {@link BlobStoreManager#acquireLease(BlobHandle, int)} and + * release the lease with {@link BlobStoreManager#releaseLease(BlobHandle)}. A blob will not be + * deleted from the system while there is at least one app leasing it. + * + * <p> The following code snippet shows how to access the data blob: + * <pre class="prettyprint"> + * try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream( + * blobStoreManager.openBlob(blobHandle)) { + * useData(in); + * } + * </pre> + */ +@SystemService(Context.BLOB_STORE_SERVICE) +public class BlobStoreManager { + /** @hide */ + public static final int COMMIT_RESULT_SUCCESS = 0; + /** @hide */ + public static final int COMMIT_RESULT_ERROR = 1; + + /** @hide */ + public static final int INVALID_RES_ID = -1; + + private final Context mContext; + private final IBlobStoreManager mService; + + /** @hide */ + public BlobStoreManager(@NonNull Context context, @NonNull IBlobStoreManager service) { + mContext = context; + mService = service; + } + + /** + * Create a new session using the given {@link BlobHandle}, returning a unique id + * that represents the session. Once created, the session can be opened + * multiple times across multiple device boots. + * + * <p> The system may automatically destroy sessions that have not been + * finalized (either committed or abandoned) within a reasonable period of + * time, typically about a week. + * + * <p> If an app is planning to acquire a lease on this data (using + * {@link #acquireLease(BlobHandle, int)} or one of it's other variants) after committing + * this data (using {@link Session#commit(Executor, Consumer)}), it is recommended that + * the app checks the remaining quota for acquiring a lease first using + * {@link #getRemainingLeaseQuotaBytes()} and can skip contributing this data if needed. + * + * @param blobHandle the {@link BlobHandle} identifier for which a new session + * needs to be created. + * @return positive, non-zero unique id that represents the created session. + * This id remains consistent across device reboots until the + * session is finalized. IDs are not reused during a given boot. + * + * @throws IOException when there is an I/O error while creating the session. + * @throws SecurityException when the caller is not allowed to create a session, such + * as when called from an Instant app. + * @throws IllegalArgumentException when {@code blobHandle} is invalid. + * @throws LimitExceededException when a new session could not be created, such as when the + * caller is trying to create too many sessions. + */ + public @IntRange(from = 1) long createSession(@NonNull BlobHandle blobHandle) + throws IOException { + try { + return mService.createSession(blobHandle, mContext.getOpPackageName()); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + e.maybeRethrow(LimitExceededException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Open an existing session to actively perform work. + * + * @param sessionId a unique id obtained via {@link #createSession(BlobHandle)} that + * represents a particular session. + * @return the {@link Session} object corresponding to the {@code sessionId}. + * + * @throws IOException when there is an I/O error while opening the session. + * @throws SecurityException when the caller does not own the session, or + * the session does not exist or is invalid. + */ + public @NonNull Session openSession(@IntRange(from = 1) long sessionId) throws IOException { + try { + return new Session(mService.openSession(sessionId, mContext.getOpPackageName())); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Abandons an existing session and deletes any data that was written to that session so far. + * + * @param sessionId a unique id obtained via {@link #createSession(BlobHandle)} that + * represents a particular session. + * + * @throws IOException when there is an I/O error while deleting the session. + * @throws SecurityException when the caller does not own the session, or + * the session does not exist or is invalid. + */ + public void abandonSession(@IntRange(from = 1) long sessionId) throws IOException { + try { + mService.abandonSession(sessionId, mContext.getOpPackageName()); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Opens an existing blob for reading from the blob store managed by the system. + * + * @param blobHandle the {@link BlobHandle} representing the blob that the caller + * wants to access. + * @return a {@link ParcelFileDescriptor} that can be used to read the blob content. + * + * @throws IOException when there is an I/O while opening the blob for read. + * @throws IllegalArgumentException when {@code blobHandle} is invalid. + * @throws SecurityException when the blob represented by the {@code blobHandle} does not + * exist or the caller does not have access to it. + */ + public @NonNull ParcelFileDescriptor openBlob(@NonNull BlobHandle blobHandle) + throws IOException { + try { + return mService.openBlob(blobHandle, mContext.getOpPackageName()); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the + * system that the caller wants the blob to be kept around. + * + * <p> Any active leases will be automatically released when the blob's expiry time + * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed. + * + * <p> This lease information is persisted and calling this more than once will result in + * latest lease overriding any previous lease. + * + * <p> When an app acquires a lease on a blob, the System will try to keep this + * blob around but note that it can still be deleted if it was requested by the user. + * + * <p> In case the resource name for the {@code descriptionResId} is modified as part of + * an app update, apps should re-acquire the lease with the new resource id. + * + * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to + * acquire a lease for. + * @param descriptionResId the resource id for a short description string that can be surfaced + * to the user explaining what the blob is used for. + * @param leaseExpiryTimeMillis the time in milliseconds after which the lease can be + * automatically released, in {@link System#currentTimeMillis()} + * timebase. If its value is {@code 0}, then the behavior of this + * API is identical to {@link #acquireLease(BlobHandle, int)} + * where clients have to explicitly call + * {@link #releaseLease(BlobHandle)} when they don't + * need the blob anymore. + * + * @throws IOException when there is an I/O error while acquiring a lease to the blob. + * @throws SecurityException when the blob represented by the {@code blobHandle} does not + * exist or the caller does not have access to it. + * @throws IllegalArgumentException when {@code blobHandle} is invalid or + * if the {@code leaseExpiryTimeMillis} is greater than the + * {@link BlobHandle#getExpiryTimeMillis()}. + * @throws LimitExceededException when a lease could not be acquired, such as when the + * caller is trying to acquire too many leases or acquire + * leases on too much data. Apps can avoid this by checking + * the remaining quota using + * {@link #getRemainingLeaseQuotaBytes()} before trying to + * acquire a lease. + * + * @see #acquireLease(BlobHandle, int) + * @see #acquireLease(BlobHandle, CharSequence) + */ + public void acquireLease(@NonNull BlobHandle blobHandle, @IdRes int descriptionResId, + @CurrentTimeMillisLong long leaseExpiryTimeMillis) throws IOException { + try { + mService.acquireLease(blobHandle, descriptionResId, null, leaseExpiryTimeMillis, + mContext.getOpPackageName()); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + e.maybeRethrow(LimitExceededException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the + * system that the caller wants the blob to be kept around. + * + * <p> This is a variant of {@link #acquireLease(BlobHandle, int, long)} taking a + * {@link CharSequence} for {@code description}. It is highly recommended that callers only + * use this when a valid resource ID for {@code description} could not be provided. Otherwise, + * apps should prefer using {@link #acquireLease(BlobHandle, int)} which will allow + * {@code description} to be localized. + * + * <p> Any active leases will be automatically released when the blob's expiry time + * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed. + * + * <p> This lease information is persisted and calling this more than once will result in + * latest lease overriding any previous lease. + * + * <p> When an app acquires a lease on a blob, the System will try to keep this + * blob around but note that it can still be deleted if it was requested by the user. + * + * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to + * acquire a lease for. + * @param description a short description string that can be surfaced + * to the user explaining what the blob is used for. It is recommended to + * keep this description brief. This may be truncated and ellipsized + * if it is too long to be displayed to the user. + * @param leaseExpiryTimeMillis the time in milliseconds after which the lease can be + * automatically released, in {@link System#currentTimeMillis()} + * timebase. If its value is {@code 0}, then the behavior of this + * API is identical to {@link #acquireLease(BlobHandle, int)} + * where clients have to explicitly call + * {@link #releaseLease(BlobHandle)} when they don't + * need the blob anymore. + * + * @throws IOException when there is an I/O error while acquiring a lease to the blob. + * @throws SecurityException when the blob represented by the {@code blobHandle} does not + * exist or the caller does not have access to it. + * @throws IllegalArgumentException when {@code blobHandle} is invalid or + * if the {@code leaseExpiryTimeMillis} is greater than the + * {@link BlobHandle#getExpiryTimeMillis()}. + * @throws LimitExceededException when a lease could not be acquired, such as when the + * caller is trying to acquire too many leases or acquire + * leases on too much data. Apps can avoid this by checking + * the remaining quota using + * {@link #getRemainingLeaseQuotaBytes()} before trying to + * acquire a lease. + * + * @see #acquireLease(BlobHandle, int, long) + * @see #acquireLease(BlobHandle, CharSequence) + */ + public void acquireLease(@NonNull BlobHandle blobHandle, @NonNull CharSequence description, + @CurrentTimeMillisLong long leaseExpiryTimeMillis) throws IOException { + try { + mService.acquireLease(blobHandle, INVALID_RES_ID, description, leaseExpiryTimeMillis, + mContext.getOpPackageName()); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + e.maybeRethrow(LimitExceededException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the + * system that the caller wants the blob to be kept around. + * + * <p> This is similar to {@link #acquireLease(BlobHandle, int, long)} except clients don't + * have to specify the lease expiry time upfront using this API and need to explicitly + * release the lease using {@link #releaseLease(BlobHandle)} when they no longer like to keep + * a blob around. + * + * <p> Any active leases will be automatically released when the blob's expiry time + * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed. + * + * <p> This lease information is persisted and calling this more than once will result in + * latest lease overriding any previous lease. + * + * <p> When an app acquires a lease on a blob, the System will try to keep this + * blob around but note that it can still be deleted if it was requested by the user. + * + * <p> In case the resource name for the {@code descriptionResId} is modified as part of + * an app update, apps should re-acquire the lease with the new resource id. + * + * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to + * acquire a lease for. + * @param descriptionResId the resource id for a short description string that can be surfaced + * to the user explaining what the blob is used for. + * + * @throws IOException when there is an I/O error while acquiring a lease to the blob. + * @throws SecurityException when the blob represented by the {@code blobHandle} does not + * exist or the caller does not have access to it. + * @throws IllegalArgumentException when {@code blobHandle} is invalid. + * @throws LimitExceededException when a lease could not be acquired, such as when the + * caller is trying to acquire too many leases or acquire + * leases on too much data. Apps can avoid this by checking + * the remaining quota using + * {@link #getRemainingLeaseQuotaBytes()} before trying to + * acquire a lease. + * + * @see #acquireLease(BlobHandle, int, long) + * @see #acquireLease(BlobHandle, CharSequence, long) + */ + public void acquireLease(@NonNull BlobHandle blobHandle, @IdRes int descriptionResId) + throws IOException { + acquireLease(blobHandle, descriptionResId, 0); + } + + /** + * Acquire a lease to the blob represented by {@code blobHandle}. This lease indicates to the + * system that the caller wants the blob to be kept around. + * + * <p> This is a variant of {@link #acquireLease(BlobHandle, int)} taking a {@link CharSequence} + * for {@code description}. It is highly recommended that callers only use this when a valid + * resource ID for {@code description} could not be provided. Otherwise, apps should prefer + * using {@link #acquireLease(BlobHandle, int)} which will allow {@code description} to be + * localized. + * + * <p> This is similar to {@link #acquireLease(BlobHandle, CharSequence, long)} except clients + * don't have to specify the lease expiry time upfront using this API and need to explicitly + * release the lease using {@link #releaseLease(BlobHandle)} when they no longer like to keep + * a blob around. + * + * <p> Any active leases will be automatically released when the blob's expiry time + * ({@link BlobHandle#getExpiryTimeMillis()}) is elapsed. + * + * <p> This lease information is persisted and calling this more than once will result in + * latest lease overriding any previous lease. + * + * <p> When an app acquires a lease on a blob, the System will try to keep this + * blob around but note that it can still be deleted if it was requested by the user. + * + * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to + * acquire a lease for. + * @param description a short description string that can be surfaced + * to the user explaining what the blob is used for. It is recommended to + * keep this description brief. This may be truncated and + * ellipsized if it is too long to be displayed to the user. + * + * @throws IOException when there is an I/O error while acquiring a lease to the blob. + * @throws SecurityException when the blob represented by the {@code blobHandle} does not + * exist or the caller does not have access to it. + * @throws IllegalArgumentException when {@code blobHandle} is invalid. + * @throws LimitExceededException when a lease could not be acquired, such as when the + * caller is trying to acquire too many leases or acquire + * leases on too much data. Apps can avoid this by checking + * the remaining quota using + * {@link #getRemainingLeaseQuotaBytes()} before trying to + * acquire a lease. + * + * @see #acquireLease(BlobHandle, int) + * @see #acquireLease(BlobHandle, CharSequence, long) + */ + public void acquireLease(@NonNull BlobHandle blobHandle, @NonNull CharSequence description) + throws IOException { + acquireLease(blobHandle, description, 0); + } + + /** + * Release any active lease to the blob represented by {@code blobHandle} which is + * currently held by the caller. + * + * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to + * release the lease for. + * + * @throws IOException when there is an I/O error while releasing the release to the blob. + * @throws SecurityException when the blob represented by the {@code blobHandle} does not + * exist or the caller does not have access to it. + * @throws IllegalArgumentException when {@code blobHandle} is invalid. + */ + public void releaseLease(@NonNull BlobHandle blobHandle) throws IOException { + try { + mService.releaseLease(blobHandle, mContext.getOpPackageName()); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the remaining quota size for acquiring a lease (in bytes) which indicates the + * remaining amount of data that an app can acquire a lease on before the System starts + * rejecting lease requests. + * + * If an app wants to acquire a lease on a blob but the remaining quota size is not sufficient, + * then it can try releasing leases on any older blobs which are not needed anymore. + * + * @return the remaining quota size for acquiring a lease. + */ + public @IntRange(from = 0) long getRemainingLeaseQuotaBytes() { + try { + return mService.getRemainingLeaseQuotaBytes(mContext.getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Wait until any pending tasks (like persisting data to disk) have finished. + * + * @hide + */ + @TestApi + public void waitForIdle(long timeoutMillis) throws InterruptedException, TimeoutException { + try { + final CountDownLatch countDownLatch = new CountDownLatch(1); + mService.waitForIdle(new RemoteCallback((result) -> countDownLatch.countDown())); + if (!countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Timed out waiting for service to become idle"); + } + } catch (ParcelableException e) { + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide */ + @NonNull + public List<BlobInfo> queryBlobsForUser(@NonNull UserHandle user) throws IOException { + try { + return mService.queryBlobsForUser(user.getIdentifier()); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide */ + public void deleteBlob(@NonNull BlobInfo blobInfo) throws IOException { + try { + mService.deleteBlob(blobInfo.getId()); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the {@link BlobHandle BlobHandles} corresponding to the data blobs that + * the calling app currently has a lease on. + * + * @return a list of {@link BlobHandle BlobHandles} that the caller has a lease on. + */ + @NonNull + public List<BlobHandle> getLeasedBlobs() throws IOException { + try { + return mService.getLeasedBlobs(mContext.getOpPackageName()); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return {@link LeaseInfo} representing a lease acquired using + * {@link #acquireLease(BlobHandle, int)} or one of it's other variants, + * or {@code null} if there is no lease acquired. + * + * @throws SecurityException when the blob represented by the {@code blobHandle} does not + * exist or the caller does not have access to it. + * @throws IllegalArgumentException when {@code blobHandle} is invalid. + * + * @hide + */ + @TestApi + @Nullable + public LeaseInfo getLeaseInfo(@NonNull BlobHandle blobHandle) throws IOException { + try { + return mService.getLeaseInfo(blobHandle, mContext.getOpPackageName()); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Represents an ongoing session of a blob's contribution to the blob store managed by the + * system. + * + * <p> Clients that want to contribute a blob need to first create a {@link Session} using + * {@link #createSession(BlobHandle)} and once the session is created, clients can open and + * close this session multiple times using {@link #openSession(long)} and + * {@link Session#close()} before committing it using + * {@link Session#commit(Executor, Consumer)}, at which point system will take + * ownership of the blob and the client can no longer make any modifications to the blob's + * content. + */ + public static class Session implements Closeable { + private final IBlobStoreSession mSession; + + private Session(@NonNull IBlobStoreSession session) { + mSession = session; + } + + /** + * Opens a file descriptor to write a blob into the session. + * + * <p> The returned file descriptor will start writing data at the requested offset + * in the underlying file, which can be used to resume a partially + * written file. If a valid file length is specified, the system will + * preallocate the underlying disk space to optimize placement on disk. + * It is strongly recommended to provide a valid file length when known. + * + * @param offsetBytes offset into the file to begin writing at, or 0 to + * start at the beginning of the file. + * @param lengthBytes total size of the file being written, used to + * preallocate the underlying disk space, or -1 if unknown. + * The system may clear various caches as needed to allocate + * this space. + * + * @return a {@link ParcelFileDescriptor} for writing to the blob file. + * + * @throws IOException when there is an I/O error while opening the file to write. + * @throws SecurityException when the caller is not the owner of the session. + * @throws IllegalStateException when the caller tries to write to the file after it is + * abandoned (using {@link #abandon()}) + * or committed (using {@link #commit}) + * or closed (using {@link #close()}). + */ + public @NonNull ParcelFileDescriptor openWrite(@BytesLong long offsetBytes, + @BytesLong long lengthBytes) throws IOException { + try { + final ParcelFileDescriptor pfd = mSession.openWrite(offsetBytes, lengthBytes); + pfd.seekTo(offsetBytes); + return pfd; + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Opens a file descriptor to read the blob content already written into this session. + * + * @return a {@link ParcelFileDescriptor} for reading from the blob file. + * + * @throws IOException when there is an I/O error while opening the file to read. + * @throws SecurityException when the caller is not the owner of the session. + * @throws IllegalStateException when the caller tries to read the file after it is + * abandoned (using {@link #abandon()}) + * or closed (using {@link #close()}). + */ + public @NonNull ParcelFileDescriptor openRead() throws IOException { + try { + return mSession.openRead(); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Gets the size of the blob file that was written to the session so far. + * + * @return the size of the blob file so far. + * + * @throws IOException when there is an I/O error while opening the file to read. + * @throws SecurityException when the caller is not the owner of the session. + * @throws IllegalStateException when the caller tries to get the file size after it is + * abandoned (using {@link #abandon()}) + * or closed (using {@link #close()}). + */ + public @BytesLong long getSize() throws IOException { + try { + return mSession.getSize(); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Close this session. It can be re-opened for writing/reading if it has not been + * abandoned (using {@link #abandon}) or committed (using {@link #commit}). + * + * @throws IOException when there is an I/O error while closing the session. + * @throws SecurityException when the caller is not the owner of the session. + */ + public void close() throws IOException { + try { + mSession.close(); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Abandon this session and delete any data that was written to this session so far. + * + * @throws IOException when there is an I/O error while abandoning the session. + * @throws SecurityException when the caller is not the owner of the session. + * @throws IllegalStateException when the caller tries to abandon a session which was + * already finalized. + */ + public void abandon() throws IOException { + try { + mSession.abandon(); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Allow {@code packageName} with a particular signing certificate to access this blob + * data once it is committed using a {@link BlobHandle} representing the blob. + * + * <p> This needs to be called before committing the blob using + * {@link #commit(Executor, Consumer)}. + * + * @param packageName the name of the package which should be allowed to access the blob. + * @param certificate the input bytes representing a certificate of type + * {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}. + * + * @throws IOException when there is an I/O error while changing the access. + * @throws SecurityException when the caller is not the owner of the session. + * @throws IllegalStateException when the caller tries to change access for a blob which is + * already committed. + * @throws LimitExceededException when the caller tries to explicitly allow too + * many packages using this API. + */ + public void allowPackageAccess(@NonNull String packageName, @NonNull byte[] certificate) + throws IOException { + try { + mSession.allowPackageAccess(packageName, certificate); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + e.maybeRethrow(LimitExceededException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns {@code true} if access has been allowed for a {@code packageName} using either + * {@link #allowPackageAccess(String, byte[])}. + * Otherwise, {@code false}. + * + * @param packageName the name of the package to check the access for. + * @param certificate the input bytes representing a certificate of type + * {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}. + * + * @throws IOException when there is an I/O error while getting the access type. + * @throws IllegalStateException when the caller tries to get access type from a session + * which is closed or abandoned. + */ + public boolean isPackageAccessAllowed(@NonNull String packageName, + @NonNull byte[] certificate) throws IOException { + try { + return mSession.isPackageAccessAllowed(packageName, certificate); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Allow packages which are signed with the same certificate as the caller to access this + * blob data once it is committed using a {@link BlobHandle} representing the blob. + * + * <p> This needs to be called before committing the blob using + * {@link #commit(Executor, Consumer)}. + * + * @throws IOException when there is an I/O error while changing the access. + * @throws SecurityException when the caller is not the owner of the session. + * @throws IllegalStateException when the caller tries to change access for a blob which is + * already committed. + */ + public void allowSameSignatureAccess() throws IOException { + try { + mSession.allowSameSignatureAccess(); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns {@code true} if access has been allowed for packages signed with the same + * certificate as the caller by using {@link #allowSameSignatureAccess()}. + * Otherwise, {@code false}. + * + * @throws IOException when there is an I/O error while getting the access type. + * @throws IllegalStateException when the caller tries to get access type from a session + * which is closed or abandoned. + */ + public boolean isSameSignatureAccessAllowed() throws IOException { + try { + return mSession.isSameSignatureAccessAllowed(); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Allow any app on the device to access this blob data once it is committed using + * a {@link BlobHandle} representing the blob. + * + * <p><strong>Note:</strong> This is only meant to be used from libraries and SDKs where + * the apps which we want to allow access is not known ahead of time. + * If a blob is being committed to be shared with a particular set of apps, it is highly + * recommended to use {@link #allowPackageAccess(String, byte[])} instead. + * + * <p> This needs to be called before committing the blob using + * {@link #commit(Executor, Consumer)}. + * + * @throws IOException when there is an I/O error while changing the access. + * @throws SecurityException when the caller is not the owner of the session. + * @throws IllegalStateException when the caller tries to change access for a blob which is + * already committed. + */ + public void allowPublicAccess() throws IOException { + try { + mSession.allowPublicAccess(); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns {@code true} if public access has been allowed by using + * {@link #allowPublicAccess()}. Otherwise, {@code false}. + * + * @throws IOException when there is an I/O error while getting the access type. + * @throws IllegalStateException when the caller tries to get access type from a session + * which is closed or abandoned. + */ + public boolean isPublicAccessAllowed() throws IOException { + try { + return mSession.isPublicAccessAllowed(); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Commit the file that was written so far to this session to the blob store maintained by + * the system. + * + * <p> Once this method is called, the session is finalized and no additional + * mutations can be performed on the session. If the device reboots + * before the session has been finalized, you may commit the session again. + * + * <p> Note that this commit operation will fail if the hash of the data written so far + * to this session does not match with the one used for + * {@link BlobHandle#createWithSha256(byte[], CharSequence, long, String)} BlobHandle} + * associated with this session. + * + * <p> Committing the same data more than once will result in replacing the corresponding + * access mode (via calling one of {@link #allowPackageAccess(String, byte[])}, + * {@link #allowSameSignatureAccess()}, etc) with the latest one. + * + * @param executor the executor on which result callback will be invoked. + * @param resultCallback a callback to receive the commit result. when the result is + * {@code 0}, it indicates success. Otherwise, failure. + * + * @throws IOException when there is an I/O error while committing the session. + * @throws SecurityException when the caller is not the owner of the session. + * @throws IllegalArgumentException when the passed parameters are not valid. + * @throws IllegalStateException when the caller tries to commit a session which was + * already finalized. + */ + public void commit(@NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<Integer> resultCallback) throws IOException { + try { + mSession.commit(new IBlobCommitCallback.Stub() { + public void onResult(int result) { + executor.execute(PooledLambda.obtainRunnable( + Consumer::accept, resultCallback, result)); + } + }); + } catch (ParcelableException e) { + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } +} diff --git a/apex/blobstore/framework/java/android/app/blob/BlobStoreManagerFrameworkInitializer.java b/apex/blobstore/framework/java/android/app/blob/BlobStoreManagerFrameworkInitializer.java new file mode 100644 index 000000000000..56c419ab0591 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/BlobStoreManagerFrameworkInitializer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.blob; + +import android.app.SystemServiceRegistry; +import android.content.Context; + +/** + * This is where the BlobStoreManagerService wrapper is registered. + * + * @hide + */ +public class BlobStoreManagerFrameworkInitializer { + /** Register the BlobStoreManager wrapper class */ + public static void initialize() { + SystemServiceRegistry.registerContextAwareService( + Context.BLOB_STORE_SERVICE, BlobStoreManager.class, + (context, service) -> + new BlobStoreManager(context, IBlobStoreManager.Stub.asInterface(service))); + } +} diff --git a/apex/blobstore/framework/java/android/app/blob/IBlobCommitCallback.aidl b/apex/blobstore/framework/java/android/app/blob/IBlobCommitCallback.aidl new file mode 100644 index 000000000000..a9b30e20f5bd --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/IBlobCommitCallback.aidl @@ -0,0 +1,21 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.blob; + +/** {@hide} */ +oneway interface IBlobCommitCallback { + void onResult(int result); +}
\ No newline at end of file diff --git a/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl b/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl new file mode 100644 index 000000000000..39a9fb4bb1f4 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl @@ -0,0 +1,43 @@ +/** + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.blob; + +import android.app.blob.BlobHandle; +import android.app.blob.BlobInfo; +import android.app.blob.IBlobStoreSession; +import android.app.blob.LeaseInfo; +import android.os.RemoteCallback; + +/** {@hide} */ +interface IBlobStoreManager { + long createSession(in BlobHandle handle, in String packageName); + IBlobStoreSession openSession(long sessionId, in String packageName); + ParcelFileDescriptor openBlob(in BlobHandle handle, in String packageName); + void abandonSession(long sessionId, in String packageName); + + void acquireLease(in BlobHandle handle, int descriptionResId, in CharSequence description, + long leaseTimeoutMillis, in String packageName); + void releaseLease(in BlobHandle handle, in String packageName); + long getRemainingLeaseQuotaBytes(String packageName); + + void waitForIdle(in RemoteCallback callback); + + List<BlobInfo> queryBlobsForUser(int userId); + void deleteBlob(long blobId); + + List<BlobHandle> getLeasedBlobs(in String packageName); + LeaseInfo getLeaseInfo(in BlobHandle blobHandle, in String packageName); +}
\ No newline at end of file diff --git a/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl b/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl new file mode 100644 index 000000000000..4035b96938d9 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/IBlobStoreSession.aidl @@ -0,0 +1,39 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.blob; + +import android.app.blob.IBlobCommitCallback; +import android.os.ParcelFileDescriptor; + +/** {@hide} */ +interface IBlobStoreSession { + ParcelFileDescriptor openWrite(long offsetBytes, long lengthBytes); + ParcelFileDescriptor openRead(); + + void allowPackageAccess(in String packageName, in byte[] certificate); + void allowSameSignatureAccess(); + void allowPublicAccess(); + + boolean isPackageAccessAllowed(in String packageName, in byte[] certificate); + boolean isSameSignatureAccessAllowed(); + boolean isPublicAccessAllowed(); + + long getSize(); + void close(); + void abandon(); + + void commit(in IBlobCommitCallback callback); +}
\ No newline at end of file diff --git a/apex/blobstore/framework/java/android/app/blob/LeaseInfo.aidl b/apex/blobstore/framework/java/android/app/blob/LeaseInfo.aidl new file mode 100644 index 000000000000..908885731bb1 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/LeaseInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.blob; + +/** {@hide} */ +parcelable LeaseInfo;
\ No newline at end of file diff --git a/apex/blobstore/framework/java/android/app/blob/LeaseInfo.java b/apex/blobstore/framework/java/android/app/blob/LeaseInfo.java new file mode 100644 index 000000000000..fef50c9e8dba --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/LeaseInfo.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.blob; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.IdRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.List; + +/** + * Class to provide information about a lease (acquired using + * {@link BlobStoreManager#acquireLease(BlobHandle, int)} or one of it's variants) + * for a shared blob. + * + * @hide + */ +@TestApi +public final class LeaseInfo implements Parcelable { + private final String mPackageName; + private final long mExpiryTimeMillis; + private final int mDescriptionResId; + private final CharSequence mDescription; + + public LeaseInfo(@NonNull String packageName, @CurrentTimeMillisLong long expiryTimeMs, + @IdRes int descriptionResId, @Nullable CharSequence description) { + mPackageName = packageName; + mExpiryTimeMillis = expiryTimeMs; + mDescriptionResId = descriptionResId; + mDescription = description; + } + + private LeaseInfo(Parcel in) { + mPackageName = in.readString(); + mExpiryTimeMillis = in.readLong(); + mDescriptionResId = in.readInt(); + mDescription = in.readCharSequence(); + } + + @NonNull + public String getPackageName() { + return mPackageName; + } + + @CurrentTimeMillisLong + public long getExpiryTimeMillis() { + return mExpiryTimeMillis; + } + + @IdRes + public int getDescriptionResId() { + return mDescriptionResId; + } + + @Nullable + public CharSequence getDescription() { + return mDescription; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mPackageName); + dest.writeLong(mExpiryTimeMillis); + dest.writeInt(mDescriptionResId); + dest.writeCharSequence(mDescription); + } + + @Override + public String toString() { + return "LeaseInfo {" + + "package: " + mPackageName + "," + + "expiryMs: " + mExpiryTimeMillis + "," + + "descriptionResId: " + mDescriptionResId + "," + + "description: " + mDescription + "," + + "}"; + } + + private String toShortString() { + return mPackageName; + } + + static String toShortString(List<LeaseInfo> leaseInfos) { + final StringBuilder sb = new StringBuilder(); + sb.append("["); + for (int i = 0, size = leaseInfos.size(); i < size; ++i) { + sb.append(leaseInfos.get(i).toShortString()); + sb.append(","); + } + sb.append("]"); + return sb.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Creator<LeaseInfo> CREATOR = new Creator<LeaseInfo>() { + @Override + @NonNull + public LeaseInfo createFromParcel(Parcel source) { + return new LeaseInfo(source); + } + + @Override + @NonNull + public LeaseInfo[] newArray(int size) { + return new LeaseInfo[size]; + } + }; +} diff --git a/apex/blobstore/framework/java/android/app/blob/XmlTags.java b/apex/blobstore/framework/java/android/app/blob/XmlTags.java new file mode 100644 index 000000000000..656749d1a8c4 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/XmlTags.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.blob; + +/** @hide */ +public final class XmlTags { + public static final String ATTR_VERSION = "v"; + + public static final String TAG_SESSIONS = "ss"; + public static final String TAG_BLOBS = "bs"; + + // For BlobStoreSession + public static final String TAG_SESSION = "s"; + public static final String ATTR_ID = "id"; + public static final String ATTR_PACKAGE = "p"; + public static final String ATTR_UID = "u"; + public static final String ATTR_CREATION_TIME_MS = "crt"; + + // For BlobMetadata + public static final String TAG_BLOB = "b"; + public static final String ATTR_USER_ID = "us"; + + // For BlobAccessMode + public static final String TAG_ACCESS_MODE = "am"; + public static final String ATTR_TYPE = "t"; + public static final String TAG_WHITELISTED_PACKAGE = "wl"; + public static final String ATTR_CERTIFICATE = "ct"; + + // For BlobHandle + public static final String TAG_BLOB_HANDLE = "bh"; + public static final String ATTR_ALGO = "al"; + public static final String ATTR_DIGEST = "dg"; + public static final String ATTR_LABEL = "lbl"; + public static final String ATTR_EXPIRY_TIME = "ex"; + public static final String ATTR_TAG = "tg"; + + // For committer + public static final String TAG_COMMITTER = "c"; + public static final String ATTR_COMMIT_TIME_MS = "cmt"; + + // For leasee + public static final String TAG_LEASEE = "l"; + public static final String ATTR_DESCRIPTION_RES_NAME = "rn"; + public static final String ATTR_DESCRIPTION = "d"; +} diff --git a/apex/blobstore/service/Android.bp b/apex/blobstore/service/Android.bp new file mode 100644 index 000000000000..22b0cbe91e23 --- /dev/null +++ b/apex/blobstore/service/Android.bp @@ -0,0 +1,28 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +java_library { + name: "service-blobstore", + installable: true, + + srcs: [ + "java/**/*.java", + ], + + libs: [ + "framework", + "services.core", + "services.usage", + ], +} diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java b/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java new file mode 100644 index 000000000000..ba0fab6b4bc5 --- /dev/null +++ b/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java @@ -0,0 +1,221 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.blob; + +import static android.app.blob.XmlTags.ATTR_CERTIFICATE; +import static android.app.blob.XmlTags.ATTR_PACKAGE; +import static android.app.blob.XmlTags.ATTR_TYPE; +import static android.app.blob.XmlTags.TAG_WHITELISTED_PACKAGE; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.content.Context; +import android.content.pm.PackageManager; +import android.util.ArraySet; +import android.util.Base64; +import android.util.DebugUtils; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Objects; + +/** + * Class for representing how a blob can be shared. + * + * Note that this class is not thread-safe, callers need to take care of synchronizing access. + */ +class BlobAccessMode { + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { + ACCESS_TYPE_PRIVATE, + ACCESS_TYPE_PUBLIC, + ACCESS_TYPE_SAME_SIGNATURE, + ACCESS_TYPE_WHITELIST, + }) + @interface AccessType {} + public static final int ACCESS_TYPE_PRIVATE = 1 << 0; + public static final int ACCESS_TYPE_PUBLIC = 1 << 1; + public static final int ACCESS_TYPE_SAME_SIGNATURE = 1 << 2; + public static final int ACCESS_TYPE_WHITELIST = 1 << 3; + + private int mAccessType = ACCESS_TYPE_PRIVATE; + + private final ArraySet<PackageIdentifier> mWhitelistedPackages = new ArraySet<>(); + + void allow(BlobAccessMode other) { + if ((other.mAccessType & ACCESS_TYPE_WHITELIST) != 0) { + mWhitelistedPackages.addAll(other.mWhitelistedPackages); + } + mAccessType |= other.mAccessType; + } + + void allowPublicAccess() { + mAccessType |= ACCESS_TYPE_PUBLIC; + } + + void allowSameSignatureAccess() { + mAccessType |= ACCESS_TYPE_SAME_SIGNATURE; + } + + void allowPackageAccess(@NonNull String packageName, @NonNull byte[] certificate) { + mAccessType |= ACCESS_TYPE_WHITELIST; + mWhitelistedPackages.add(PackageIdentifier.create(packageName, certificate)); + } + + boolean isPublicAccessAllowed() { + return (mAccessType & ACCESS_TYPE_PUBLIC) != 0; + } + + boolean isSameSignatureAccessAllowed() { + return (mAccessType & ACCESS_TYPE_SAME_SIGNATURE) != 0; + } + + boolean isPackageAccessAllowed(@NonNull String packageName, @NonNull byte[] certificate) { + if ((mAccessType & ACCESS_TYPE_WHITELIST) == 0) { + return false; + } + return mWhitelistedPackages.contains(PackageIdentifier.create(packageName, certificate)); + } + + boolean isAccessAllowedForCaller(Context context, + @NonNull String callingPackage, @NonNull String committerPackage) { + if ((mAccessType & ACCESS_TYPE_PUBLIC) != 0) { + return true; + } + + final PackageManager pm = context.getPackageManager(); + if ((mAccessType & ACCESS_TYPE_SAME_SIGNATURE) != 0) { + if (pm.checkSignatures(committerPackage, callingPackage) + == PackageManager.SIGNATURE_MATCH) { + return true; + } + } + + if ((mAccessType & ACCESS_TYPE_WHITELIST) != 0) { + for (int i = 0; i < mWhitelistedPackages.size(); ++i) { + final PackageIdentifier packageIdentifier = mWhitelistedPackages.valueAt(i); + if (packageIdentifier.packageName.equals(callingPackage) + && pm.hasSigningCertificate(callingPackage, packageIdentifier.certificate, + PackageManager.CERT_INPUT_SHA256)) { + return true; + } + } + } + + return false; + } + + int getAccessType() { + return mAccessType; + } + + int getNumWhitelistedPackages() { + return mWhitelistedPackages.size(); + } + + void dump(IndentingPrintWriter fout) { + fout.println("accessType: " + DebugUtils.flagsToString( + BlobAccessMode.class, "ACCESS_TYPE_", mAccessType)); + fout.print("Whitelisted pkgs:"); + if (mWhitelistedPackages.isEmpty()) { + fout.println(" (Empty)"); + } else { + fout.increaseIndent(); + for (int i = 0, count = mWhitelistedPackages.size(); i < count; ++i) { + fout.println(mWhitelistedPackages.valueAt(i).toString()); + } + fout.decreaseIndent(); + } + } + + void writeToXml(@NonNull XmlSerializer out) throws IOException { + XmlUtils.writeIntAttribute(out, ATTR_TYPE, mAccessType); + for (int i = 0, count = mWhitelistedPackages.size(); i < count; ++i) { + out.startTag(null, TAG_WHITELISTED_PACKAGE); + final PackageIdentifier packageIdentifier = mWhitelistedPackages.valueAt(i); + XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, packageIdentifier.packageName); + XmlUtils.writeByteArrayAttribute(out, ATTR_CERTIFICATE, packageIdentifier.certificate); + out.endTag(null, TAG_WHITELISTED_PACKAGE); + } + } + + @NonNull + static BlobAccessMode createFromXml(@NonNull XmlPullParser in) + throws IOException, XmlPullParserException { + final BlobAccessMode blobAccessMode = new BlobAccessMode(); + + final int accessType = XmlUtils.readIntAttribute(in, ATTR_TYPE); + blobAccessMode.mAccessType = accessType; + + final int depth = in.getDepth(); + while (XmlUtils.nextElementWithin(in, depth)) { + if (TAG_WHITELISTED_PACKAGE.equals(in.getName())) { + final String packageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE); + final byte[] certificate = XmlUtils.readByteArrayAttribute(in, ATTR_CERTIFICATE); + blobAccessMode.allowPackageAccess(packageName, certificate); + } + } + return blobAccessMode; + } + + private static final class PackageIdentifier { + public final String packageName; + public final byte[] certificate; + + private PackageIdentifier(@NonNull String packageName, @NonNull byte[] certificate) { + this.packageName = packageName; + this.certificate = certificate; + } + + public static PackageIdentifier create(@NonNull String packageName, + @NonNull byte[] certificate) { + return new PackageIdentifier(packageName, certificate); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || !(obj instanceof PackageIdentifier)) { + return false; + } + final PackageIdentifier other = (PackageIdentifier) obj; + return this.packageName.equals(other.packageName) + && Arrays.equals(this.certificate, other.certificate); + } + + @Override + public int hashCode() { + return Objects.hash(packageName, Arrays.hashCode(certificate)); + } + + @Override + public String toString() { + return "[" + packageName + ", " + + Base64.encodeToString(certificate, Base64.NO_WRAP) + "]"; + } + } +} diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java new file mode 100644 index 000000000000..0b760a621d22 --- /dev/null +++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java @@ -0,0 +1,824 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.blob; + +import static android.app.blob.XmlTags.ATTR_COMMIT_TIME_MS; +import static android.app.blob.XmlTags.ATTR_DESCRIPTION; +import static android.app.blob.XmlTags.ATTR_DESCRIPTION_RES_NAME; +import static android.app.blob.XmlTags.ATTR_EXPIRY_TIME; +import static android.app.blob.XmlTags.ATTR_ID; +import static android.app.blob.XmlTags.ATTR_PACKAGE; +import static android.app.blob.XmlTags.ATTR_UID; +import static android.app.blob.XmlTags.ATTR_USER_ID; +import static android.app.blob.XmlTags.TAG_ACCESS_MODE; +import static android.app.blob.XmlTags.TAG_BLOB_HANDLE; +import static android.app.blob.XmlTags.TAG_COMMITTER; +import static android.app.blob.XmlTags.TAG_LEASEE; +import static android.os.Process.INVALID_UID; +import static android.system.OsConstants.O_RDONLY; +import static android.text.format.Formatter.FLAG_IEC_UNITS; +import static android.text.format.Formatter.formatFileSize; + +import static com.android.server.blob.BlobStoreConfig.TAG; +import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_COMMIT_TIME; +import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_DESC_RES_NAME; +import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_STRING_DESC; +import static com.android.server.blob.BlobStoreConfig.hasLeaseWaitTimeElapsed; +import static com.android.server.blob.BlobStoreUtils.getDescriptionResourceId; +import static com.android.server.blob.BlobStoreUtils.getPackageResources; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.blob.BlobHandle; +import android.app.blob.LeaseInfo; +import android.content.Context; +import android.content.res.ResourceId; +import android.content.res.Resources; +import android.os.ParcelFileDescriptor; +import android.os.RevocableFileDescriptor; +import android.os.UserHandle; +import android.system.ErrnoException; +import android.system.Os; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Slog; +import android.util.SparseArray; +import android.util.StatsEvent; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.XmlUtils; +import com.android.server.blob.BlobStoreManagerService.DumpArgs; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.util.Objects; +import java.util.function.Consumer; + +class BlobMetadata { + private final Object mMetadataLock = new Object(); + + private final Context mContext; + + private final long mBlobId; + private final BlobHandle mBlobHandle; + private final int mUserId; + + @GuardedBy("mMetadataLock") + private final ArraySet<Committer> mCommitters = new ArraySet<>(); + + @GuardedBy("mMetadataLock") + private final ArraySet<Leasee> mLeasees = new ArraySet<>(); + + /** + * Contains packageName -> {RevocableFileDescriptors}. + * + * Keep track of RevocableFileDescriptors given to clients which are not yet revoked/closed so + * that when clients access is revoked or the blob gets deleted, we can be sure that clients + * do not have any reference to the blob and the space occupied by the blob can be freed. + */ + @GuardedBy("mRevocableFds") + private final ArrayMap<String, ArraySet<RevocableFileDescriptor>> mRevocableFds = + new ArrayMap<>(); + + // Do not access this directly, instead use #getBlobFile(). + private File mBlobFile; + + BlobMetadata(Context context, long blobId, BlobHandle blobHandle, int userId) { + mContext = context; + this.mBlobId = blobId; + this.mBlobHandle = blobHandle; + this.mUserId = userId; + } + + long getBlobId() { + return mBlobId; + } + + BlobHandle getBlobHandle() { + return mBlobHandle; + } + + int getUserId() { + return mUserId; + } + + void addOrReplaceCommitter(@NonNull Committer committer) { + synchronized (mMetadataLock) { + // We need to override the committer data, so first remove any existing + // committer before adding the new one. + mCommitters.remove(committer); + mCommitters.add(committer); + } + } + + void setCommitters(ArraySet<Committer> committers) { + synchronized (mMetadataLock) { + mCommitters.clear(); + mCommitters.addAll(committers); + } + } + + void removeCommitter(@NonNull String packageName, int uid) { + synchronized (mMetadataLock) { + mCommitters.removeIf((committer) -> + committer.uid == uid && committer.packageName.equals(packageName)); + } + } + + void removeCommitter(@NonNull Committer committer) { + synchronized (mMetadataLock) { + mCommitters.remove(committer); + } + } + + void removeCommittersFromUnknownPkgs(SparseArray<String> knownPackages) { + synchronized (mMetadataLock) { + mCommitters.removeIf(committer -> + !committer.packageName.equals(knownPackages.get(committer.uid))); + } + } + + @Nullable + Committer getExistingCommitter(@NonNull String packageName, int uid) { + synchronized (mCommitters) { + for (int i = 0, size = mCommitters.size(); i < size; ++i) { + final Committer committer = mCommitters.valueAt(i); + if (committer.uid == uid && committer.packageName.equals(packageName)) { + return committer; + } + } + } + return null; + } + + void addOrReplaceLeasee(String callingPackage, int callingUid, int descriptionResId, + CharSequence description, long leaseExpiryTimeMillis) { + synchronized (mMetadataLock) { + // We need to override the leasee data, so first remove any existing + // leasee before adding the new one. + final Leasee leasee = new Leasee(mContext, callingPackage, callingUid, + descriptionResId, description, leaseExpiryTimeMillis); + mLeasees.remove(leasee); + mLeasees.add(leasee); + } + } + + void setLeasees(ArraySet<Leasee> leasees) { + synchronized (mMetadataLock) { + mLeasees.clear(); + mLeasees.addAll(leasees); + } + } + + void removeLeasee(String packageName, int uid) { + synchronized (mMetadataLock) { + mLeasees.removeIf((leasee) -> + leasee.uid == uid && leasee.packageName.equals(packageName)); + } + } + + void removeLeaseesFromUnknownPkgs(SparseArray<String> knownPackages) { + synchronized (mMetadataLock) { + mLeasees.removeIf(leasee -> + !leasee.packageName.equals(knownPackages.get(leasee.uid))); + } + } + + void removeExpiredLeases() { + synchronized (mMetadataLock) { + mLeasees.removeIf(leasee -> !leasee.isStillValid()); + } + } + + boolean hasValidLeases() { + synchronized (mMetadataLock) { + for (int i = 0, size = mLeasees.size(); i < size; ++i) { + if (mLeasees.valueAt(i).isStillValid()) { + return true; + } + } + return false; + } + } + + long getSize() { + return getBlobFile().length(); + } + + boolean isAccessAllowedForCaller(@NonNull String callingPackage, int callingUid) { + // Don't allow the blob to be accessed after it's expiry time has passed. + if (getBlobHandle().isExpired()) { + return false; + } + synchronized (mMetadataLock) { + // Check if packageName already holds a lease on the blob. + for (int i = 0, size = mLeasees.size(); i < size; ++i) { + final Leasee leasee = mLeasees.valueAt(i); + if (leasee.isStillValid() && leasee.equals(callingPackage, callingUid)) { + return true; + } + } + + for (int i = 0, size = mCommitters.size(); i < size; ++i) { + final Committer committer = mCommitters.valueAt(i); + + // Check if the caller is the same package that committed the blob. + if (committer.equals(callingPackage, callingUid)) { + return true; + } + + // Check if the caller is allowed access as per the access mode specified + // by the committer. + if (committer.blobAccessMode.isAccessAllowedForCaller(mContext, + callingPackage, committer.packageName)) { + return true; + } + } + } + return false; + } + + boolean isACommitter(@NonNull String packageName, int uid) { + synchronized (mMetadataLock) { + return isAnAccessor(mCommitters, packageName, uid); + } + } + + boolean isALeasee(@Nullable String packageName, int uid) { + synchronized (mMetadataLock) { + final Leasee leasee = getAccessor(mLeasees, packageName, uid); + return leasee != null && leasee.isStillValid(); + } + } + + private static <T extends Accessor> boolean isAnAccessor(@NonNull ArraySet<T> accessors, + @Nullable String packageName, int uid) { + // Check if the package is an accessor of the data blob. + return getAccessor(accessors, packageName, uid) != null; + } + + private static <T extends Accessor> T getAccessor(@NonNull ArraySet<T> accessors, + @Nullable String packageName, int uid) { + // Check if the package is an accessor of the data blob. + for (int i = 0, size = accessors.size(); i < size; ++i) { + final Accessor accessor = accessors.valueAt(i); + if (packageName != null && uid != INVALID_UID + && accessor.equals(packageName, uid)) { + return (T) accessor; + } else if (packageName != null && accessor.packageName.equals(packageName)) { + return (T) accessor; + } else if (uid != INVALID_UID && accessor.uid == uid) { + return (T) accessor; + } + } + return null; + } + + boolean isALeasee(@NonNull String packageName) { + return isALeasee(packageName, INVALID_UID); + } + + boolean isALeasee(int uid) { + return isALeasee(null, uid); + } + + boolean hasOtherLeasees(@NonNull String packageName) { + return hasOtherLeasees(packageName, INVALID_UID); + } + + boolean hasOtherLeasees(int uid) { + return hasOtherLeasees(null, uid); + } + + private boolean hasOtherLeasees(@Nullable String packageName, int uid) { + synchronized (mMetadataLock) { + for (int i = 0, size = mLeasees.size(); i < size; ++i) { + final Leasee leasee = mLeasees.valueAt(i); + if (!leasee.isStillValid()) { + continue; + } + // TODO: Also exclude packages which are signed with same cert? + if (packageName != null && uid != INVALID_UID + && !leasee.equals(packageName, uid)) { + return true; + } else if (packageName != null && !leasee.packageName.equals(packageName)) { + return true; + } else if (uid != INVALID_UID && leasee.uid != uid) { + return true; + } + } + } + return false; + } + + @Nullable + LeaseInfo getLeaseInfo(@NonNull String packageName, int uid) { + synchronized (mMetadataLock) { + for (int i = 0, size = mLeasees.size(); i < size; ++i) { + final Leasee leasee = mLeasees.valueAt(i); + if (!leasee.isStillValid()) { + continue; + } + if (leasee.uid == uid && leasee.packageName.equals(packageName)) { + final int descriptionResId = leasee.descriptionResEntryName == null + ? Resources.ID_NULL + : BlobStoreUtils.getDescriptionResourceId( + mContext, leasee.descriptionResEntryName, leasee.packageName, + UserHandle.getUserId(leasee.uid)); + return new LeaseInfo(packageName, leasee.expiryTimeMillis, + descriptionResId, leasee.description); + } + } + } + return null; + } + + void forEachLeasee(Consumer<Leasee> consumer) { + synchronized (mMetadataLock) { + mLeasees.forEach(consumer); + } + } + + File getBlobFile() { + if (mBlobFile == null) { + mBlobFile = BlobStoreConfig.getBlobFile(mBlobId); + } + return mBlobFile; + } + + ParcelFileDescriptor openForRead(String callingPackage) throws IOException { + // TODO: Add limit on opened fds + FileDescriptor fd; + try { + fd = Os.open(getBlobFile().getPath(), O_RDONLY, 0); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + try { + if (BlobStoreConfig.shouldUseRevocableFdForReads()) { + return createRevocableFd(fd, callingPackage); + } else { + return new ParcelFileDescriptor(fd); + } + } catch (IOException e) { + IoUtils.closeQuietly(fd); + throw e; + } + } + + @NonNull + private ParcelFileDescriptor createRevocableFd(FileDescriptor fd, + String callingPackage) throws IOException { + final RevocableFileDescriptor revocableFd = + new RevocableFileDescriptor(mContext, fd); + synchronized (mRevocableFds) { + ArraySet<RevocableFileDescriptor> revocableFdsForPkg = + mRevocableFds.get(callingPackage); + if (revocableFdsForPkg == null) { + revocableFdsForPkg = new ArraySet<>(); + mRevocableFds.put(callingPackage, revocableFdsForPkg); + } + revocableFdsForPkg.add(revocableFd); + } + revocableFd.addOnCloseListener((e) -> { + synchronized (mRevocableFds) { + final ArraySet<RevocableFileDescriptor> revocableFdsForPkg = + mRevocableFds.get(callingPackage); + if (revocableFdsForPkg != null) { + revocableFdsForPkg.remove(revocableFd); + if (revocableFdsForPkg.isEmpty()) { + mRevocableFds.remove(callingPackage); + } + } + } + }); + return revocableFd.getRevocableFileDescriptor(); + } + + void destroy() { + revokeAllFds(); + getBlobFile().delete(); + } + + private void revokeAllFds() { + synchronized (mRevocableFds) { + for (int i = 0, pkgCount = mRevocableFds.size(); i < pkgCount; ++i) { + final ArraySet<RevocableFileDescriptor> packageFds = + mRevocableFds.valueAt(i); + if (packageFds == null) { + continue; + } + for (int j = 0, fdCount = packageFds.size(); j < fdCount; ++j) { + packageFds.valueAt(j).revoke(); + } + } + } + } + + boolean shouldBeDeleted(boolean respectLeaseWaitTime) { + // Expired data blobs + if (getBlobHandle().isExpired()) { + return true; + } + + // Blobs with no active leases + if ((!respectLeaseWaitTime || hasLeaseWaitTimeElapsedForAll()) + && !hasValidLeases()) { + return true; + } + + return false; + } + + @VisibleForTesting + boolean hasLeaseWaitTimeElapsedForAll() { + for (int i = 0, size = mCommitters.size(); i < size; ++i) { + final Committer committer = mCommitters.valueAt(i); + if (!hasLeaseWaitTimeElapsed(committer.getCommitTimeMs())) { + return false; + } + } + return true; + } + + StatsEvent dumpAsStatsEvent(int atomTag) { + synchronized (mMetadataLock) { + ProtoOutputStream proto = new ProtoOutputStream(); + // Write Committer data to proto format + for (int i = 0, size = mCommitters.size(); i < size; ++i) { + final Committer committer = mCommitters.valueAt(i); + final long token = proto.start( + BlobStatsEventProto.BlobCommitterListProto.COMMITTER); + proto.write(BlobStatsEventProto.BlobCommitterProto.UID, committer.uid); + proto.write(BlobStatsEventProto.BlobCommitterProto.COMMIT_TIMESTAMP_MILLIS, + committer.commitTimeMs); + proto.write(BlobStatsEventProto.BlobCommitterProto.ACCESS_MODE, + committer.blobAccessMode.getAccessType()); + proto.write(BlobStatsEventProto.BlobCommitterProto.NUM_WHITELISTED_PACKAGE, + committer.blobAccessMode.getNumWhitelistedPackages()); + proto.end(token); + } + final byte[] committersBytes = proto.getBytes(); + + proto = new ProtoOutputStream(); + // Write Leasee data to proto format + for (int i = 0, size = mLeasees.size(); i < size; ++i) { + final Leasee leasee = mLeasees.valueAt(i); + final long token = proto.start(BlobStatsEventProto.BlobLeaseeListProto.LEASEE); + proto.write(BlobStatsEventProto.BlobLeaseeProto.UID, leasee.uid); + proto.write(BlobStatsEventProto.BlobLeaseeProto.LEASE_EXPIRY_TIMESTAMP_MILLIS, + leasee.expiryTimeMillis); + proto.end(token); + } + final byte[] leaseesBytes = proto.getBytes(); + + // Construct the StatsEvent to represent this Blob + return StatsEvent.newBuilder() + .setAtomId(atomTag) + .writeLong(mBlobId) + .writeLong(getSize()) + .writeLong(mBlobHandle.getExpiryTimeMillis()) + .writeByteArray(committersBytes) + .writeByteArray(leaseesBytes) + .build(); + } + } + + void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) { + synchronized (mMetadataLock) { + fout.println("blobHandle:"); + fout.increaseIndent(); + mBlobHandle.dump(fout, dumpArgs.shouldDumpFull()); + fout.decreaseIndent(); + fout.println("size: " + formatFileSize(mContext, getSize(), FLAG_IEC_UNITS)); + + fout.println("Committers:"); + fout.increaseIndent(); + if (mCommitters.isEmpty()) { + fout.println("<empty>"); + } else { + for (int i = 0, count = mCommitters.size(); i < count; ++i) { + final Committer committer = mCommitters.valueAt(i); + fout.println("committer " + committer.toString()); + fout.increaseIndent(); + committer.dump(fout); + fout.decreaseIndent(); + } + } + fout.decreaseIndent(); + + fout.println("Leasees:"); + fout.increaseIndent(); + if (mLeasees.isEmpty()) { + fout.println("<empty>"); + } else { + for (int i = 0, count = mLeasees.size(); i < count; ++i) { + final Leasee leasee = mLeasees.valueAt(i); + fout.println("leasee " + leasee.toString()); + fout.increaseIndent(); + leasee.dump(mContext, fout); + fout.decreaseIndent(); + } + } + fout.decreaseIndent(); + + fout.println("Open fds:"); + fout.increaseIndent(); + if (mRevocableFds.isEmpty()) { + fout.println("<empty>"); + } else { + for (int i = 0, count = mRevocableFds.size(); i < count; ++i) { + final String packageName = mRevocableFds.keyAt(i); + final ArraySet<RevocableFileDescriptor> packageFds = + mRevocableFds.valueAt(i); + fout.println(packageName + "#" + packageFds.size()); + } + } + fout.decreaseIndent(); + } + } + + void writeToXml(XmlSerializer out) throws IOException { + synchronized (mMetadataLock) { + XmlUtils.writeLongAttribute(out, ATTR_ID, mBlobId); + XmlUtils.writeIntAttribute(out, ATTR_USER_ID, mUserId); + + out.startTag(null, TAG_BLOB_HANDLE); + mBlobHandle.writeToXml(out); + out.endTag(null, TAG_BLOB_HANDLE); + + for (int i = 0, count = mCommitters.size(); i < count; ++i) { + out.startTag(null, TAG_COMMITTER); + mCommitters.valueAt(i).writeToXml(out); + out.endTag(null, TAG_COMMITTER); + } + + for (int i = 0, count = mLeasees.size(); i < count; ++i) { + out.startTag(null, TAG_LEASEE); + mLeasees.valueAt(i).writeToXml(out); + out.endTag(null, TAG_LEASEE); + } + } + } + + @Nullable + static BlobMetadata createFromXml(XmlPullParser in, int version, Context context) + throws XmlPullParserException, IOException { + final long blobId = XmlUtils.readLongAttribute(in, ATTR_ID); + final int userId = XmlUtils.readIntAttribute(in, ATTR_USER_ID); + + BlobHandle blobHandle = null; + final ArraySet<Committer> committers = new ArraySet<>(); + final ArraySet<Leasee> leasees = new ArraySet<>(); + final int depth = in.getDepth(); + while (XmlUtils.nextElementWithin(in, depth)) { + if (TAG_BLOB_HANDLE.equals(in.getName())) { + blobHandle = BlobHandle.createFromXml(in); + } else if (TAG_COMMITTER.equals(in.getName())) { + final Committer committer = Committer.createFromXml(in, version); + if (committer != null) { + committers.add(committer); + } + } else if (TAG_LEASEE.equals(in.getName())) { + leasees.add(Leasee.createFromXml(in, version)); + } + } + + if (blobHandle == null) { + Slog.wtf(TAG, "blobHandle should be available"); + return null; + } + + final BlobMetadata blobMetadata = new BlobMetadata(context, blobId, blobHandle, userId); + blobMetadata.setCommitters(committers); + blobMetadata.setLeasees(leasees); + return blobMetadata; + } + + static final class Committer extends Accessor { + public final BlobAccessMode blobAccessMode; + public final long commitTimeMs; + + Committer(String packageName, int uid, BlobAccessMode blobAccessMode, long commitTimeMs) { + super(packageName, uid); + this.blobAccessMode = blobAccessMode; + this.commitTimeMs = commitTimeMs; + } + + long getCommitTimeMs() { + return commitTimeMs; + } + + void dump(IndentingPrintWriter fout) { + fout.println("commit time: " + + (commitTimeMs == 0 ? "<null>" : BlobStoreUtils.formatTime(commitTimeMs))); + fout.println("accessMode:"); + fout.increaseIndent(); + blobAccessMode.dump(fout); + fout.decreaseIndent(); + } + + void writeToXml(@NonNull XmlSerializer out) throws IOException { + XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, packageName); + XmlUtils.writeIntAttribute(out, ATTR_UID, uid); + XmlUtils.writeLongAttribute(out, ATTR_COMMIT_TIME_MS, commitTimeMs); + + out.startTag(null, TAG_ACCESS_MODE); + blobAccessMode.writeToXml(out); + out.endTag(null, TAG_ACCESS_MODE); + } + + @Nullable + static Committer createFromXml(@NonNull XmlPullParser in, int version) + throws XmlPullParserException, IOException { + final String packageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE); + final int uid = XmlUtils.readIntAttribute(in, ATTR_UID); + final long commitTimeMs = version >= XML_VERSION_ADD_COMMIT_TIME + ? XmlUtils.readLongAttribute(in, ATTR_COMMIT_TIME_MS) + : 0; + + final int depth = in.getDepth(); + BlobAccessMode blobAccessMode = null; + while (XmlUtils.nextElementWithin(in, depth)) { + if (TAG_ACCESS_MODE.equals(in.getName())) { + blobAccessMode = BlobAccessMode.createFromXml(in); + } + } + if (blobAccessMode == null) { + Slog.wtf(TAG, "blobAccessMode should be available"); + return null; + } + return new Committer(packageName, uid, blobAccessMode, commitTimeMs); + } + } + + static final class Leasee extends Accessor { + public final String descriptionResEntryName; + public final CharSequence description; + public final long expiryTimeMillis; + + Leasee(@NonNull Context context, @NonNull String packageName, + int uid, int descriptionResId, + @Nullable CharSequence description, long expiryTimeMillis) { + super(packageName, uid); + final Resources packageResources = getPackageResources(context, packageName, + UserHandle.getUserId(uid)); + this.descriptionResEntryName = getResourceEntryName(packageResources, descriptionResId); + this.expiryTimeMillis = expiryTimeMillis; + this.description = description == null + ? getDescription(packageResources, descriptionResId) + : description; + } + + Leasee(String packageName, int uid, @Nullable String descriptionResEntryName, + @Nullable CharSequence description, long expiryTimeMillis) { + super(packageName, uid); + this.descriptionResEntryName = descriptionResEntryName; + this.expiryTimeMillis = expiryTimeMillis; + this.description = description; + } + + @Nullable + private static String getResourceEntryName(@Nullable Resources packageResources, + int resId) { + if (!ResourceId.isValid(resId) || packageResources == null) { + return null; + } + return packageResources.getResourceEntryName(resId); + } + + @Nullable + private static String getDescription(@NonNull Context context, + @NonNull String descriptionResEntryName, @NonNull String packageName, int userId) { + if (descriptionResEntryName == null || descriptionResEntryName.isEmpty()) { + return null; + } + final Resources resources = getPackageResources(context, packageName, userId); + if (resources == null) { + return null; + } + final int resId = getDescriptionResourceId(resources, descriptionResEntryName, + packageName); + return resId == Resources.ID_NULL ? null : resources.getString(resId); + } + + @Nullable + private static String getDescription(@Nullable Resources packageResources, + int descriptionResId) { + if (!ResourceId.isValid(descriptionResId) || packageResources == null) { + return null; + } + return packageResources.getString(descriptionResId); + } + + boolean isStillValid() { + return expiryTimeMillis == 0 || expiryTimeMillis >= System.currentTimeMillis(); + } + + void dump(@NonNull Context context, @NonNull IndentingPrintWriter fout) { + fout.println("desc: " + getDescriptionToDump(context)); + fout.println("expiryMs: " + expiryTimeMillis); + } + + @NonNull + private String getDescriptionToDump(@NonNull Context context) { + String desc = getDescription(context, descriptionResEntryName, packageName, + UserHandle.getUserId(uid)); + if (desc == null) { + desc = description.toString(); + } + return desc == null ? "<none>" : desc; + } + + void writeToXml(@NonNull XmlSerializer out) throws IOException { + XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, packageName); + XmlUtils.writeIntAttribute(out, ATTR_UID, uid); + XmlUtils.writeStringAttribute(out, ATTR_DESCRIPTION_RES_NAME, descriptionResEntryName); + XmlUtils.writeLongAttribute(out, ATTR_EXPIRY_TIME, expiryTimeMillis); + XmlUtils.writeStringAttribute(out, ATTR_DESCRIPTION, description); + } + + @NonNull + static Leasee createFromXml(@NonNull XmlPullParser in, int version) + throws IOException { + final String packageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE); + final int uid = XmlUtils.readIntAttribute(in, ATTR_UID); + final String descriptionResEntryName; + if (version >= XML_VERSION_ADD_DESC_RES_NAME) { + descriptionResEntryName = XmlUtils.readStringAttribute( + in, ATTR_DESCRIPTION_RES_NAME); + } else { + descriptionResEntryName = null; + } + final long expiryTimeMillis = XmlUtils.readLongAttribute(in, ATTR_EXPIRY_TIME); + final CharSequence description; + if (version >= XML_VERSION_ADD_STRING_DESC) { + description = XmlUtils.readStringAttribute(in, ATTR_DESCRIPTION); + } else { + description = null; + } + + return new Leasee(packageName, uid, descriptionResEntryName, + description, expiryTimeMillis); + } + } + + static class Accessor { + public final String packageName; + public final int uid; + + Accessor(String packageName, int uid) { + this.packageName = packageName; + this.uid = uid; + } + + public boolean equals(String packageName, int uid) { + return this.uid == uid && this.packageName.equals(packageName); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || !(obj instanceof Accessor)) { + return false; + } + final Accessor other = (Accessor) obj; + return this.uid == other.uid && this.packageName.equals(other.packageName); + } + + @Override + public int hashCode() { + return Objects.hash(packageName, uid); + } + + @Override + public String toString() { + return "[" + packageName + ", " + uid + "]"; + } + } +} diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java new file mode 100644 index 000000000000..bb9f13f1712c --- /dev/null +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java @@ -0,0 +1,479 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.blob; + +import static android.provider.DeviceConfig.NAMESPACE_BLOBSTORE; +import static android.text.format.Formatter.FLAG_IEC_UNITS; +import static android.text.format.Formatter.formatFileSize; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.Environment; +import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; +import android.text.TextUtils; +import android.util.DataUnit; +import android.util.Log; +import android.util.Slog; +import android.util.TimeUtils; + +import com.android.internal.util.IndentingPrintWriter; + +import java.io.File; +import java.util.concurrent.TimeUnit; + +class BlobStoreConfig { + public static final String TAG = "BlobStore"; + public static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE); + + // Initial version. + public static final int XML_VERSION_INIT = 1; + // Added a string variant of lease description. + public static final int XML_VERSION_ADD_STRING_DESC = 2; + public static final int XML_VERSION_ADD_DESC_RES_NAME = 3; + public static final int XML_VERSION_ADD_COMMIT_TIME = 4; + public static final int XML_VERSION_ADD_SESSION_CREATION_TIME = 5; + + public static final int XML_VERSION_CURRENT = XML_VERSION_ADD_SESSION_CREATION_TIME; + + public static final long INVALID_BLOB_ID = 0; + public static final long INVALID_BLOB_SIZE = 0; + + private static final String ROOT_DIR_NAME = "blobstore"; + private static final String BLOBS_DIR_NAME = "blobs"; + private static final String SESSIONS_INDEX_FILE_NAME = "sessions_index.xml"; + private static final String BLOBS_INDEX_FILE_NAME = "blobs_index.xml"; + + /** + * Job Id for idle maintenance job ({@link BlobStoreIdleJobService}). + */ + public static final int IDLE_JOB_ID = 0xB70B1D7; // 191934935L + + public static class DeviceConfigProperties { + /** + * Denotes the max time period (in millis) between each idle maintenance job run. + */ + public static final String KEY_IDLE_JOB_PERIOD_MS = "idle_job_period_ms"; + public static final long DEFAULT_IDLE_JOB_PERIOD_MS = TimeUnit.DAYS.toMillis(1); + public static long IDLE_JOB_PERIOD_MS = DEFAULT_IDLE_JOB_PERIOD_MS; + + /** + * Denotes the timeout in millis after which sessions with no updates will be deleted. + */ + public static final String KEY_SESSION_EXPIRY_TIMEOUT_MS = + "session_expiry_timeout_ms"; + public static final long DEFAULT_SESSION_EXPIRY_TIMEOUT_MS = TimeUnit.DAYS.toMillis(7); + public static long SESSION_EXPIRY_TIMEOUT_MS = DEFAULT_SESSION_EXPIRY_TIMEOUT_MS; + + /** + * Denotes how low the limit for the amount of data, that an app will be allowed to acquire + * a lease on, can be. + */ + public static final String KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR = + "total_bytes_per_app_limit_floor"; + public static final long DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR = + DataUnit.MEBIBYTES.toBytes(300); // 300 MiB + public static long TOTAL_BYTES_PER_APP_LIMIT_FLOOR = + DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR; + + /** + * Denotes the maximum amount of data an app can acquire a lease on, in terms of fraction + * of total disk space. + */ + public static final String KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION = + "total_bytes_per_app_limit_fraction"; + public static final float DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION = 0.01f; + public static float TOTAL_BYTES_PER_APP_LIMIT_FRACTION = + DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION; + + /** + * Denotes the duration from the time a blob is committed that we wait for a lease to + * be acquired before deciding to delete the blob for having no leases. + */ + public static final String KEY_LEASE_ACQUISITION_WAIT_DURATION_MS = + "lease_acquisition_wait_time_ms"; + public static final long DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS = + TimeUnit.HOURS.toMillis(6); + public static long LEASE_ACQUISITION_WAIT_DURATION_MS = + DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS; + + /** + * Denotes the duration from the time a blob is committed that any new commits of the same + * data blob from the same committer will be treated as if they occurred at the earlier + * commit time. + */ + public static final String KEY_COMMIT_COOL_OFF_DURATION_MS = + "commit_cool_off_duration_ms"; + public static final long DEFAULT_COMMIT_COOL_OFF_DURATION_MS = + TimeUnit.HOURS.toMillis(48); + public static long COMMIT_COOL_OFF_DURATION_MS = + DEFAULT_COMMIT_COOL_OFF_DURATION_MS; + + /** + * Denotes whether to use RevocableFileDescriptor when apps try to read session/blob data. + */ + public static final String KEY_USE_REVOCABLE_FD_FOR_READS = + "use_revocable_fd_for_reads"; + public static final boolean DEFAULT_USE_REVOCABLE_FD_FOR_READS = true; + public static boolean USE_REVOCABLE_FD_FOR_READS = + DEFAULT_USE_REVOCABLE_FD_FOR_READS; + + /** + * Denotes how long before a blob is deleted, once the last lease on it is released. + */ + public static final String KEY_DELETE_ON_LAST_LEASE_DELAY_MS = + "delete_on_last_lease_delay_ms"; + public static final long DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS = + TimeUnit.HOURS.toMillis(6); + public static long DELETE_ON_LAST_LEASE_DELAY_MS = + DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS; + + /** + * Denotes the maximum number of active sessions per app at any time. + */ + public static final String KEY_MAX_ACTIVE_SESSIONS = "max_active_sessions"; + public static int DEFAULT_MAX_ACTIVE_SESSIONS = 250; + public static int MAX_ACTIVE_SESSIONS = DEFAULT_MAX_ACTIVE_SESSIONS; + + /** + * Denotes the maximum number of committed blobs per app at any time. + */ + public static final String KEY_MAX_COMMITTED_BLOBS = "max_committed_blobs"; + public static int DEFAULT_MAX_COMMITTED_BLOBS = 1000; + public static int MAX_COMMITTED_BLOBS = DEFAULT_MAX_COMMITTED_BLOBS; + + /** + * Denotes the maximum number of leased blobs per app at any time. + */ + public static final String KEY_MAX_LEASED_BLOBS = "max_leased_blobs"; + public static int DEFAULT_MAX_LEASED_BLOBS = 500; + public static int MAX_LEASED_BLOBS = DEFAULT_MAX_LEASED_BLOBS; + + /** + * Denotes the maximum number of packages explicitly permitted to access a blob + * (permitted as part of creating a {@link BlobAccessMode}). + */ + public static final String KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES = "max_permitted_pks"; + public static int DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES = 300; + public static int MAX_BLOB_ACCESS_PERMITTED_PACKAGES = + DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES; + + /** + * Denotes the maximum number of characters that a lease description can have. + */ + public static final String KEY_LEASE_DESC_CHAR_LIMIT = "lease_desc_char_limit"; + public static int DEFAULT_LEASE_DESC_CHAR_LIMIT = 300; + public static int LEASE_DESC_CHAR_LIMIT = DEFAULT_LEASE_DESC_CHAR_LIMIT; + + static void refresh(Properties properties) { + if (!NAMESPACE_BLOBSTORE.equals(properties.getNamespace())) { + return; + } + properties.getKeyset().forEach(key -> { + switch (key) { + case KEY_IDLE_JOB_PERIOD_MS: + IDLE_JOB_PERIOD_MS = properties.getLong(key, DEFAULT_IDLE_JOB_PERIOD_MS); + break; + case KEY_SESSION_EXPIRY_TIMEOUT_MS: + SESSION_EXPIRY_TIMEOUT_MS = properties.getLong(key, + DEFAULT_SESSION_EXPIRY_TIMEOUT_MS); + break; + case KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR: + TOTAL_BYTES_PER_APP_LIMIT_FLOOR = properties.getLong(key, + DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR); + break; + case KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION: + TOTAL_BYTES_PER_APP_LIMIT_FRACTION = properties.getFloat(key, + DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION); + break; + case KEY_LEASE_ACQUISITION_WAIT_DURATION_MS: + LEASE_ACQUISITION_WAIT_DURATION_MS = properties.getLong(key, + DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS); + break; + case KEY_COMMIT_COOL_OFF_DURATION_MS: + COMMIT_COOL_OFF_DURATION_MS = properties.getLong(key, + DEFAULT_COMMIT_COOL_OFF_DURATION_MS); + break; + case KEY_USE_REVOCABLE_FD_FOR_READS: + USE_REVOCABLE_FD_FOR_READS = properties.getBoolean(key, + DEFAULT_USE_REVOCABLE_FD_FOR_READS); + break; + case KEY_DELETE_ON_LAST_LEASE_DELAY_MS: + DELETE_ON_LAST_LEASE_DELAY_MS = properties.getLong(key, + DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS); + break; + case KEY_MAX_ACTIVE_SESSIONS: + MAX_ACTIVE_SESSIONS = properties.getInt(key, DEFAULT_MAX_ACTIVE_SESSIONS); + break; + case KEY_MAX_COMMITTED_BLOBS: + MAX_COMMITTED_BLOBS = properties.getInt(key, DEFAULT_MAX_COMMITTED_BLOBS); + break; + case KEY_MAX_LEASED_BLOBS: + MAX_LEASED_BLOBS = properties.getInt(key, DEFAULT_MAX_LEASED_BLOBS); + break; + case KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES: + MAX_BLOB_ACCESS_PERMITTED_PACKAGES = properties.getInt(key, + DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES); + break; + case KEY_LEASE_DESC_CHAR_LIMIT: + LEASE_DESC_CHAR_LIMIT = properties.getInt(key, + DEFAULT_LEASE_DESC_CHAR_LIMIT); + break; + default: + Slog.wtf(TAG, "Unknown key in device config properties: " + key); + } + }); + } + + static void dump(IndentingPrintWriter fout, Context context) { + final String dumpFormat = "%s: [cur: %s, def: %s]"; + fout.println(String.format(dumpFormat, KEY_IDLE_JOB_PERIOD_MS, + TimeUtils.formatDuration(IDLE_JOB_PERIOD_MS), + TimeUtils.formatDuration(DEFAULT_IDLE_JOB_PERIOD_MS))); + fout.println(String.format(dumpFormat, KEY_SESSION_EXPIRY_TIMEOUT_MS, + TimeUtils.formatDuration(SESSION_EXPIRY_TIMEOUT_MS), + TimeUtils.formatDuration(DEFAULT_SESSION_EXPIRY_TIMEOUT_MS))); + fout.println(String.format(dumpFormat, KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR, + formatFileSize(context, TOTAL_BYTES_PER_APP_LIMIT_FLOOR, FLAG_IEC_UNITS), + formatFileSize(context, DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR, + FLAG_IEC_UNITS))); + fout.println(String.format(dumpFormat, KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION, + TOTAL_BYTES_PER_APP_LIMIT_FRACTION, + DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION)); + fout.println(String.format(dumpFormat, KEY_LEASE_ACQUISITION_WAIT_DURATION_MS, + TimeUtils.formatDuration(LEASE_ACQUISITION_WAIT_DURATION_MS), + TimeUtils.formatDuration(DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS))); + fout.println(String.format(dumpFormat, KEY_COMMIT_COOL_OFF_DURATION_MS, + TimeUtils.formatDuration(COMMIT_COOL_OFF_DURATION_MS), + TimeUtils.formatDuration(DEFAULT_COMMIT_COOL_OFF_DURATION_MS))); + fout.println(String.format(dumpFormat, KEY_USE_REVOCABLE_FD_FOR_READS, + USE_REVOCABLE_FD_FOR_READS, DEFAULT_USE_REVOCABLE_FD_FOR_READS)); + fout.println(String.format(dumpFormat, KEY_DELETE_ON_LAST_LEASE_DELAY_MS, + TimeUtils.formatDuration(DELETE_ON_LAST_LEASE_DELAY_MS), + TimeUtils.formatDuration(DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS))); + fout.println(String.format(dumpFormat, KEY_MAX_ACTIVE_SESSIONS, + MAX_ACTIVE_SESSIONS, DEFAULT_MAX_ACTIVE_SESSIONS)); + fout.println(String.format(dumpFormat, KEY_MAX_COMMITTED_BLOBS, + MAX_COMMITTED_BLOBS, DEFAULT_MAX_COMMITTED_BLOBS)); + fout.println(String.format(dumpFormat, KEY_MAX_LEASED_BLOBS, + MAX_LEASED_BLOBS, DEFAULT_MAX_LEASED_BLOBS)); + fout.println(String.format(dumpFormat, KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES, + MAX_BLOB_ACCESS_PERMITTED_PACKAGES, + DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES)); + fout.println(String.format(dumpFormat, KEY_LEASE_DESC_CHAR_LIMIT, + LEASE_DESC_CHAR_LIMIT, DEFAULT_LEASE_DESC_CHAR_LIMIT)); + } + } + + public static void initialize(Context context) { + DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_BLOBSTORE, + context.getMainExecutor(), + properties -> DeviceConfigProperties.refresh(properties)); + DeviceConfigProperties.refresh(DeviceConfig.getProperties(NAMESPACE_BLOBSTORE)); + } + + /** + * Returns the max time period (in millis) between each idle maintenance job run. + */ + public static long getIdleJobPeriodMs() { + return DeviceConfigProperties.IDLE_JOB_PERIOD_MS; + } + + /** + * Returns whether a session is expired or not. A session is considered expired if the session + * has not been modified in a while (i.e. SESSION_EXPIRY_TIMEOUT_MS). + */ + public static boolean hasSessionExpired(long sessionLastModifiedMs) { + return sessionLastModifiedMs + < System.currentTimeMillis() - DeviceConfigProperties.SESSION_EXPIRY_TIMEOUT_MS; + } + + /** + * Returns the maximum amount of data that an app can acquire a lease on. + */ + public static long getAppDataBytesLimit() { + final long totalBytesLimit = (long) (Environment.getDataSystemDirectory().getTotalSpace() + * DeviceConfigProperties.TOTAL_BYTES_PER_APP_LIMIT_FRACTION); + return Math.max(DeviceConfigProperties.TOTAL_BYTES_PER_APP_LIMIT_FLOOR, totalBytesLimit); + } + + /** + * Returns whether the wait time for lease acquisition for a blob has elapsed. + */ + public static boolean hasLeaseWaitTimeElapsed(long commitTimeMs) { + return commitTimeMs + DeviceConfigProperties.LEASE_ACQUISITION_WAIT_DURATION_MS + < System.currentTimeMillis(); + } + + /** + * Returns an adjusted commit time depending on whether commit cool-off period has elapsed. + * + * If this is the initial commit or the earlier commit cool-off period has elapsed, then + * the new commit time is used. Otherwise, the earlier commit time is used. + */ + public static long getAdjustedCommitTimeMs(long oldCommitTimeMs, long newCommitTimeMs) { + if (oldCommitTimeMs == 0 || hasCommitCoolOffPeriodElapsed(oldCommitTimeMs)) { + return newCommitTimeMs; + } + return oldCommitTimeMs; + } + + /** + * Returns whether the commit cool-off period has elapsed. + */ + private static boolean hasCommitCoolOffPeriodElapsed(long commitTimeMs) { + return commitTimeMs + DeviceConfigProperties.COMMIT_COOL_OFF_DURATION_MS + < System.currentTimeMillis(); + } + + /** + * Return whether to use RevocableFileDescriptor when apps try to read session/blob data. + */ + public static boolean shouldUseRevocableFdForReads() { + return DeviceConfigProperties.USE_REVOCABLE_FD_FOR_READS; + } + + /** + * Returns the duration to wait before a blob is deleted, once the last lease on it is released. + */ + public static long getDeletionOnLastLeaseDelayMs() { + return DeviceConfigProperties.DELETE_ON_LAST_LEASE_DELAY_MS; + } + + /** + * Returns the maximum number of active sessions per app. + */ + public static int getMaxActiveSessions() { + return DeviceConfigProperties.MAX_ACTIVE_SESSIONS; + } + + /** + * Returns the maximum number of committed blobs per app. + */ + public static int getMaxCommittedBlobs() { + return DeviceConfigProperties.MAX_COMMITTED_BLOBS; + } + + /** + * Returns the maximum number of leased blobs per app. + */ + public static int getMaxLeasedBlobs() { + return DeviceConfigProperties.MAX_LEASED_BLOBS; + } + + /** + * Returns the maximum number of packages explicitly permitted to access a blob. + */ + public static int getMaxPermittedPackages() { + return DeviceConfigProperties.MAX_BLOB_ACCESS_PERMITTED_PACKAGES; + } + + /** + * Returns the lease description truncated to + * {@link DeviceConfigProperties#LEASE_DESC_CHAR_LIMIT} characters. + */ + public static CharSequence getTruncatedLeaseDescription(CharSequence description) { + if (TextUtils.isEmpty(description)) { + return description; + } + return TextUtils.trimToLengthWithEllipsis(description, + DeviceConfigProperties.LEASE_DESC_CHAR_LIMIT); + } + + @Nullable + public static File prepareBlobFile(long sessionId) { + final File blobsDir = prepareBlobsDir(); + return blobsDir == null ? null : getBlobFile(blobsDir, sessionId); + } + + @NonNull + public static File getBlobFile(long sessionId) { + return getBlobFile(getBlobsDir(), sessionId); + } + + @NonNull + private static File getBlobFile(File blobsDir, long sessionId) { + return new File(blobsDir, String.valueOf(sessionId)); + } + + @Nullable + public static File prepareBlobsDir() { + final File blobsDir = getBlobsDir(prepareBlobStoreRootDir()); + if (!blobsDir.exists() && !blobsDir.mkdir()) { + Slog.e(TAG, "Failed to mkdir(): " + blobsDir); + return null; + } + return blobsDir; + } + + @NonNull + public static File getBlobsDir() { + return getBlobsDir(getBlobStoreRootDir()); + } + + @NonNull + private static File getBlobsDir(File blobsRootDir) { + return new File(blobsRootDir, BLOBS_DIR_NAME); + } + + @Nullable + public static File prepareSessionIndexFile() { + final File blobStoreRootDir = prepareBlobStoreRootDir(); + if (blobStoreRootDir == null) { + return null; + } + return new File(blobStoreRootDir, SESSIONS_INDEX_FILE_NAME); + } + + @Nullable + public static File prepareBlobsIndexFile() { + final File blobsStoreRootDir = prepareBlobStoreRootDir(); + if (blobsStoreRootDir == null) { + return null; + } + return new File(blobsStoreRootDir, BLOBS_INDEX_FILE_NAME); + } + + @Nullable + public static File prepareBlobStoreRootDir() { + final File blobStoreRootDir = getBlobStoreRootDir(); + if (!blobStoreRootDir.exists() && !blobStoreRootDir.mkdir()) { + Slog.e(TAG, "Failed to mkdir(): " + blobStoreRootDir); + return null; + } + return blobStoreRootDir; + } + + @NonNull + public static File getBlobStoreRootDir() { + return new File(Environment.getDataSystemDirectory(), ROOT_DIR_NAME); + } + + public static void dump(IndentingPrintWriter fout, Context context) { + fout.println("XML current version: " + XML_VERSION_CURRENT); + + fout.println("Idle job ID: " + IDLE_JOB_ID); + + fout.println("Total bytes per app limit: " + formatFileSize(context, + getAppDataBytesLimit(), FLAG_IEC_UNITS)); + + fout.println("Device config properties:"); + fout.increaseIndent(); + DeviceConfigProperties.dump(fout, context); + fout.decreaseIndent(); + } +} diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java new file mode 100644 index 000000000000..4b0f719b13be --- /dev/null +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.blob; + +import static com.android.server.blob.BlobStoreConfig.IDLE_JOB_ID; +import static com.android.server.blob.BlobStoreConfig.LOGV; +import static com.android.server.blob.BlobStoreConfig.TAG; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.os.AsyncTask; +import android.util.Slog; + +import com.android.server.LocalServices; + +/** + * Maintenance job to clean up stale sessions and blobs. + */ +public class BlobStoreIdleJobService extends JobService { + @Override + public boolean onStartJob(final JobParameters params) { + AsyncTask.execute(() -> { + final BlobStoreManagerInternal blobStoreManagerInternal = LocalServices.getService( + BlobStoreManagerInternal.class); + blobStoreManagerInternal.onIdleMaintenance(); + jobFinished(params, false); + }); + return false; + } + + @Override + public boolean onStopJob(final JobParameters params) { + Slog.d(TAG, "Idle maintenance job is stopped; id=" + params.getJobId() + + ", reason=" + JobParameters.getReasonCodeDescription(params.getStopReason())); + return false; + } + + static void schedule(Context context) { + final JobScheduler jobScheduler = (JobScheduler) context.getSystemService( + Context.JOB_SCHEDULER_SERVICE); + final JobInfo job = new JobInfo.Builder(IDLE_JOB_ID, + new ComponentName(context, BlobStoreIdleJobService.class)) + .setRequiresDeviceIdle(true) + .setRequiresCharging(true) + .setPeriodic(BlobStoreConfig.getIdleJobPeriodMs()) + .build(); + jobScheduler.schedule(job); + if (LOGV) { + Slog.v(TAG, "Scheduling the idle maintenance job"); + } + } +} diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerInternal.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerInternal.java new file mode 100644 index 000000000000..5358245f517f --- /dev/null +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerInternal.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.blob; + +/** + * BlobStoreManager local system service interface. + * + * Only for use within the system server. + */ +public abstract class BlobStoreManagerInternal { + /** + * Triggered from idle maintenance job to cleanup stale blobs and sessions. + */ + public abstract void onIdleMaintenance(); +} diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java new file mode 100644 index 000000000000..d37dfdeaa583 --- /dev/null +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java @@ -0,0 +1,1915 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.blob; + +import static android.app.blob.BlobStoreManager.COMMIT_RESULT_ERROR; +import static android.app.blob.BlobStoreManager.COMMIT_RESULT_SUCCESS; +import static android.app.blob.XmlTags.ATTR_VERSION; +import static android.app.blob.XmlTags.TAG_BLOB; +import static android.app.blob.XmlTags.TAG_BLOBS; +import static android.app.blob.XmlTags.TAG_SESSION; +import static android.app.blob.XmlTags.TAG_SESSIONS; +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; +import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES; +import static android.os.UserHandle.USER_CURRENT; +import static android.os.UserHandle.USER_NULL; + +import static com.android.server.blob.BlobStoreConfig.INVALID_BLOB_ID; +import static com.android.server.blob.BlobStoreConfig.INVALID_BLOB_SIZE; +import static com.android.server.blob.BlobStoreConfig.LOGV; +import static com.android.server.blob.BlobStoreConfig.TAG; +import static com.android.server.blob.BlobStoreConfig.XML_VERSION_CURRENT; +import static com.android.server.blob.BlobStoreConfig.getAdjustedCommitTimeMs; +import static com.android.server.blob.BlobStoreConfig.getDeletionOnLastLeaseDelayMs; +import static com.android.server.blob.BlobStoreConfig.getMaxActiveSessions; +import static com.android.server.blob.BlobStoreConfig.getMaxCommittedBlobs; +import static com.android.server.blob.BlobStoreConfig.getMaxLeasedBlobs; +import static com.android.server.blob.BlobStoreSession.STATE_ABANDONED; +import static com.android.server.blob.BlobStoreSession.STATE_COMMITTED; +import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_INVALID; +import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_VALID; +import static com.android.server.blob.BlobStoreSession.stateToString; +import static com.android.server.blob.BlobStoreUtils.getDescriptionResourceId; +import static com.android.server.blob.BlobStoreUtils.getPackageResources; + +import android.annotation.CurrentTimeSecondsLong; +import android.annotation.IdRes; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.app.StatsManager; +import android.app.blob.BlobHandle; +import android.app.blob.BlobInfo; +import android.app.blob.IBlobStoreManager; +import android.app.blob.IBlobStoreSession; +import android.app.blob.LeaseInfo; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.content.pm.PackageStats; +import android.content.res.ResourceId; +import android.content.res.Resources; +import android.os.Binder; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.LimitExceededException; +import android.os.ParcelFileDescriptor; +import android.os.ParcelableException; +import android.os.Process; +import android.os.RemoteCallback; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.UserManagerInternal; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.ExceptionUtils; +import android.util.LongSparseArray; +import android.util.Slog; +import android.util.SparseArray; +import android.util.StatsEvent; +import android.util.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.CollectionUtils; +import com.android.internal.util.DumpUtils; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.Preconditions; +import com.android.internal.util.XmlUtils; +import com.android.internal.util.function.pooled.PooledLambda; +import com.android.server.LocalServices; +import com.android.server.ServiceThread; +import com.android.server.SystemService; +import com.android.server.Watchdog; +import com.android.server.blob.BlobMetadata.Committer; +import com.android.server.usage.StorageStatsManagerInternal; +import com.android.server.usage.StorageStatsManagerInternal.StorageStatsAugmenter; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.ref.WeakReference; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Service responsible for maintaining and facilitating access to data blobs published by apps. + */ +public class BlobStoreManagerService extends SystemService { + + private final Object mBlobsLock = new Object(); + + // Contains data of userId -> {sessionId -> {BlobStoreSession}}. + @GuardedBy("mBlobsLock") + private final SparseArray<LongSparseArray<BlobStoreSession>> mSessions = new SparseArray<>(); + + @GuardedBy("mBlobsLock") + private long mCurrentMaxSessionId; + + // Contains data of userId -> {BlobHandle -> {BlobMetadata}} + @GuardedBy("mBlobsLock") + private final SparseArray<ArrayMap<BlobHandle, BlobMetadata>> mBlobsMap = new SparseArray<>(); + + // Contains all ids that are currently in use. + @GuardedBy("mBlobsLock") + private final ArraySet<Long> mActiveBlobIds = new ArraySet<>(); + // Contains all ids that are currently in use and those that were in use but got deleted in the + // current boot session. + @GuardedBy("mBlobsLock") + private final ArraySet<Long> mKnownBlobIds = new ArraySet<>(); + + // Random number generator for new session ids. + private final Random mRandom = new SecureRandom(); + + private final Context mContext; + private final Handler mHandler; + private final Handler mBackgroundHandler; + private final Injector mInjector; + private final SessionStateChangeListener mSessionStateChangeListener = + new SessionStateChangeListener(); + + private PackageManagerInternal mPackageManagerInternal; + private StatsManager mStatsManager; + private StatsPullAtomCallbackImpl mStatsCallbackImpl = new StatsPullAtomCallbackImpl(); + + private final Runnable mSaveBlobsInfoRunnable = this::writeBlobsInfo; + private final Runnable mSaveSessionsRunnable = this::writeBlobSessions; + + public BlobStoreManagerService(Context context) { + this(context, new Injector()); + } + + @VisibleForTesting + BlobStoreManagerService(Context context, Injector injector) { + super(context); + + mContext = context; + mInjector = injector; + mHandler = mInjector.initializeMessageHandler(); + mBackgroundHandler = mInjector.getBackgroundHandler(); + } + + private static Handler initializeMessageHandler() { + final HandlerThread handlerThread = new ServiceThread(TAG, + Process.THREAD_PRIORITY_DEFAULT, true /* allowIo */); + handlerThread.start(); + final Handler handler = new Handler(handlerThread.getLooper()); + Watchdog.getInstance().addThread(handler); + return handler; + } + + @Override + public void onStart() { + publishBinderService(Context.BLOB_STORE_SERVICE, new Stub()); + LocalServices.addService(BlobStoreManagerInternal.class, new LocalService()); + + mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); + mStatsManager = getContext().getSystemService(StatsManager.class); + registerReceivers(); + LocalServices.getService(StorageStatsManagerInternal.class) + .registerStorageStatsAugmenter(new BlobStorageStatsAugmenter(), TAG); + } + + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_ACTIVITY_MANAGER_READY) { + BlobStoreConfig.initialize(mContext); + } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + synchronized (mBlobsLock) { + final SparseArray<SparseArray<String>> allPackages = getAllPackages(); + readBlobSessionsLocked(allPackages); + readBlobsInfoLocked(allPackages); + } + registerBlobStorePuller(); + } else if (phase == PHASE_BOOT_COMPLETED) { + BlobStoreIdleJobService.schedule(mContext); + } + } + + @GuardedBy("mBlobsLock") + private long generateNextSessionIdLocked() { + // Logic borrowed from PackageInstallerService. + int n = 0; + long sessionId; + do { + final long randomLong = mRandom.nextLong(); + sessionId = (randomLong == Long.MIN_VALUE) ? INVALID_BLOB_ID : Math.abs(randomLong); + if (mKnownBlobIds.indexOf(sessionId) < 0 && sessionId != INVALID_BLOB_ID) { + return sessionId; + } + } while (n++ < 32); + throw new IllegalStateException("Failed to allocate session ID"); + } + + private void registerReceivers() { + final IntentFilter packageChangedFilter = new IntentFilter(); + packageChangedFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); + packageChangedFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED); + packageChangedFilter.addDataScheme("package"); + mContext.registerReceiverAsUser(new PackageChangedReceiver(), UserHandle.ALL, + packageChangedFilter, null, mHandler); + + final IntentFilter userActionFilter = new IntentFilter(); + userActionFilter.addAction(Intent.ACTION_USER_REMOVED); + mContext.registerReceiverAsUser(new UserActionReceiver(), UserHandle.ALL, + userActionFilter, null, mHandler); + } + + @GuardedBy("mBlobsLock") + private LongSparseArray<BlobStoreSession> getUserSessionsLocked(int userId) { + LongSparseArray<BlobStoreSession> userSessions = mSessions.get(userId); + if (userSessions == null) { + userSessions = new LongSparseArray<>(); + mSessions.put(userId, userSessions); + } + return userSessions; + } + + @GuardedBy("mBlobsLock") + private ArrayMap<BlobHandle, BlobMetadata> getUserBlobsLocked(int userId) { + ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.get(userId); + if (userBlobs == null) { + userBlobs = new ArrayMap<>(); + mBlobsMap.put(userId, userBlobs); + } + return userBlobs; + } + + @VisibleForTesting + void addUserSessionsForTest(LongSparseArray<BlobStoreSession> userSessions, int userId) { + synchronized (mBlobsLock) { + mSessions.put(userId, userSessions); + } + } + + @VisibleForTesting + void addUserBlobsForTest(ArrayMap<BlobHandle, BlobMetadata> userBlobs, int userId) { + synchronized (mBlobsLock) { + mBlobsMap.put(userId, userBlobs); + } + } + + @VisibleForTesting + void addActiveIdsForTest(long... activeIds) { + synchronized (mBlobsLock) { + for (long id : activeIds) { + addActiveBlobIdLocked(id); + } + } + } + + @VisibleForTesting + Set<Long> getActiveIdsForTest() { + synchronized (mBlobsLock) { + return mActiveBlobIds; + } + } + + @VisibleForTesting + Set<Long> getKnownIdsForTest() { + synchronized (mBlobsLock) { + return mKnownBlobIds; + } + } + + @GuardedBy("mBlobsLock") + private void addSessionForUserLocked(BlobStoreSession session, int userId) { + getUserSessionsLocked(userId).put(session.getSessionId(), session); + addActiveBlobIdLocked(session.getSessionId()); + } + + @GuardedBy("mBlobsLock") + private void addBlobForUserLocked(BlobMetadata blobMetadata, int userId) { + addBlobForUserLocked(blobMetadata, getUserBlobsLocked(userId)); + } + + @GuardedBy("mBlobsLock") + private void addBlobForUserLocked(BlobMetadata blobMetadata, + ArrayMap<BlobHandle, BlobMetadata> userBlobs) { + userBlobs.put(blobMetadata.getBlobHandle(), blobMetadata); + addActiveBlobIdLocked(blobMetadata.getBlobId()); + } + + @GuardedBy("mBlobsLock") + private void addActiveBlobIdLocked(long id) { + mActiveBlobIds.add(id); + mKnownBlobIds.add(id); + } + + @GuardedBy("mBlobsLock") + private int getSessionsCountLocked(int uid, String packageName) { + // TODO: Maintain a counter instead of traversing all the sessions + final AtomicInteger sessionsCount = new AtomicInteger(0); + forEachSessionInUser(session -> { + if (session.getOwnerUid() == uid && session.getOwnerPackageName().equals(packageName)) { + sessionsCount.getAndIncrement(); + } + }, UserHandle.getUserId(uid)); + return sessionsCount.get(); + } + + private long createSessionInternal(BlobHandle blobHandle, + int callingUid, String callingPackage) { + synchronized (mBlobsLock) { + final int sessionsCount = getSessionsCountLocked(callingUid, callingPackage); + if (sessionsCount >= getMaxActiveSessions()) { + throw new LimitExceededException("Too many active sessions for the caller: " + + sessionsCount); + } + // TODO: throw if there is already an active session associated with blobHandle. + final long sessionId = generateNextSessionIdLocked(); + final BlobStoreSession session = new BlobStoreSession(mContext, + sessionId, blobHandle, callingUid, callingPackage, + mSessionStateChangeListener); + addSessionForUserLocked(session, UserHandle.getUserId(callingUid)); + if (LOGV) { + Slog.v(TAG, "Created session for " + blobHandle + + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage); + } + writeBlobSessionsAsync(); + return sessionId; + } + } + + private BlobStoreSession openSessionInternal(long sessionId, + int callingUid, String callingPackage) { + final BlobStoreSession session; + synchronized (mBlobsLock) { + session = getUserSessionsLocked( + UserHandle.getUserId(callingUid)).get(sessionId); + if (session == null || !session.hasAccess(callingUid, callingPackage) + || session.isFinalized()) { + throw new SecurityException("Session not found: " + sessionId); + } + } + session.open(); + return session; + } + + private void abandonSessionInternal(long sessionId, + int callingUid, String callingPackage) { + synchronized (mBlobsLock) { + final BlobStoreSession session = openSessionInternal(sessionId, + callingUid, callingPackage); + session.open(); + session.abandon(); + if (LOGV) { + Slog.v(TAG, "Abandoned session with id " + sessionId + + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage); + } + writeBlobSessionsAsync(); + } + } + + private ParcelFileDescriptor openBlobInternal(BlobHandle blobHandle, int callingUid, + String callingPackage) throws IOException { + synchronized (mBlobsLock) { + final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid)) + .get(blobHandle); + if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller( + callingPackage, callingUid)) { + if (blobMetadata == null) { + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_OPENED, callingUid, + INVALID_BLOB_ID, INVALID_BLOB_SIZE, + FrameworkStatsLog.BLOB_OPENED__RESULT__BLOB_DNE); + } else { + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_OPENED, callingUid, + blobMetadata.getBlobId(), blobMetadata.getSize(), + FrameworkStatsLog.BLOB_LEASED__RESULT__ACCESS_NOT_ALLOWED); + } + throw new SecurityException("Caller not allowed to access " + blobHandle + + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage); + } + + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_OPENED, callingUid, + blobMetadata.getBlobId(), blobMetadata.getSize(), + FrameworkStatsLog.BLOB_OPENED__RESULT__SUCCESS); + + return blobMetadata.openForRead(callingPackage); + } + } + + @GuardedBy("mBlobsLock") + private int getCommittedBlobsCountLocked(int uid, String packageName) { + // TODO: Maintain a counter instead of traversing all the blobs + final AtomicInteger blobsCount = new AtomicInteger(0); + forEachBlobInUser((blobMetadata) -> { + if (blobMetadata.isACommitter(packageName, uid)) { + blobsCount.getAndIncrement(); + } + }, UserHandle.getUserId(uid)); + return blobsCount.get(); + } + + @GuardedBy("mBlobsLock") + private int getLeasedBlobsCountLocked(int uid, String packageName) { + // TODO: Maintain a counter instead of traversing all the blobs + final AtomicInteger blobsCount = new AtomicInteger(0); + forEachBlobInUser((blobMetadata) -> { + if (blobMetadata.isALeasee(packageName, uid)) { + blobsCount.getAndIncrement(); + } + }, UserHandle.getUserId(uid)); + return blobsCount.get(); + } + + private void acquireLeaseInternal(BlobHandle blobHandle, int descriptionResId, + CharSequence description, long leaseExpiryTimeMillis, + int callingUid, String callingPackage) { + synchronized (mBlobsLock) { + final int leasesCount = getLeasedBlobsCountLocked(callingUid, callingPackage); + if (leasesCount >= getMaxLeasedBlobs()) { + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid, + INVALID_BLOB_ID, INVALID_BLOB_SIZE, + FrameworkStatsLog.BLOB_LEASED__RESULT__COUNT_LIMIT_EXCEEDED); + throw new LimitExceededException("Too many leased blobs for the caller: " + + leasesCount); + } + final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid)) + .get(blobHandle); + if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller( + callingPackage, callingUid)) { + if (blobMetadata == null) { + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid, + INVALID_BLOB_ID, INVALID_BLOB_SIZE, + FrameworkStatsLog.BLOB_LEASED__RESULT__BLOB_DNE); + } else { + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid, + blobMetadata.getBlobId(), blobMetadata.getSize(), + FrameworkStatsLog.BLOB_LEASED__RESULT__ACCESS_NOT_ALLOWED); + } + throw new SecurityException("Caller not allowed to access " + blobHandle + + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage); + } + if (leaseExpiryTimeMillis != 0 && blobHandle.expiryTimeMillis != 0 + && leaseExpiryTimeMillis > blobHandle.expiryTimeMillis) { + + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid, + blobMetadata.getBlobId(), blobMetadata.getSize(), + FrameworkStatsLog.BLOB_LEASED__RESULT__LEASE_EXPIRY_INVALID); + throw new IllegalArgumentException( + "Lease expiry cannot be later than blobs expiry time"); + } + if (blobMetadata.getSize() + > getRemainingLeaseQuotaBytesInternal(callingUid, callingPackage)) { + + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid, + blobMetadata.getBlobId(), blobMetadata.getSize(), + FrameworkStatsLog.BLOB_LEASED__RESULT__DATA_SIZE_LIMIT_EXCEEDED); + throw new LimitExceededException("Total amount of data with an active lease" + + " is exceeding the max limit"); + } + + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_LEASED, callingUid, + blobMetadata.getBlobId(), blobMetadata.getSize(), + FrameworkStatsLog.BLOB_LEASED__RESULT__SUCCESS); + + blobMetadata.addOrReplaceLeasee(callingPackage, callingUid, + descriptionResId, description, leaseExpiryTimeMillis); + if (LOGV) { + Slog.v(TAG, "Acquired lease on " + blobHandle + + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage); + } + writeBlobsInfoAsync(); + } + } + + @VisibleForTesting + @GuardedBy("mBlobsLock") + long getTotalUsageBytesLocked(int callingUid, String callingPackage) { + final AtomicLong totalBytes = new AtomicLong(0); + forEachBlobInUser((blobMetadata) -> { + if (blobMetadata.isALeasee(callingPackage, callingUid)) { + totalBytes.getAndAdd(blobMetadata.getSize()); + } + }, UserHandle.getUserId(callingUid)); + return totalBytes.get(); + } + + private void releaseLeaseInternal(BlobHandle blobHandle, int callingUid, + String callingPackage) { + synchronized (mBlobsLock) { + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = + getUserBlobsLocked(UserHandle.getUserId(callingUid)); + final BlobMetadata blobMetadata = userBlobs.get(blobHandle); + if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller( + callingPackage, callingUid)) { + throw new SecurityException("Caller not allowed to access " + blobHandle + + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage); + } + blobMetadata.removeLeasee(callingPackage, callingUid); + if (LOGV) { + Slog.v(TAG, "Released lease on " + blobHandle + + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage); + } + if (!blobMetadata.hasValidLeases()) { + mHandler.postDelayed(() -> { + synchronized (mBlobsLock) { + // Check if blobMetadata object is still valid. If it is not, then + // it means that it was already deleted and nothing else to do here. + if (!Objects.equals(userBlobs.get(blobHandle), blobMetadata)) { + return; + } + if (blobMetadata.shouldBeDeleted(true /* respectLeaseWaitTime */)) { + deleteBlobLocked(blobMetadata); + userBlobs.remove(blobHandle); + } + writeBlobsInfoAsync(); + } + }, getDeletionOnLastLeaseDelayMs()); + } + writeBlobsInfoAsync(); + } + } + + private long getRemainingLeaseQuotaBytesInternal(int callingUid, String callingPackage) { + synchronized (mBlobsLock) { + final long remainingQuota = BlobStoreConfig.getAppDataBytesLimit() + - getTotalUsageBytesLocked(callingUid, callingPackage); + return remainingQuota > 0 ? remainingQuota : 0; + } + } + + private List<BlobInfo> queryBlobsForUserInternal(int userId) { + final ArrayList<BlobInfo> blobInfos = new ArrayList<>(); + synchronized (mBlobsLock) { + final ArrayMap<String, WeakReference<Resources>> resources = new ArrayMap<>(); + final Function<String, Resources> resourcesGetter = (packageName) -> { + final WeakReference<Resources> resourcesRef = resources.get(packageName); + Resources packageResources = resourcesRef == null ? null : resourcesRef.get(); + if (packageResources == null) { + packageResources = getPackageResources(mContext, packageName, userId); + resources.put(packageName, new WeakReference<>(packageResources)); + } + return packageResources; + }; + getUserBlobsLocked(userId).forEach((blobHandle, blobMetadata) -> { + final ArrayList<LeaseInfo> leaseInfos = new ArrayList<>(); + blobMetadata.forEachLeasee(leasee -> { + if (!leasee.isStillValid()) { + return; + } + final int descriptionResId = leasee.descriptionResEntryName == null + ? Resources.ID_NULL + : getDescriptionResourceId(resourcesGetter.apply(leasee.packageName), + leasee.descriptionResEntryName, leasee.packageName); + final long expiryTimeMs = leasee.expiryTimeMillis == 0 + ? blobHandle.getExpiryTimeMillis() : leasee.expiryTimeMillis; + leaseInfos.add(new LeaseInfo(leasee.packageName, expiryTimeMs, + descriptionResId, leasee.description)); + }); + blobInfos.add(new BlobInfo(blobMetadata.getBlobId(), + blobHandle.getExpiryTimeMillis(), blobHandle.getLabel(), + blobMetadata.getSize(), leaseInfos)); + }); + } + return blobInfos; + } + + private void deleteBlobInternal(long blobId, int callingUid) { + synchronized (mBlobsLock) { + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked( + UserHandle.getUserId(callingUid)); + userBlobs.entrySet().removeIf(entry -> { + final BlobMetadata blobMetadata = entry.getValue(); + if (blobMetadata.getBlobId() == blobId) { + deleteBlobLocked(blobMetadata); + return true; + } + return false; + }); + writeBlobsInfoAsync(); + } + } + + private List<BlobHandle> getLeasedBlobsInternal(int callingUid, + @NonNull String callingPackage) { + final ArrayList<BlobHandle> leasedBlobs = new ArrayList<>(); + forEachBlobInUser(blobMetadata -> { + if (blobMetadata.isALeasee(callingPackage, callingUid)) { + leasedBlobs.add(blobMetadata.getBlobHandle()); + } + }, UserHandle.getUserId(callingUid)); + return leasedBlobs; + } + + private LeaseInfo getLeaseInfoInternal(BlobHandle blobHandle, + int callingUid, @NonNull String callingPackage) { + synchronized (mBlobsLock) { + final BlobMetadata blobMetadata = getUserBlobsLocked(UserHandle.getUserId(callingUid)) + .get(blobHandle); + if (blobMetadata == null || !blobMetadata.isAccessAllowedForCaller( + callingPackage, callingUid)) { + throw new SecurityException("Caller not allowed to access " + blobHandle + + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage); + } + return blobMetadata.getLeaseInfo(callingPackage, callingUid); + } + } + + private void verifyCallingPackage(int callingUid, String callingPackage) { + if (mPackageManagerInternal.getPackageUid( + callingPackage, 0, UserHandle.getUserId(callingUid)) != callingUid) { + throw new SecurityException("Specified calling package [" + callingPackage + + "] does not match the calling uid " + callingUid); + } + } + + class SessionStateChangeListener { + public void onStateChanged(@NonNull BlobStoreSession session) { + mHandler.post(PooledLambda.obtainRunnable( + BlobStoreManagerService::onStateChangedInternal, + BlobStoreManagerService.this, session).recycleOnUse()); + } + } + + private void onStateChangedInternal(@NonNull BlobStoreSession session) { + switch (session.getState()) { + case STATE_ABANDONED: + case STATE_VERIFIED_INVALID: + synchronized (mBlobsLock) { + deleteSessionLocked(session); + getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid())) + .remove(session.getSessionId()); + if (LOGV) { + Slog.v(TAG, "Session is invalid; deleted " + session); + } + } + break; + case STATE_COMMITTED: + mBackgroundHandler.post(() -> { + session.computeDigest(); + mHandler.post(PooledLambda.obtainRunnable( + BlobStoreSession::verifyBlobData, session).recycleOnUse()); + }); + break; + case STATE_VERIFIED_VALID: + synchronized (mBlobsLock) { + final int committedBlobsCount = getCommittedBlobsCountLocked( + session.getOwnerUid(), session.getOwnerPackageName()); + if (committedBlobsCount >= getMaxCommittedBlobs()) { + Slog.d(TAG, "Failed to commit: too many committed blobs. count: " + + committedBlobsCount + "; blob: " + session); + session.sendCommitCallbackResult(COMMIT_RESULT_ERROR); + deleteSessionLocked(session); + getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid())) + .remove(session.getSessionId()); + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED, + session.getOwnerUid(), session.getSessionId(), session.getSize(), + FrameworkStatsLog.BLOB_COMMITTED__RESULT__COUNT_LIMIT_EXCEEDED); + break; + } + final int userId = UserHandle.getUserId(session.getOwnerUid()); + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked( + userId); + BlobMetadata blob = userBlobs.get(session.getBlobHandle()); + if (blob == null) { + blob = new BlobMetadata(mContext, session.getSessionId(), + session.getBlobHandle(), userId); + addBlobForUserLocked(blob, userBlobs); + } + final Committer existingCommitter = blob.getExistingCommitter( + session.getOwnerPackageName(), session.getOwnerUid()); + final long existingCommitTimeMs = + (existingCommitter == null) ? 0 : existingCommitter.getCommitTimeMs(); + final Committer newCommitter = new Committer(session.getOwnerPackageName(), + session.getOwnerUid(), session.getBlobAccessMode(), + getAdjustedCommitTimeMs(existingCommitTimeMs, + System.currentTimeMillis())); + blob.addOrReplaceCommitter(newCommitter); + try { + writeBlobsInfoLocked(); + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED, + session.getOwnerUid(), blob.getBlobId(), blob.getSize(), + FrameworkStatsLog.BLOB_COMMITTED__RESULT__SUCCESS); + session.sendCommitCallbackResult(COMMIT_RESULT_SUCCESS); + } catch (Exception e) { + if (existingCommitter == null) { + blob.removeCommitter(newCommitter); + } else { + blob.addOrReplaceCommitter(existingCommitter); + } + Slog.d(TAG, "Error committing the blob: " + session, e); + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED, + session.getOwnerUid(), session.getSessionId(), blob.getSize(), + FrameworkStatsLog.BLOB_COMMITTED__RESULT__ERROR_DURING_COMMIT); + session.sendCommitCallbackResult(COMMIT_RESULT_ERROR); + // If the commit fails and this blob data didn't exist before, delete it. + // But if it is a recommit, just leave it as is. + if (session.getSessionId() == blob.getBlobId()) { + deleteBlobLocked(blob); + userBlobs.remove(blob.getBlobHandle()); + } + } + // Delete redundant data from recommits. + if (session.getSessionId() != blob.getBlobId()) { + deleteSessionLocked(session); + } + getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid())) + .remove(session.getSessionId()); + if (LOGV) { + Slog.v(TAG, "Successfully committed session " + session); + } + } + break; + default: + Slog.wtf(TAG, "Invalid session state: " + + stateToString(session.getState())); + } + synchronized (mBlobsLock) { + try { + writeBlobSessionsLocked(); + } catch (Exception e) { + // already logged, ignore. + } + } + } + + @GuardedBy("mBlobsLock") + private void writeBlobSessionsLocked() throws Exception { + final AtomicFile sessionsIndexFile = prepareSessionsIndexFile(); + if (sessionsIndexFile == null) { + Slog.wtf(TAG, "Error creating sessions index file"); + return; + } + FileOutputStream fos = null; + try { + fos = sessionsIndexFile.startWrite(SystemClock.uptimeMillis()); + final XmlSerializer out = new FastXmlSerializer(); + out.setOutput(fos, StandardCharsets.UTF_8.name()); + out.startDocument(null, true); + out.startTag(null, TAG_SESSIONS); + XmlUtils.writeIntAttribute(out, ATTR_VERSION, XML_VERSION_CURRENT); + + for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) { + final LongSparseArray<BlobStoreSession> userSessions = + mSessions.valueAt(i); + for (int j = 0, sessionsCount = userSessions.size(); j < sessionsCount; ++j) { + out.startTag(null, TAG_SESSION); + userSessions.valueAt(j).writeToXml(out); + out.endTag(null, TAG_SESSION); + } + } + + out.endTag(null, TAG_SESSIONS); + out.endDocument(); + sessionsIndexFile.finishWrite(fos); + if (LOGV) { + Slog.v(TAG, "Finished persisting sessions data"); + } + } catch (Exception e) { + sessionsIndexFile.failWrite(fos); + Slog.wtf(TAG, "Error writing sessions data", e); + throw e; + } + } + + @GuardedBy("mBlobsLock") + private void readBlobSessionsLocked(SparseArray<SparseArray<String>> allPackages) { + if (!BlobStoreConfig.getBlobStoreRootDir().exists()) { + return; + } + final AtomicFile sessionsIndexFile = prepareSessionsIndexFile(); + if (sessionsIndexFile == null) { + Slog.wtf(TAG, "Error creating sessions index file"); + return; + } else if (!sessionsIndexFile.exists()) { + Slog.w(TAG, "Sessions index file not available: " + sessionsIndexFile.getBaseFile()); + return; + } + + mSessions.clear(); + try (FileInputStream fis = sessionsIndexFile.openRead()) { + final XmlPullParser in = Xml.newPullParser(); + in.setInput(fis, StandardCharsets.UTF_8.name()); + XmlUtils.beginDocument(in, TAG_SESSIONS); + final int version = XmlUtils.readIntAttribute(in, ATTR_VERSION); + while (true) { + XmlUtils.nextElement(in); + if (in.getEventType() == XmlPullParser.END_DOCUMENT) { + break; + } + + if (TAG_SESSION.equals(in.getName())) { + final BlobStoreSession session = BlobStoreSession.createFromXml( + in, version, mContext, mSessionStateChangeListener); + if (session == null) { + continue; + } + final SparseArray<String> userPackages = allPackages.get( + UserHandle.getUserId(session.getOwnerUid())); + if (userPackages != null + && session.getOwnerPackageName().equals( + userPackages.get(session.getOwnerUid()))) { + addSessionForUserLocked(session, + UserHandle.getUserId(session.getOwnerUid())); + } else { + // Unknown package or the session data does not belong to this package. + session.getSessionFile().delete(); + } + mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, session.getSessionId()); + } + } + if (LOGV) { + Slog.v(TAG, "Finished reading sessions data"); + } + } catch (Exception e) { + Slog.wtf(TAG, "Error reading sessions data", e); + } + } + + @GuardedBy("mBlobsLock") + private void writeBlobsInfoLocked() throws Exception { + final AtomicFile blobsIndexFile = prepareBlobsIndexFile(); + if (blobsIndexFile == null) { + Slog.wtf(TAG, "Error creating blobs index file"); + return; + } + FileOutputStream fos = null; + try { + fos = blobsIndexFile.startWrite(SystemClock.uptimeMillis()); + final XmlSerializer out = new FastXmlSerializer(); + out.setOutput(fos, StandardCharsets.UTF_8.name()); + out.startDocument(null, true); + out.startTag(null, TAG_BLOBS); + XmlUtils.writeIntAttribute(out, ATTR_VERSION, XML_VERSION_CURRENT); + + for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) { + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i); + for (int j = 0, blobsCount = userBlobs.size(); j < blobsCount; ++j) { + out.startTag(null, TAG_BLOB); + userBlobs.valueAt(j).writeToXml(out); + out.endTag(null, TAG_BLOB); + } + } + + out.endTag(null, TAG_BLOBS); + out.endDocument(); + blobsIndexFile.finishWrite(fos); + if (LOGV) { + Slog.v(TAG, "Finished persisting blobs data"); + } + } catch (Exception e) { + blobsIndexFile.failWrite(fos); + Slog.wtf(TAG, "Error writing blobs data", e); + throw e; + } + } + + @GuardedBy("mBlobsLock") + private void readBlobsInfoLocked(SparseArray<SparseArray<String>> allPackages) { + if (!BlobStoreConfig.getBlobStoreRootDir().exists()) { + return; + } + final AtomicFile blobsIndexFile = prepareBlobsIndexFile(); + if (blobsIndexFile == null) { + Slog.wtf(TAG, "Error creating blobs index file"); + return; + } else if (!blobsIndexFile.exists()) { + Slog.w(TAG, "Blobs index file not available: " + blobsIndexFile.getBaseFile()); + return; + } + + mBlobsMap.clear(); + try (FileInputStream fis = blobsIndexFile.openRead()) { + final XmlPullParser in = Xml.newPullParser(); + in.setInput(fis, StandardCharsets.UTF_8.name()); + XmlUtils.beginDocument(in, TAG_BLOBS); + final int version = XmlUtils.readIntAttribute(in, ATTR_VERSION); + while (true) { + XmlUtils.nextElement(in); + if (in.getEventType() == XmlPullParser.END_DOCUMENT) { + break; + } + + if (TAG_BLOB.equals(in.getName())) { + final BlobMetadata blobMetadata = BlobMetadata.createFromXml( + in, version, mContext); + final SparseArray<String> userPackages = allPackages.get( + blobMetadata.getUserId()); + if (userPackages == null) { + blobMetadata.getBlobFile().delete(); + } else { + addBlobForUserLocked(blobMetadata, blobMetadata.getUserId()); + blobMetadata.removeCommittersFromUnknownPkgs(userPackages); + blobMetadata.removeLeaseesFromUnknownPkgs(userPackages); + } + mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, blobMetadata.getBlobId()); + } + } + if (LOGV) { + Slog.v(TAG, "Finished reading blobs data"); + } + } catch (Exception e) { + Slog.wtf(TAG, "Error reading blobs data", e); + } + } + + private void writeBlobsInfo() { + synchronized (mBlobsLock) { + try { + writeBlobsInfoLocked(); + } catch (Exception e) { + // Already logged, ignore + } + } + } + + private void writeBlobsInfoAsync() { + if (!mHandler.hasCallbacks(mSaveBlobsInfoRunnable)) { + mHandler.post(mSaveBlobsInfoRunnable); + } + } + + private void writeBlobSessions() { + synchronized (mBlobsLock) { + try { + writeBlobSessionsLocked(); + } catch (Exception e) { + // Already logged, ignore + } + } + } + + private void writeBlobSessionsAsync() { + if (!mHandler.hasCallbacks(mSaveSessionsRunnable)) { + mHandler.post(mSaveSessionsRunnable); + } + } + + private int getPackageUid(String packageName, int userId) { + final int uid = mPackageManagerInternal.getPackageUid( + packageName, + MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE | MATCH_UNINSTALLED_PACKAGES, + userId); + return uid; + } + + private SparseArray<SparseArray<String>> getAllPackages() { + final SparseArray<SparseArray<String>> allPackages = new SparseArray<>(); + final int[] allUsers = LocalServices.getService(UserManagerInternal.class).getUserIds(); + for (int userId : allUsers) { + final SparseArray<String> userPackages = new SparseArray<>(); + allPackages.put(userId, userPackages); + final List<ApplicationInfo> applicationInfos = mPackageManagerInternal + .getInstalledApplications( + MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE + | MATCH_UNINSTALLED_PACKAGES, + userId, Process.myUid()); + for (int i = 0, count = applicationInfos.size(); i < count; ++i) { + final ApplicationInfo applicationInfo = applicationInfos.get(i); + userPackages.put(applicationInfo.uid, applicationInfo.packageName); + } + } + return allPackages; + } + + AtomicFile prepareSessionsIndexFile() { + final File file = BlobStoreConfig.prepareSessionIndexFile(); + if (file == null) { + return null; + } + return new AtomicFile(file, "session_index" /* commitLogTag */); + } + + AtomicFile prepareBlobsIndexFile() { + final File file = BlobStoreConfig.prepareBlobsIndexFile(); + if (file == null) { + return null; + } + return new AtomicFile(file, "blobs_index" /* commitLogTag */); + } + + @VisibleForTesting + void handlePackageRemoved(String packageName, int uid) { + synchronized (mBlobsLock) { + // Clean up any pending sessions + final LongSparseArray<BlobStoreSession> userSessions = + getUserSessionsLocked(UserHandle.getUserId(uid)); + userSessions.removeIf((sessionId, blobStoreSession) -> { + if (blobStoreSession.getOwnerUid() == uid + && blobStoreSession.getOwnerPackageName().equals(packageName)) { + deleteSessionLocked(blobStoreSession); + return true; + } + return false; + }); + writeBlobSessionsAsync(); + + // Remove the package from the committer and leasee list + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = + getUserBlobsLocked(UserHandle.getUserId(uid)); + userBlobs.entrySet().removeIf(entry -> { + final BlobMetadata blobMetadata = entry.getValue(); + final boolean isACommitter = blobMetadata.isACommitter(packageName, uid); + if (isACommitter) { + blobMetadata.removeCommitter(packageName, uid); + } + blobMetadata.removeLeasee(packageName, uid); + // Regardless of when the blob is committed, we need to delete + // it if it was from the deleted package to ensure we delete all traces of it. + if (blobMetadata.shouldBeDeleted(isACommitter /* respectLeaseWaitTime */)) { + deleteBlobLocked(blobMetadata); + return true; + } + return false; + }); + writeBlobsInfoAsync(); + + if (LOGV) { + Slog.v(TAG, "Removed blobs data associated with pkg=" + + packageName + ", uid=" + uid); + } + } + } + + private void handleUserRemoved(int userId) { + synchronized (mBlobsLock) { + final LongSparseArray<BlobStoreSession> userSessions = + mSessions.removeReturnOld(userId); + if (userSessions != null) { + for (int i = 0, count = userSessions.size(); i < count; ++i) { + final BlobStoreSession session = userSessions.valueAt(i); + deleteSessionLocked(session); + } + } + + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = + mBlobsMap.removeReturnOld(userId); + if (userBlobs != null) { + for (int i = 0, count = userBlobs.size(); i < count; ++i) { + final BlobMetadata blobMetadata = userBlobs.valueAt(i); + deleteBlobLocked(blobMetadata); + } + } + if (LOGV) { + Slog.v(TAG, "Removed blobs data in user " + userId); + } + } + } + + @GuardedBy("mBlobsLock") + @VisibleForTesting + void handleIdleMaintenanceLocked() { + // Cleanup any left over data on disk that is not part of index. + final ArrayList<Long> deletedBlobIds = new ArrayList<>(); + final ArrayList<File> filesToDelete = new ArrayList<>(); + final File blobsDir = BlobStoreConfig.getBlobsDir(); + if (blobsDir.exists()) { + for (File file : blobsDir.listFiles()) { + try { + final long id = Long.parseLong(file.getName()); + if (mActiveBlobIds.indexOf(id) < 0) { + filesToDelete.add(file); + deletedBlobIds.add(id); + } + } catch (NumberFormatException e) { + Slog.wtf(TAG, "Error parsing the file name: " + file, e); + filesToDelete.add(file); + } + } + for (int i = 0, count = filesToDelete.size(); i < count; ++i) { + filesToDelete.get(i).delete(); + } + } + + // Cleanup any stale blobs. + for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) { + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i); + userBlobs.entrySet().removeIf(entry -> { + final BlobMetadata blobMetadata = entry.getValue(); + + // Remove expired leases + blobMetadata.removeExpiredLeases(); + + if (blobMetadata.shouldBeDeleted(true /* respectLeaseWaitTime */)) { + deleteBlobLocked(blobMetadata); + deletedBlobIds.add(blobMetadata.getBlobId()); + return true; + } + return false; + }); + } + writeBlobsInfoAsync(); + + // Cleanup any stale sessions. + for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) { + final LongSparseArray<BlobStoreSession> userSessions = mSessions.valueAt(i); + userSessions.removeIf((sessionId, blobStoreSession) -> { + boolean shouldRemove = false; + + // Cleanup sessions which haven't been modified in a while. + if (blobStoreSession.isExpired()) { + shouldRemove = true; + } + + // Cleanup sessions with already expired data. + if (blobStoreSession.getBlobHandle().isExpired()) { + shouldRemove = true; + } + + if (shouldRemove) { + deleteSessionLocked(blobStoreSession); + deletedBlobIds.add(blobStoreSession.getSessionId()); + } + return shouldRemove; + }); + } + Slog.d(TAG, "Completed idle maintenance; deleted " + + Arrays.toString(deletedBlobIds.toArray())); + writeBlobSessionsAsync(); + } + + @GuardedBy("mBlobsLock") + private void deleteSessionLocked(BlobStoreSession blobStoreSession) { + blobStoreSession.destroy(); + mActiveBlobIds.remove(blobStoreSession.getSessionId()); + } + + @GuardedBy("mBlobsLock") + private void deleteBlobLocked(BlobMetadata blobMetadata) { + blobMetadata.destroy(); + mActiveBlobIds.remove(blobMetadata.getBlobId()); + } + + void runClearAllSessions(@UserIdInt int userId) { + synchronized (mBlobsLock) { + for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) { + final int sessionUserId = mSessions.keyAt(i); + if (userId != UserHandle.USER_ALL && userId != sessionUserId) { + continue; + } + final LongSparseArray<BlobStoreSession> userSessions = mSessions.valueAt(i); + for (int j = 0, sessionsCount = userSessions.size(); j < sessionsCount; ++j) { + mActiveBlobIds.remove(userSessions.valueAt(j).getSessionId()); + } + } + if (userId == UserHandle.USER_ALL) { + mSessions.clear(); + } else { + mSessions.remove(userId); + } + writeBlobSessionsAsync(); + } + } + + void runClearAllBlobs(@UserIdInt int userId) { + synchronized (mBlobsLock) { + for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) { + final int blobUserId = mBlobsMap.keyAt(i); + if (userId != UserHandle.USER_ALL && userId != blobUserId) { + continue; + } + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i); + for (int j = 0, blobsCount = userBlobs.size(); j < blobsCount; ++j) { + mActiveBlobIds.remove(userBlobs.valueAt(j).getBlobId()); + } + } + if (userId == UserHandle.USER_ALL) { + mBlobsMap.clear(); + } else { + mBlobsMap.remove(userId); + } + writeBlobsInfoAsync(); + } + } + + void deleteBlob(@NonNull BlobHandle blobHandle, @UserIdInt int userId) { + synchronized (mBlobsLock) { + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked(userId); + final BlobMetadata blobMetadata = userBlobs.get(blobHandle); + if (blobMetadata == null) { + return; + } + deleteBlobLocked(blobMetadata); + userBlobs.remove(blobHandle); + writeBlobsInfoAsync(); + } + } + + void runIdleMaintenance() { + synchronized (mBlobsLock) { + handleIdleMaintenanceLocked(); + } + } + + boolean isBlobAvailable(long blobId, int userId) { + synchronized (mBlobsLock) { + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked(userId); + for (BlobMetadata blobMetadata : userBlobs.values()) { + if (blobMetadata.getBlobId() == blobId) { + return true; + } + } + return false; + } + } + + @GuardedBy("mBlobsLock") + private void dumpSessionsLocked(IndentingPrintWriter fout, DumpArgs dumpArgs) { + for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) { + final int userId = mSessions.keyAt(i); + if (!dumpArgs.shouldDumpUser(userId)) { + continue; + } + final LongSparseArray<BlobStoreSession> userSessions = mSessions.valueAt(i); + fout.println("List of sessions in user #" + + userId + " (" + userSessions.size() + "):"); + fout.increaseIndent(); + for (int j = 0, sessionsCount = userSessions.size(); j < sessionsCount; ++j) { + final long sessionId = userSessions.keyAt(j); + final BlobStoreSession session = userSessions.valueAt(j); + if (!dumpArgs.shouldDumpSession(session.getOwnerPackageName(), + session.getOwnerUid(), session.getSessionId())) { + continue; + } + fout.println("Session #" + sessionId); + fout.increaseIndent(); + session.dump(fout, dumpArgs); + fout.decreaseIndent(); + } + fout.decreaseIndent(); + } + } + + @GuardedBy("mBlobsLock") + private void dumpBlobsLocked(IndentingPrintWriter fout, DumpArgs dumpArgs) { + for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) { + final int userId = mBlobsMap.keyAt(i); + if (!dumpArgs.shouldDumpUser(userId)) { + continue; + } + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i); + fout.println("List of blobs in user #" + + userId + " (" + userBlobs.size() + "):"); + fout.increaseIndent(); + for (int j = 0, blobsCount = userBlobs.size(); j < blobsCount; ++j) { + final BlobMetadata blobMetadata = userBlobs.valueAt(j); + if (!dumpArgs.shouldDumpBlob(blobMetadata.getBlobId())) { + continue; + } + fout.println("Blob #" + blobMetadata.getBlobId()); + fout.increaseIndent(); + blobMetadata.dump(fout, dumpArgs); + fout.decreaseIndent(); + } + fout.decreaseIndent(); + } + } + + private class BlobStorageStatsAugmenter implements StorageStatsAugmenter { + @Override + public void augmentStatsForPackage(@NonNull PackageStats stats, @NonNull String packageName, + @UserIdInt int userId, boolean callerHasStatsPermission) { + final AtomicLong blobsDataSize = new AtomicLong(0); + forEachSessionInUser(session -> { + if (session.getOwnerPackageName().equals(packageName)) { + blobsDataSize.getAndAdd(session.getSize()); + } + }, userId); + + forEachBlobInUser(blobMetadata -> { + if (blobMetadata.isALeasee(packageName)) { + if (!blobMetadata.hasOtherLeasees(packageName) || !callerHasStatsPermission) { + blobsDataSize.getAndAdd(blobMetadata.getSize()); + } + } + }, userId); + + stats.dataSize += blobsDataSize.get(); + } + + @Override + public void augmentStatsForUid(@NonNull PackageStats stats, int uid, + boolean callerHasStatsPermission) { + final int userId = UserHandle.getUserId(uid); + final AtomicLong blobsDataSize = new AtomicLong(0); + forEachSessionInUser(session -> { + if (session.getOwnerUid() == uid) { + blobsDataSize.getAndAdd(session.getSize()); + } + }, userId); + + forEachBlobInUser(blobMetadata -> { + if (blobMetadata.isALeasee(uid)) { + if (!blobMetadata.hasOtherLeasees(uid) || !callerHasStatsPermission) { + blobsDataSize.getAndAdd(blobMetadata.getSize()); + } + } + }, userId); + + stats.dataSize += blobsDataSize.get(); + } + } + + private void forEachSessionInUser(Consumer<BlobStoreSession> consumer, int userId) { + synchronized (mBlobsLock) { + final LongSparseArray<BlobStoreSession> userSessions = getUserSessionsLocked(userId); + for (int i = 0, count = userSessions.size(); i < count; ++i) { + final BlobStoreSession session = userSessions.valueAt(i); + consumer.accept(session); + } + } + } + + private void forEachBlobInUser(Consumer<BlobMetadata> consumer, int userId) { + synchronized (mBlobsLock) { + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked(userId); + for (int i = 0, count = userBlobs.size(); i < count; ++i) { + final BlobMetadata blobMetadata = userBlobs.valueAt(i); + consumer.accept(blobMetadata); + } + } + } + + private class PackageChangedReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (LOGV) { + Slog.v(TAG, "Received " + intent); + } + switch (intent.getAction()) { + case Intent.ACTION_PACKAGE_FULLY_REMOVED: + case Intent.ACTION_PACKAGE_DATA_CLEARED: + final String packageName = intent.getData().getSchemeSpecificPart(); + if (packageName == null) { + Slog.wtf(TAG, "Package name is missing in the intent: " + intent); + return; + } + final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); + if (uid == -1) { + Slog.wtf(TAG, "uid is missing in the intent: " + intent); + return; + } + handlePackageRemoved(packageName, uid); + break; + default: + Slog.wtf(TAG, "Received unknown intent: " + intent); + } + } + } + + private class UserActionReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (LOGV) { + Slog.v(TAG, "Received: " + intent); + } + switch (intent.getAction()) { + case Intent.ACTION_USER_REMOVED: + final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, + USER_NULL); + if (userId == USER_NULL) { + Slog.wtf(TAG, "userId is missing in the intent: " + intent); + return; + } + handleUserRemoved(userId); + break; + default: + Slog.wtf(TAG, "Received unknown intent: " + intent); + } + } + } + + private class Stub extends IBlobStoreManager.Stub { + @Override + @IntRange(from = 1) + public long createSession(@NonNull BlobHandle blobHandle, + @NonNull String packageName) { + Objects.requireNonNull(blobHandle, "blobHandle must not be null"); + blobHandle.assertIsValid(); + Objects.requireNonNull(packageName, "packageName must not be null"); + + final int callingUid = Binder.getCallingUid(); + verifyCallingPackage(callingUid, packageName); + + if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp( + packageName, UserHandle.getUserId(callingUid))) { + throw new SecurityException("Caller not allowed to create session; " + + "callingUid=" + callingUid + ", callingPackage=" + packageName); + } + + try { + return createSessionInternal(blobHandle, callingUid, packageName); + } catch (LimitExceededException e) { + throw new ParcelableException(e); + } + } + + @Override + @NonNull + public IBlobStoreSession openSession(@IntRange(from = 1) long sessionId, + @NonNull String packageName) { + Preconditions.checkArgumentPositive(sessionId, + "sessionId must be positive: " + sessionId); + Objects.requireNonNull(packageName, "packageName must not be null"); + + final int callingUid = Binder.getCallingUid(); + verifyCallingPackage(callingUid, packageName); + + return openSessionInternal(sessionId, callingUid, packageName); + } + + @Override + public void abandonSession(@IntRange(from = 1) long sessionId, + @NonNull String packageName) { + Preconditions.checkArgumentPositive(sessionId, + "sessionId must be positive: " + sessionId); + Objects.requireNonNull(packageName, "packageName must not be null"); + + final int callingUid = Binder.getCallingUid(); + verifyCallingPackage(callingUid, packageName); + + abandonSessionInternal(sessionId, callingUid, packageName); + } + + @Override + public ParcelFileDescriptor openBlob(@NonNull BlobHandle blobHandle, + @NonNull String packageName) { + Objects.requireNonNull(blobHandle, "blobHandle must not be null"); + blobHandle.assertIsValid(); + Objects.requireNonNull(packageName, "packageName must not be null"); + + final int callingUid = Binder.getCallingUid(); + verifyCallingPackage(callingUid, packageName); + + if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp( + packageName, UserHandle.getUserId(callingUid))) { + throw new SecurityException("Caller not allowed to open blob; " + + "callingUid=" + callingUid + ", callingPackage=" + packageName); + } + + try { + return openBlobInternal(blobHandle, callingUid, packageName); + } catch (IOException e) { + throw ExceptionUtils.wrap(e); + } + } + + @Override + public void acquireLease(@NonNull BlobHandle blobHandle, @IdRes int descriptionResId, + @Nullable CharSequence description, + @CurrentTimeSecondsLong long leaseExpiryTimeMillis, @NonNull String packageName) { + Objects.requireNonNull(blobHandle, "blobHandle must not be null"); + blobHandle.assertIsValid(); + Preconditions.checkArgument( + ResourceId.isValid(descriptionResId) || description != null, + "Description must be valid; descriptionId=" + descriptionResId + + ", description=" + description); + Preconditions.checkArgumentNonnegative(leaseExpiryTimeMillis, + "leaseExpiryTimeMillis must not be negative"); + Objects.requireNonNull(packageName, "packageName must not be null"); + + description = BlobStoreConfig.getTruncatedLeaseDescription(description); + + final int callingUid = Binder.getCallingUid(); + verifyCallingPackage(callingUid, packageName); + + if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp( + packageName, UserHandle.getUserId(callingUid))) { + throw new SecurityException("Caller not allowed to open blob; " + + "callingUid=" + callingUid + ", callingPackage=" + packageName); + } + + try { + acquireLeaseInternal(blobHandle, descriptionResId, description, + leaseExpiryTimeMillis, callingUid, packageName); + } catch (Resources.NotFoundException e) { + throw new IllegalArgumentException(e); + } catch (LimitExceededException e) { + throw new ParcelableException(e); + } + } + + @Override + public void releaseLease(@NonNull BlobHandle blobHandle, @NonNull String packageName) { + Objects.requireNonNull(blobHandle, "blobHandle must not be null"); + blobHandle.assertIsValid(); + Objects.requireNonNull(packageName, "packageName must not be null"); + + final int callingUid = Binder.getCallingUid(); + verifyCallingPackage(callingUid, packageName); + + if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp( + packageName, UserHandle.getUserId(callingUid))) { + throw new SecurityException("Caller not allowed to open blob; " + + "callingUid=" + callingUid + ", callingPackage=" + packageName); + } + + releaseLeaseInternal(blobHandle, callingUid, packageName); + } + + @Override + public long getRemainingLeaseQuotaBytes(@NonNull String packageName) { + final int callingUid = Binder.getCallingUid(); + verifyCallingPackage(callingUid, packageName); + + return getRemainingLeaseQuotaBytesInternal(callingUid, packageName); + } + + @Override + public void waitForIdle(@NonNull RemoteCallback remoteCallback) { + Objects.requireNonNull(remoteCallback, "remoteCallback must not be null"); + + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, + "Caller is not allowed to call this; caller=" + Binder.getCallingUid()); + // We post messages back and forth between mHandler thread and mBackgroundHandler + // thread while committing a blob. We need to replicate the same pattern here to + // ensure pending messages have been handled. + mHandler.post(() -> { + mBackgroundHandler.post(() -> { + mHandler.post(PooledLambda.obtainRunnable(remoteCallback::sendResult, null) + .recycleOnUse()); + }); + }); + } + + @Override + @NonNull + public List<BlobInfo> queryBlobsForUser(@UserIdInt int userId) { + if (Binder.getCallingUid() != Process.SYSTEM_UID) { + throw new SecurityException("Only system uid is allowed to call " + + "queryBlobsForUser()"); + } + + final int resolvedUserId = userId == USER_CURRENT + ? ActivityManager.getCurrentUser() : userId; + // Don't allow any other special user ids apart from USER_CURRENT + final ActivityManagerInternal amInternal = LocalServices.getService( + ActivityManagerInternal.class); + amInternal.ensureNotSpecialUser(resolvedUserId); + + return queryBlobsForUserInternal(resolvedUserId); + } + + @Override + public void deleteBlob(long blobId) { + final int callingUid = Binder.getCallingUid(); + if (callingUid != Process.SYSTEM_UID) { + throw new SecurityException("Only system uid is allowed to call " + + "deleteBlob()"); + } + + deleteBlobInternal(blobId, callingUid); + } + + @Override + @NonNull + public List<BlobHandle> getLeasedBlobs(@NonNull String packageName) { + Objects.requireNonNull(packageName, "packageName must not be null"); + + final int callingUid = Binder.getCallingUid(); + verifyCallingPackage(callingUid, packageName); + + return getLeasedBlobsInternal(callingUid, packageName); + } + + @Override + @Nullable + public LeaseInfo getLeaseInfo(@NonNull BlobHandle blobHandle, @NonNull String packageName) { + Objects.requireNonNull(blobHandle, "blobHandle must not be null"); + blobHandle.assertIsValid(); + Objects.requireNonNull(packageName, "packageName must not be null"); + + final int callingUid = Binder.getCallingUid(); + verifyCallingPackage(callingUid, packageName); + + if (Process.isIsolated(callingUid) || mPackageManagerInternal.isInstantApp( + packageName, UserHandle.getUserId(callingUid))) { + throw new SecurityException("Caller not allowed to open blob; " + + "callingUid=" + callingUid + ", callingPackage=" + packageName); + } + + return getLeaseInfoInternal(blobHandle, callingUid, packageName); + } + + @Override + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, + @Nullable String[] args) { + // TODO: add proto-based version of this. + if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, writer)) return; + + final DumpArgs dumpArgs = DumpArgs.parse(args); + + final IndentingPrintWriter fout = new IndentingPrintWriter(writer, " "); + if (dumpArgs.shouldDumpHelp()) { + writer.println("dumpsys blob_store [options]:"); + fout.increaseIndent(); + dumpArgs.dumpArgsUsage(fout); + fout.decreaseIndent(); + return; + } + + synchronized (mBlobsLock) { + if (dumpArgs.shouldDumpAllSections()) { + fout.println("mCurrentMaxSessionId: " + mCurrentMaxSessionId); + fout.println(); + } + + if (dumpArgs.shouldDumpSessions()) { + dumpSessionsLocked(fout, dumpArgs); + fout.println(); + } + if (dumpArgs.shouldDumpBlobs()) { + dumpBlobsLocked(fout, dumpArgs); + fout.println(); + } + } + + if (dumpArgs.shouldDumpConfig()) { + fout.println("BlobStore config:"); + fout.increaseIndent(); + BlobStoreConfig.dump(fout, mContext); + fout.decreaseIndent(); + fout.println(); + } + } + + @Override + public int handleShellCommand(@NonNull ParcelFileDescriptor in, + @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, + @NonNull String[] args) { + return new BlobStoreManagerShellCommand(BlobStoreManagerService.this).exec(this, + in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args); + } + } + + static final class DumpArgs { + private static final int FLAG_DUMP_SESSIONS = 1 << 0; + private static final int FLAG_DUMP_BLOBS = 1 << 1; + private static final int FLAG_DUMP_CONFIG = 1 << 2; + + private int mSelectedSectionFlags; + private boolean mDumpUnredacted; + private final ArrayList<String> mDumpPackages = new ArrayList<>(); + private final ArrayList<Integer> mDumpUids = new ArrayList<>(); + private final ArrayList<Integer> mDumpUserIds = new ArrayList<>(); + private final ArrayList<Long> mDumpBlobIds = new ArrayList<>(); + private boolean mDumpHelp; + private boolean mDumpAll; + + public boolean shouldDumpSession(String packageName, int uid, long blobId) { + if (!CollectionUtils.isEmpty(mDumpPackages) + && mDumpPackages.indexOf(packageName) < 0) { + return false; + } + if (!CollectionUtils.isEmpty(mDumpUids) + && mDumpUids.indexOf(uid) < 0) { + return false; + } + if (!CollectionUtils.isEmpty(mDumpBlobIds) + && mDumpBlobIds.indexOf(blobId) < 0) { + return false; + } + return true; + } + + public boolean shouldDumpAllSections() { + return mDumpAll || (mSelectedSectionFlags == 0); + } + + public void allowDumpSessions() { + mSelectedSectionFlags |= FLAG_DUMP_SESSIONS; + } + + public boolean shouldDumpSessions() { + if (shouldDumpAllSections()) { + return true; + } + return (mSelectedSectionFlags & FLAG_DUMP_SESSIONS) != 0; + } + + public void allowDumpBlobs() { + mSelectedSectionFlags |= FLAG_DUMP_BLOBS; + } + + public boolean shouldDumpBlobs() { + if (shouldDumpAllSections()) { + return true; + } + return (mSelectedSectionFlags & FLAG_DUMP_BLOBS) != 0; + } + + public void allowDumpConfig() { + mSelectedSectionFlags |= FLAG_DUMP_CONFIG; + } + + public boolean shouldDumpConfig() { + if (shouldDumpAllSections()) { + return true; + } + return (mSelectedSectionFlags & FLAG_DUMP_CONFIG) != 0; + } + + public boolean shouldDumpBlob(long blobId) { + return CollectionUtils.isEmpty(mDumpBlobIds) + || mDumpBlobIds.indexOf(blobId) >= 0; + } + + public boolean shouldDumpFull() { + return mDumpUnredacted; + } + + public boolean shouldDumpUser(int userId) { + return CollectionUtils.isEmpty(mDumpUserIds) + || mDumpUserIds.indexOf(userId) >= 0; + } + + public boolean shouldDumpHelp() { + return mDumpHelp; + } + + private DumpArgs() {} + + public static DumpArgs parse(String[] args) { + final DumpArgs dumpArgs = new DumpArgs(); + if (args == null) { + return dumpArgs; + } + + for (int i = 0; i < args.length; ++i) { + final String opt = args[i]; + if ("--all".equals(opt) || "-a".equals(opt)) { + dumpArgs.mDumpAll = true; + } else if ("--unredacted".equals(opt) || "-u".equals(opt)) { + final int callingUid = Binder.getCallingUid(); + if (callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID) { + dumpArgs.mDumpUnredacted = true; + } + } else if ("--sessions".equals(opt)) { + dumpArgs.allowDumpSessions(); + } else if ("--blobs".equals(opt)) { + dumpArgs.allowDumpBlobs(); + } else if ("--config".equals(opt)) { + dumpArgs.allowDumpConfig(); + } else if ("--package".equals(opt) || "-p".equals(opt)) { + dumpArgs.mDumpPackages.add(getStringArgRequired(args, ++i, "packageName")); + } else if ("--uid".equals(opt)) { + dumpArgs.mDumpUids.add(getIntArgRequired(args, ++i, "uid")); + } else if ("--user".equals(opt)) { + dumpArgs.mDumpUserIds.add(getIntArgRequired(args, ++i, "userId")); + } else if ("--blob".equals(opt) || "-b".equals(opt)) { + dumpArgs.mDumpBlobIds.add(getLongArgRequired(args, ++i, "blobId")); + } else if ("--help".equals(opt) || "-h".equals(opt)) { + dumpArgs.mDumpHelp = true; + } else { + // Everything else is assumed to be blob ids. + dumpArgs.mDumpBlobIds.add(getLongArgRequired(args, i, "blobId")); + } + } + return dumpArgs; + } + + private static String getStringArgRequired(String[] args, int index, String argName) { + if (index >= args.length) { + throw new IllegalArgumentException("Missing " + argName); + } + return args[index]; + } + + private static int getIntArgRequired(String[] args, int index, String argName) { + if (index >= args.length) { + throw new IllegalArgumentException("Missing " + argName); + } + final int value; + try { + value = Integer.parseInt(args[index]); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid " + argName + ": " + args[index]); + } + return value; + } + + private static long getLongArgRequired(String[] args, int index, String argName) { + if (index >= args.length) { + throw new IllegalArgumentException("Missing " + argName); + } + final long value; + try { + value = Long.parseLong(args[index]); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid " + argName + ": " + args[index]); + } + return value; + } + + private void dumpArgsUsage(IndentingPrintWriter pw) { + pw.println("--help | -h"); + printWithIndent(pw, "Dump this help text"); + pw.println("--sessions"); + printWithIndent(pw, "Dump only the sessions info"); + pw.println("--blobs"); + printWithIndent(pw, "Dump only the committed blobs info"); + pw.println("--config"); + printWithIndent(pw, "Dump only the config values"); + pw.println("--package | -p [package-name]"); + printWithIndent(pw, "Dump blobs info associated with the given package"); + pw.println("--uid | -u [uid]"); + printWithIndent(pw, "Dump blobs info associated with the given uid"); + pw.println("--user [user-id]"); + printWithIndent(pw, "Dump blobs info in the given user"); + pw.println("--blob | -b [session-id | blob-id]"); + printWithIndent(pw, "Dump blob info corresponding to the given ID"); + pw.println("--full | -f"); + printWithIndent(pw, "Dump full unredacted blobs data"); + } + + private void printWithIndent(IndentingPrintWriter pw, String str) { + pw.increaseIndent(); + pw.println(str); + pw.decreaseIndent(); + } + } + + private void registerBlobStorePuller() { + mStatsManager.setPullAtomCallback( + FrameworkStatsLog.BLOB_INFO, + null, // use default PullAtomMetadata values + BackgroundThread.getExecutor(), + mStatsCallbackImpl + ); + } + + private class StatsPullAtomCallbackImpl implements StatsManager.StatsPullAtomCallback { + @Override + public int onPullAtom(int atomTag, List<StatsEvent> data) { + switch (atomTag) { + case FrameworkStatsLog.BLOB_INFO: + return pullBlobData(atomTag, data); + default: + throw new UnsupportedOperationException("Unknown tagId=" + atomTag); + } + } + } + + private int pullBlobData(int atomTag, List<StatsEvent> data) { + synchronized (mBlobsLock) { + for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) { + final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i); + for (int j = 0, blobsCount = userBlobs.size(); j < blobsCount; ++j) { + final BlobMetadata blob = userBlobs.valueAt(j); + data.add(blob.dumpAsStatsEvent(atomTag)); + } + } + } + return StatsManager.PULL_SUCCESS; + } + + private class LocalService extends BlobStoreManagerInternal { + @Override + public void onIdleMaintenance() { + runIdleMaintenance(); + } + } + + @VisibleForTesting + static class Injector { + public Handler initializeMessageHandler() { + return BlobStoreManagerService.initializeMessageHandler(); + } + + public Handler getBackgroundHandler() { + return BackgroundThread.getHandler(); + } + } +}
\ No newline at end of file diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java new file mode 100644 index 000000000000..a4a2e80c195a --- /dev/null +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java @@ -0,0 +1,192 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.blob; + +import android.app.ActivityManager; +import android.app.blob.BlobHandle; +import android.os.ShellCommand; +import android.os.UserHandle; + +import java.io.PrintWriter; +import java.util.Base64; + +class BlobStoreManagerShellCommand extends ShellCommand { + + private final BlobStoreManagerService mService; + + BlobStoreManagerShellCommand(BlobStoreManagerService blobStoreManagerService) { + mService = blobStoreManagerService; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(null); + } + final PrintWriter pw = getOutPrintWriter(); + switch (cmd) { + case "clear-all-sessions": + return runClearAllSessions(pw); + case "clear-all-blobs": + return runClearAllBlobs(pw); + case "delete-blob": + return runDeleteBlob(pw); + case "idle-maintenance": + return runIdleMaintenance(pw); + case "query-blob-existence": + return runQueryBlobExistence(pw); + default: + return handleDefaultCommands(cmd); + } + } + + private int runClearAllSessions(PrintWriter pw) { + final ParsedArgs args = new ParsedArgs(); + args.userId = UserHandle.USER_ALL; + + if (parseOptions(pw, args) < 0) { + return -1; + } + + mService.runClearAllSessions(args.userId); + return 0; + } + + private int runClearAllBlobs(PrintWriter pw) { + final ParsedArgs args = new ParsedArgs(); + args.userId = UserHandle.USER_ALL; + + if (parseOptions(pw, args) < 0) { + return -1; + } + + mService.runClearAllBlobs(args.userId); + return 0; + } + + private int runDeleteBlob(PrintWriter pw) { + final ParsedArgs args = new ParsedArgs(); + + if (parseOptions(pw, args) < 0) { + return -1; + } + + mService.deleteBlob(args.getBlobHandle(), args.userId); + return 0; + } + + private int runIdleMaintenance(PrintWriter pw) { + mService.runIdleMaintenance(); + return 0; + } + + private int runQueryBlobExistence(PrintWriter pw) { + final ParsedArgs args = new ParsedArgs(); + if (parseOptions(pw, args) < 0) { + return -1; + } + + pw.println(mService.isBlobAvailable(args.blobId, args.userId) ? 1 : 0); + return 0; + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutPrintWriter(); + pw.println("BlobStore service (blob_store) commands:"); + pw.println("help"); + pw.println(" Print this help text."); + pw.println(); + pw.println("clear-all-sessions [-u | --user USER_ID]"); + pw.println(" Remove all sessions."); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's sessions to be removed."); + pw.println(" If not specified, sessions in all users are removed."); + pw.println(); + pw.println("clear-all-blobs [-u | --user USER_ID]"); + pw.println(" Remove all blobs."); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's blobs to be removed."); + pw.println(" If not specified, blobs in all users are removed."); + pw.println("delete-blob [-u | --user USER_ID] [--digest DIGEST] [--expiry EXPIRY_TIME] " + + "[--label LABEL] [--tag TAG]"); + pw.println(" Delete a blob."); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's blobs to be removed;"); + pw.println(" If not specified, blobs in all users are removed."); + pw.println(" --digest: Base64 encoded digest of the blob to delete."); + pw.println(" --expiry: Expiry time of the blob to delete, in milliseconds."); + pw.println(" --label: Label of the blob to delete."); + pw.println(" --tag: Tag of the blob to delete."); + pw.println("idle-maintenance"); + pw.println(" Run idle maintenance which takes care of removing stale data."); + pw.println("query-blob-existence [-b BLOB_ID]"); + pw.println(" Prints 1 if blob exists, otherwise 0."); + pw.println(); + } + + private int parseOptions(PrintWriter pw, ParsedArgs args) { + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-u": + case "--user": + args.userId = Integer.parseInt(getNextArgRequired()); + break; + case "--algo": + args.algorithm = getNextArgRequired(); + break; + case "--digest": + args.digest = Base64.getDecoder().decode(getNextArgRequired()); + break; + case "--label": + args.label = getNextArgRequired(); + break; + case "--expiry": + args.expiryTimeMillis = Long.parseLong(getNextArgRequired()); + break; + case "--tag": + args.tag = getNextArgRequired(); + break; + case "-b": + args.blobId = Long.parseLong(getNextArgRequired()); + break; + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + if (args.userId == UserHandle.USER_CURRENT) { + args.userId = ActivityManager.getCurrentUser(); + } + return 0; + } + + private static class ParsedArgs { + public int userId = UserHandle.USER_CURRENT; + + public String algorithm = BlobHandle.ALGO_SHA_256; + public byte[] digest; + public long expiryTimeMillis; + public CharSequence label; + public String tag; + public long blobId; + + public BlobHandle getBlobHandle() { + return BlobHandle.create(algorithm, digest, label, expiryTimeMillis, tag); + } + } +} diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java new file mode 100644 index 000000000000..2f83be1e0370 --- /dev/null +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java @@ -0,0 +1,629 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.blob; + +import static android.app.blob.BlobStoreManager.COMMIT_RESULT_ERROR; +import static android.app.blob.XmlTags.ATTR_CREATION_TIME_MS; +import static android.app.blob.XmlTags.ATTR_ID; +import static android.app.blob.XmlTags.ATTR_PACKAGE; +import static android.app.blob.XmlTags.ATTR_UID; +import static android.app.blob.XmlTags.TAG_ACCESS_MODE; +import static android.app.blob.XmlTags.TAG_BLOB_HANDLE; +import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER; +import static android.system.OsConstants.O_CREAT; +import static android.system.OsConstants.O_RDONLY; +import static android.system.OsConstants.O_RDWR; +import static android.system.OsConstants.SEEK_SET; +import static android.text.format.Formatter.FLAG_IEC_UNITS; +import static android.text.format.Formatter.formatFileSize; + +import static com.android.server.blob.BlobStoreConfig.TAG; +import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_SESSION_CREATION_TIME; +import static com.android.server.blob.BlobStoreConfig.getMaxPermittedPackages; +import static com.android.server.blob.BlobStoreConfig.hasSessionExpired; + +import android.annotation.BytesLong; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.blob.BlobHandle; +import android.app.blob.IBlobCommitCallback; +import android.app.blob.IBlobStoreSession; +import android.content.Context; +import android.os.Binder; +import android.os.FileUtils; +import android.os.LimitExceededException; +import android.os.ParcelFileDescriptor; +import android.os.ParcelableException; +import android.os.RemoteException; +import android.os.RevocableFileDescriptor; +import android.os.Trace; +import android.os.storage.StorageManager; +import android.system.ErrnoException; +import android.system.Os; +import android.util.ExceptionUtils; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.Preconditions; +import com.android.internal.util.XmlUtils; +import com.android.server.blob.BlobStoreManagerService.DumpArgs; +import com.android.server.blob.BlobStoreManagerService.SessionStateChangeListener; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; + +/** + * Class to represent the state corresponding to an ongoing + * {@link android.app.blob.BlobStoreManager.Session} + */ +@VisibleForTesting +class BlobStoreSession extends IBlobStoreSession.Stub { + + static final int STATE_OPENED = 1; + static final int STATE_CLOSED = 0; + static final int STATE_ABANDONED = 2; + static final int STATE_COMMITTED = 3; + static final int STATE_VERIFIED_VALID = 4; + static final int STATE_VERIFIED_INVALID = 5; + + private final Object mSessionLock = new Object(); + + private final Context mContext; + private final SessionStateChangeListener mListener; + + private final BlobHandle mBlobHandle; + private final long mSessionId; + private final int mOwnerUid; + private final String mOwnerPackageName; + private final long mCreationTimeMs; + + // Do not access this directly, instead use getSessionFile(). + private File mSessionFile; + + @GuardedBy("mRevocableFds") + private final ArrayList<RevocableFileDescriptor> mRevocableFds = new ArrayList<>(); + + // This will be accessed from only one thread at any point of time, so no need to grab + // a lock for this. + private byte[] mDataDigest; + + @GuardedBy("mSessionLock") + private int mState = STATE_CLOSED; + + @GuardedBy("mSessionLock") + private final BlobAccessMode mBlobAccessMode = new BlobAccessMode(); + + @GuardedBy("mSessionLock") + private IBlobCommitCallback mBlobCommitCallback; + + private BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle, + int ownerUid, String ownerPackageName, long creationTimeMs, + SessionStateChangeListener listener) { + this.mContext = context; + this.mBlobHandle = blobHandle; + this.mSessionId = sessionId; + this.mOwnerUid = ownerUid; + this.mOwnerPackageName = ownerPackageName; + this.mCreationTimeMs = creationTimeMs; + this.mListener = listener; + } + + BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle, + int ownerUid, String ownerPackageName, SessionStateChangeListener listener) { + this(context, sessionId, blobHandle, ownerUid, ownerPackageName, + System.currentTimeMillis(), listener); + } + + public BlobHandle getBlobHandle() { + return mBlobHandle; + } + + public long getSessionId() { + return mSessionId; + } + + public int getOwnerUid() { + return mOwnerUid; + } + + public String getOwnerPackageName() { + return mOwnerPackageName; + } + + boolean hasAccess(int callingUid, String callingPackageName) { + return mOwnerUid == callingUid && mOwnerPackageName.equals(callingPackageName); + } + + void open() { + synchronized (mSessionLock) { + if (isFinalized()) { + throw new IllegalStateException("Not allowed to open session with state: " + + stateToString(mState)); + } + mState = STATE_OPENED; + } + } + + int getState() { + synchronized (mSessionLock) { + return mState; + } + } + + void sendCommitCallbackResult(int result) { + synchronized (mSessionLock) { + try { + mBlobCommitCallback.onResult(result); + } catch (RemoteException e) { + Slog.d(TAG, "Error sending the callback result", e); + } + mBlobCommitCallback = null; + } + } + + BlobAccessMode getBlobAccessMode() { + synchronized (mSessionLock) { + return mBlobAccessMode; + } + } + + boolean isFinalized() { + synchronized (mSessionLock) { + return mState == STATE_COMMITTED || mState == STATE_ABANDONED; + } + } + + boolean isExpired() { + final long lastModifiedTimeMs = getSessionFile().lastModified(); + return hasSessionExpired(lastModifiedTimeMs == 0 + ? mCreationTimeMs : lastModifiedTimeMs); + } + + @Override + @NonNull + public ParcelFileDescriptor openWrite(@BytesLong long offsetBytes, + @BytesLong long lengthBytes) { + Preconditions.checkArgumentNonnegative(offsetBytes, "offsetBytes must not be negative"); + + assertCallerIsOwner(); + synchronized (mSessionLock) { + if (mState != STATE_OPENED) { + throw new IllegalStateException("Not allowed to write in state: " + + stateToString(mState)); + } + } + + FileDescriptor fd = null; + try { + fd = openWriteInternal(offsetBytes, lengthBytes); + final RevocableFileDescriptor revocableFd = new RevocableFileDescriptor(mContext, fd); + synchronized (mSessionLock) { + if (mState != STATE_OPENED) { + IoUtils.closeQuietly(fd); + throw new IllegalStateException("Not allowed to write in state: " + + stateToString(mState)); + } + trackRevocableFdLocked(revocableFd); + return revocableFd.getRevocableFileDescriptor(); + } + } catch (IOException e) { + IoUtils.closeQuietly(fd); + throw ExceptionUtils.wrap(e); + } + } + + @NonNull + private FileDescriptor openWriteInternal(@BytesLong long offsetBytes, + @BytesLong long lengthBytes) throws IOException { + // TODO: Add limit on active open sessions/writes/reads + try { + final File sessionFile = getSessionFile(); + if (sessionFile == null) { + throw new IllegalStateException("Couldn't get the file for this session"); + } + final FileDescriptor fd = Os.open(sessionFile.getPath(), O_CREAT | O_RDWR, 0600); + if (offsetBytes > 0) { + final long curOffset = Os.lseek(fd, offsetBytes, SEEK_SET); + if (curOffset != offsetBytes) { + throw new IllegalStateException("Failed to seek " + offsetBytes + + "; curOffset=" + offsetBytes); + } + } + if (lengthBytes > 0) { + mContext.getSystemService(StorageManager.class).allocateBytes(fd, lengthBytes); + } + return fd; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + @Override + @NonNull + public ParcelFileDescriptor openRead() { + assertCallerIsOwner(); + synchronized (mSessionLock) { + if (mState != STATE_OPENED) { + throw new IllegalStateException("Not allowed to read in state: " + + stateToString(mState)); + } + if (!BlobStoreConfig.shouldUseRevocableFdForReads()) { + try { + return new ParcelFileDescriptor(openReadInternal()); + } catch (IOException e) { + throw ExceptionUtils.wrap(e); + } + } + } + + FileDescriptor fd = null; + try { + fd = openReadInternal(); + final RevocableFileDescriptor revocableFd = new RevocableFileDescriptor(mContext, fd); + synchronized (mSessionLock) { + if (mState != STATE_OPENED) { + IoUtils.closeQuietly(fd); + throw new IllegalStateException("Not allowed to read in state: " + + stateToString(mState)); + } + trackRevocableFdLocked(revocableFd); + return revocableFd.getRevocableFileDescriptor(); + } + } catch (IOException e) { + IoUtils.closeQuietly(fd); + throw ExceptionUtils.wrap(e); + } + } + + @NonNull + private FileDescriptor openReadInternal() throws IOException { + try { + final File sessionFile = getSessionFile(); + if (sessionFile == null) { + throw new IllegalStateException("Couldn't get the file for this session"); + } + final FileDescriptor fd = Os.open(sessionFile.getPath(), O_RDONLY, 0); + return fd; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + @Override + @BytesLong + public long getSize() { + return getSessionFile().length(); + } + + @Override + public void allowPackageAccess(@NonNull String packageName, + @NonNull byte[] certificate) { + assertCallerIsOwner(); + Objects.requireNonNull(packageName, "packageName must not be null"); + synchronized (mSessionLock) { + if (mState != STATE_OPENED) { + throw new IllegalStateException("Not allowed to change access type in state: " + + stateToString(mState)); + } + if (mBlobAccessMode.getNumWhitelistedPackages() >= getMaxPermittedPackages()) { + throw new ParcelableException(new LimitExceededException( + "Too many packages permitted to access the blob: " + + mBlobAccessMode.getNumWhitelistedPackages())); + } + mBlobAccessMode.allowPackageAccess(packageName, certificate); + } + } + + @Override + public void allowSameSignatureAccess() { + assertCallerIsOwner(); + synchronized (mSessionLock) { + if (mState != STATE_OPENED) { + throw new IllegalStateException("Not allowed to change access type in state: " + + stateToString(mState)); + } + mBlobAccessMode.allowSameSignatureAccess(); + } + } + + @Override + public void allowPublicAccess() { + assertCallerIsOwner(); + synchronized (mSessionLock) { + if (mState != STATE_OPENED) { + throw new IllegalStateException("Not allowed to change access type in state: " + + stateToString(mState)); + } + mBlobAccessMode.allowPublicAccess(); + } + } + + @Override + public boolean isPackageAccessAllowed(@NonNull String packageName, + @NonNull byte[] certificate) { + assertCallerIsOwner(); + Objects.requireNonNull(packageName, "packageName must not be null"); + Preconditions.checkByteArrayNotEmpty(certificate, "certificate"); + + synchronized (mSessionLock) { + if (mState != STATE_OPENED) { + throw new IllegalStateException("Not allowed to get access type in state: " + + stateToString(mState)); + } + return mBlobAccessMode.isPackageAccessAllowed(packageName, certificate); + } + } + + @Override + public boolean isSameSignatureAccessAllowed() { + assertCallerIsOwner(); + synchronized (mSessionLock) { + if (mState != STATE_OPENED) { + throw new IllegalStateException("Not allowed to get access type in state: " + + stateToString(mState)); + } + return mBlobAccessMode.isSameSignatureAccessAllowed(); + } + } + + @Override + public boolean isPublicAccessAllowed() { + assertCallerIsOwner(); + synchronized (mSessionLock) { + if (mState != STATE_OPENED) { + throw new IllegalStateException("Not allowed to get access type in state: " + + stateToString(mState)); + } + return mBlobAccessMode.isPublicAccessAllowed(); + } + } + + @Override + public void close() { + closeSession(STATE_CLOSED, false /* sendCallback */); + } + + @Override + public void abandon() { + closeSession(STATE_ABANDONED, true /* sendCallback */); + } + + @Override + public void commit(IBlobCommitCallback callback) { + synchronized (mSessionLock) { + mBlobCommitCallback = callback; + + closeSession(STATE_COMMITTED, true /* sendCallback */); + } + } + + private void closeSession(int state, boolean sendCallback) { + assertCallerIsOwner(); + synchronized (mSessionLock) { + if (mState != STATE_OPENED) { + if (state == STATE_CLOSED) { + // Just trying to close the session which is already deleted or abandoned, + // ignore. + return; + } else { + throw new IllegalStateException("Not allowed to delete or abandon a session" + + " with state: " + stateToString(mState)); + } + } + + mState = state; + revokeAllFds(); + + if (sendCallback) { + mListener.onStateChanged(this); + } + } + } + + void computeDigest() { + try { + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, + "computeBlobDigest-i" + mSessionId + "-l" + getSessionFile().length()); + mDataDigest = FileUtils.digest(getSessionFile(), mBlobHandle.algorithm); + } catch (IOException | NoSuchAlgorithmException e) { + Slog.e(TAG, "Error computing the digest", e); + } finally { + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); + } + } + + void verifyBlobData() { + synchronized (mSessionLock) { + if (mDataDigest != null && Arrays.equals(mDataDigest, mBlobHandle.digest)) { + mState = STATE_VERIFIED_VALID; + // Commit callback will be sent once the data is persisted. + } else { + Slog.d(TAG, "Digest of the data (" + + (mDataDigest == null ? "null" : BlobHandle.safeDigest(mDataDigest)) + + ") didn't match the given BlobHandle.digest (" + + BlobHandle.safeDigest(mBlobHandle.digest) + ")"); + mState = STATE_VERIFIED_INVALID; + + FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED, getOwnerUid(), mSessionId, + getSize(), FrameworkStatsLog.BLOB_COMMITTED__RESULT__DIGEST_MISMATCH); + sendCommitCallbackResult(COMMIT_RESULT_ERROR); + } + mListener.onStateChanged(this); + } + } + + void destroy() { + revokeAllFds(); + getSessionFile().delete(); + } + + private void revokeAllFds() { + synchronized (mRevocableFds) { + for (int i = mRevocableFds.size() - 1; i >= 0; --i) { + mRevocableFds.get(i).revoke(); + } + mRevocableFds.clear(); + } + } + + @GuardedBy("mSessionLock") + private void trackRevocableFdLocked(RevocableFileDescriptor revocableFd) { + synchronized (mRevocableFds) { + mRevocableFds.add(revocableFd); + } + revocableFd.addOnCloseListener((e) -> { + synchronized (mRevocableFds) { + mRevocableFds.remove(revocableFd); + } + }); + } + + @Nullable + File getSessionFile() { + if (mSessionFile == null) { + mSessionFile = BlobStoreConfig.prepareBlobFile(mSessionId); + } + return mSessionFile; + } + + @NonNull + static String stateToString(int state) { + switch (state) { + case STATE_OPENED: + return "<opened>"; + case STATE_CLOSED: + return "<closed>"; + case STATE_ABANDONED: + return "<abandoned>"; + case STATE_COMMITTED: + return "<committed>"; + case STATE_VERIFIED_VALID: + return "<verified_valid>"; + case STATE_VERIFIED_INVALID: + return "<verified_invalid>"; + default: + Slog.wtf(TAG, "Unknown state: " + state); + return "<unknown>"; + } + } + + @Override + public String toString() { + return "BlobStoreSession {" + + "id:" + mSessionId + + ",handle:" + mBlobHandle + + ",uid:" + mOwnerUid + + ",pkg:" + mOwnerPackageName + + "}"; + } + + private void assertCallerIsOwner() { + final int callingUid = Binder.getCallingUid(); + if (callingUid != mOwnerUid) { + throw new SecurityException(mOwnerUid + " is not the session owner"); + } + } + + void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) { + synchronized (mSessionLock) { + fout.println("state: " + stateToString(mState)); + fout.println("ownerUid: " + mOwnerUid); + fout.println("ownerPkg: " + mOwnerPackageName); + fout.println("creation time: " + BlobStoreUtils.formatTime(mCreationTimeMs)); + fout.println("size: " + formatFileSize(mContext, getSize(), FLAG_IEC_UNITS)); + + fout.println("blobHandle:"); + fout.increaseIndent(); + mBlobHandle.dump(fout, dumpArgs.shouldDumpFull()); + fout.decreaseIndent(); + + fout.println("accessMode:"); + fout.increaseIndent(); + mBlobAccessMode.dump(fout); + fout.decreaseIndent(); + + fout.println("Open fds: #" + mRevocableFds.size()); + } + } + + void writeToXml(@NonNull XmlSerializer out) throws IOException { + synchronized (mSessionLock) { + XmlUtils.writeLongAttribute(out, ATTR_ID, mSessionId); + XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, mOwnerPackageName); + XmlUtils.writeIntAttribute(out, ATTR_UID, mOwnerUid); + XmlUtils.writeLongAttribute(out, ATTR_CREATION_TIME_MS, mCreationTimeMs); + + out.startTag(null, TAG_BLOB_HANDLE); + mBlobHandle.writeToXml(out); + out.endTag(null, TAG_BLOB_HANDLE); + + out.startTag(null, TAG_ACCESS_MODE); + mBlobAccessMode.writeToXml(out); + out.endTag(null, TAG_ACCESS_MODE); + } + } + + @Nullable + static BlobStoreSession createFromXml(@NonNull XmlPullParser in, int version, + @NonNull Context context, @NonNull SessionStateChangeListener stateChangeListener) + throws IOException, XmlPullParserException { + final long sessionId = XmlUtils.readLongAttribute(in, ATTR_ID); + final String ownerPackageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE); + final int ownerUid = XmlUtils.readIntAttribute(in, ATTR_UID); + final long creationTimeMs = version >= XML_VERSION_ADD_SESSION_CREATION_TIME + ? XmlUtils.readLongAttribute(in, ATTR_CREATION_TIME_MS) + : System.currentTimeMillis(); + + final int depth = in.getDepth(); + BlobHandle blobHandle = null; + BlobAccessMode blobAccessMode = null; + while (XmlUtils.nextElementWithin(in, depth)) { + if (TAG_BLOB_HANDLE.equals(in.getName())) { + blobHandle = BlobHandle.createFromXml(in); + } else if (TAG_ACCESS_MODE.equals(in.getName())) { + blobAccessMode = BlobAccessMode.createFromXml(in); + } + } + + if (blobHandle == null) { + Slog.wtf(TAG, "blobHandle should be available"); + return null; + } + if (blobAccessMode == null) { + Slog.wtf(TAG, "blobAccessMode should be available"); + return null; + } + + final BlobStoreSession blobStoreSession = new BlobStoreSession(context, sessionId, + blobHandle, ownerUid, ownerPackageName, creationTimeMs, stateChangeListener); + blobStoreSession.mBlobAccessMode.allow(blobAccessMode); + return blobStoreSession; + } +} diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreUtils.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreUtils.java new file mode 100644 index 000000000000..1d07e88773c3 --- /dev/null +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreUtils.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.blob; + +import static com.android.server.blob.BlobStoreConfig.TAG; + +import android.annotation.IdRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.text.format.TimeMigrationUtils; +import android.util.Slog; + +class BlobStoreUtils { + private static final String DESC_RES_TYPE_STRING = "string"; + + @Nullable + static Resources getPackageResources(@NonNull Context context, + @NonNull String packageName, int userId) { + try { + return context.getPackageManager() + .getResourcesForApplicationAsUser(packageName, userId); + } catch (PackageManager.NameNotFoundException e) { + Slog.d(TAG, "Unknown package in user " + userId + ": " + + packageName, e); + return null; + } + } + + @IdRes + static int getDescriptionResourceId(@NonNull Resources resources, + @NonNull String resourceEntryName, @NonNull String packageName) { + return resources.getIdentifier(resourceEntryName, DESC_RES_TYPE_STRING, packageName); + } + + @IdRes + static int getDescriptionResourceId(@NonNull Context context, + @NonNull String resourceEntryName, @NonNull String packageName, int userId) { + final Resources resources = getPackageResources(context, packageName, userId); + return resources == null + ? Resources.ID_NULL + : getDescriptionResourceId(resources, resourceEntryName, packageName); + } + + @NonNull + static String formatTime(long timeMs) { + return TimeMigrationUtils.formatMillisWithFixedFormat(timeMs); + } +} diff --git a/apex/extservices/Android.bp b/apex/extservices/Android.bp new file mode 100644 index 000000000000..0c6c4c23dce1 --- /dev/null +++ b/apex/extservices/Android.bp @@ -0,0 +1,39 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +apex { + name: "com.android.extservices", + defaults: ["com.android.extservices-defaults"], + manifest: "apex_manifest.json", +} + +apex_defaults { + name: "com.android.extservices-defaults", + updatable: true, + min_sdk_version: "current", + key: "com.android.extservices.key", + certificate: ":com.android.extservices.certificate", + apps: ["ExtServices"], +} + +apex_key { + name: "com.android.extservices.key", + public_key: "com.android.extservices.avbpubkey", + private_key: "com.android.extservices.pem", +} + +android_app_certificate { + name: "com.android.extservices.certificate", + certificate: "com.android.extservices", +} diff --git a/apex/extservices/apex_manifest.json b/apex/extservices/apex_manifest.json new file mode 100644 index 000000000000..b4acf1283d3e --- /dev/null +++ b/apex/extservices/apex_manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.extservices", + "version": 300000000 +} diff --git a/apex/extservices/com.android.extservices.avbpubkey b/apex/extservices/com.android.extservices.avbpubkey Binary files differnew file mode 100644 index 000000000000..f37d3e4a14d4 --- /dev/null +++ b/apex/extservices/com.android.extservices.avbpubkey diff --git a/apex/extservices/com.android.extservices.pem b/apex/extservices/com.android.extservices.pem new file mode 100644 index 000000000000..7bfbd34ff9b9 --- /dev/null +++ b/apex/extservices/com.android.extservices.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEAuYshVDiRkt3tmBhqcWkKOm5GcviKpLbHSPpYQDHGDwS0dqqL +SqAd1/BgT/bVVtUkAciFApPnXn96WhNYCypptyC5FHCxM21uBCGmow+3WermD++w +5dQk4QP2ONPIpG+KzOWBl9SiBud4SpOHDyr0JycBsrXS89Tln9kAsTDuDEFfXL/J +8cX/S3IUwhPV0pAlgUIHdDp0DGFjZaJlEZBZ+HmImriC/AUNUMVb5lfbczXOEZPF +0A9+JzYschfXUxn8nu1N7RN5GDbq+chszx1FMVhuFUheukkd4dLNSDl0O0RlUnD+ +C/xz1ilDzEVZhnMtMnxS9oJ8bA/HUVMfsFnaQbgGmQ0CcxFxnfbYyGXGG1H+b8vA +MTVQi5rZXG2p+VgHIAKVrYmpETVnRPgoMqp18KuGtp5SDngi13G3YEzS7iFbqfYh +6iW2G974nD/Dq0cSire8Oljd9PEaMCMZiP5PTFJp0G/mtw7ROoyZqsSM6rX3XVTo +Y5dBmBMctSJ8rgDMi0ZNvRH+rq/E5+RT6yMAJ7DDbOJzBnQ3IIoGn8NzUT3P1FCB +HYEp1U2N7QNirIQMAuVz3IlHae9N1kl3eGAO6f2CjV7vZmFpDeWw+KSYs71mRkOb +WBgl6D9FFq4u1azrU3AwV0dj3x1eU6yVnKUy1J7ppF/mcR+VzH7ThzTdV7cCAwEA +AQKCAgEApWFU2Mv/PYhg0bPZlLLKsiA+3RWaBo0AfpTd+oIjBpnr/OWweFjVoPcZ +8cyShe4/RPOlUxHgJcO8m/MoA/PO/LLHJWf5GlzMthQEgs1sYVJVtBiydXitUn+E +hUyIR8FAV7et1lZqAXtqJhbvSF7B9u/2vIMCv+GgtuTmkAmL9RKD3Jj6eG1CS84o +oICrkx52v4rKOBgt/icEQMAKFCi1eRti3n3eCqK6JqdzbZIcAcoQnmw34mccy/im +jx+fBuxf1oywa8NyqVmyAehazBVL6lrm7ENwY9zuLK4H2fuUFYu2QFCEsMxZt6da +TgX2cTfSLnDQRfcyzeMWhu9vjHHabjpLNjiCKhIhGyO0rO1rtea8ajZHgM/2sxXq +6gLynW0dlatlxmjANlN9WQPGNdzvcIFJ0TLnI4mlJnWpqCsN9iW1d4ey13WiZUVR +DgtnR60zao+LRCCM4D3cuVLq0DjL2BlHGXnOPK/LpQG1LbI1TroZpgSEHSZlQRzT +ql9txgNqTHxijXuPL2VhhwhW7cqDoO8sLwV3BqDMIH56U0cbUBiSA/G9fKeI/DEG +i7LcrMgrBk+xnuAWoFHuzfBMAdD9i3kYyk+41tOmcza2TNJgxadVYp5woHFvYvS/ +GKaNiRz0XmcijO5Ir0yxgCq21BdkWzo5zVrTFABiKeR7YXiee8kCggEBAOeULWgR +spolJJrACWJspRvKb9FGnbGiYOnCGJoAc751kuXmNxoyWnEwgcjrSEoayNPUfOtz +IgA+twqjgl0Zec2XFPfUcgWUBrrvvUEV4NIH5ibaR7ezHGeovCWs9XoDyzHHvhDr +c6T5kXFZ60rS5h6LGUnE1hkHFJoHuTIBbn9j7eIbri8S71i7HWQ04s4KuQ+Bwbxm +UnkEhbc+zMWHXfXy7rx4/eEZcZwtEybIORcHXYNPGeqMfOlcEMHpKEOi+NvDA6cp +vTaTSwJ6ZBgYh7Tw3bNgRxSknaIhcGwMD0ojStjC5xzXT1Zr2Z3GXwYvOGcq3MeZ +z+V2cx5xuwyp7R0CggEBAM0cKKNZEZwi/1zBPUDMFB4iJoX12BxQX6e5wdlHGXgF +XeZwCnaIxOxMDxH79M5Svmpdu/jkUijI/pRvcE1iohFyIBvTUSDmlAoy4keXqMEQ +M2hA+TwVA3JLmMcV8HKy/MFlwwKJB1JDcoxGjnXsM5UjVTD2jilO7vlJZs3+0ws0 +R7qzRT3ED25QTpZyDYcKE2otc5bzIZG3yAaJtWd3NugWsKpxDgr2RFUGJiHBq72n +48FkSjfgaDTn83zYcPvS0Uykb2ho8G/N+EurstL41n3nQo0I7FLbyptOopDDwsSp +Ndejn08NVAQ+xFAafOyqHkA3Ytpl0QCZDpMBuLdvw+MCggEAOVMt1kgjPRMat4/4 +ArxANtvqyBRB7vnyIYthiaW5ARmbrntJgpuaVdCbIABWGbn9oqpD7gjHDuZ3axPE +roUi6KiQkTSusQDOlbHI2Haw+2znJRD9ldSpoGNdh7oD3htYTk9Sll+ideEthrCq +lRAV1NO8A83M7c8Z43Mr/dvq3XAAL+uIN7DpPL687NRGnJh87QDC039ExR5Ad3b9 +O5xhvwNO46rTtcgVnoJt7ji8IR46oMmQ8cWrGh0nLMkppWyPS98/ZT7ozryxYcCo +TGquFTVWvBOGJO8G8l5ytNxbYI/R9Exy52nJAuyZpvu3BBHmVWt/0Y0asIOcxZmD +owPhZQKCAQAfWAFBzReq05JQe1s/7q/YVwGqEQKgeQvVFsbvzDSxKajK0S5YJNhq +/8iByA4GBZEBsidKhqGjh+uXhVwVB1Ca9+S+O9G3BGV1FYeMxzlLn40rjlpH+zIW +okTLj6e5724+o61kUspioNn9Y77beGf9j3OyUsswttZAFB54tktL+AZKGqEnKjHt +eqo3xWAZ1clXvXBfjfIAUaRok1y8XfRvDSCcO0CZHj8c+x6SpAT5q5FbeVb6KPnj +s9p6ppzFbtb7Llm0C+1KOKCL98YRBWPJw7Bg2w86LkpM53xiQPgfk3gd5uwuaWwA +ZhMb5qBWjjynNY+OrmZ8/+bBQk8XASZfAoIBAFkHOnZOD1JJQ0QvaJ9tuCgHi216 +I8QPMMTdm3ZEDHSYMNwl7ayeseBcmB2zaqBKYz75qcU0SK4lnZkR2wIpbsHZNSVM +J0WpN6r9G4JdnVi11J04RsfSMjCUr/PTVMmPvw8xPHrCxkJmB+d56olSE80I1Jrx +djCv1LtSsT10W7FIcY82/cOi4xxGLOA70lDCf+szofQgVP8WvuOA1YaFw98ca8zc +A401CyNexk24/c3d6C19YW/MppdE0uGMxL/oHsPgwkZAf6LmvF/UF71PsBUEniLc +YFaJl3wn1cPfBBo9L4sZzyP2qokL8YHdg+wW7b4IOsYwbeqceBvqPtcUUPs= +-----END RSA PRIVATE KEY----- diff --git a/apex/extservices/com.android.extservices.pk8 b/apex/extservices/com.android.extservices.pk8 Binary files differnew file mode 100644 index 000000000000..59585a212592 --- /dev/null +++ b/apex/extservices/com.android.extservices.pk8 diff --git a/apex/extservices/com.android.extservices.x509.pem b/apex/extservices/com.android.extservices.x509.pem new file mode 100644 index 000000000000..e0343b81d279 --- /dev/null +++ b/apex/extservices/com.android.extservices.x509.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIGLTCCBBWgAwIBAgIUdqdMmx/5OsCP3Ew3/hcr7+1ACHEwDQYJKoZIhvcNAQEL +BQAwgaQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH +DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy +b2lkMSAwHgYDVQQDDBdjb20uYW5kcm9pZC5leHRzZXJ2aWNlczEiMCAGCSqGSIb3 +DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAgFw0yMDAxMTcxMDIxMzZaGA80NzU3 +MTIxMzEwMjEzNlowgaQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh +MRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYD +VQQLDAdBbmRyb2lkMSAwHgYDVQQDDBdjb20uYW5kcm9pZC5leHRzZXJ2aWNlczEi +MCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANKaSeLGaFRRt779vAtTfG3t2aQZrWOByUYc7yUN +RdmJqWxU47OL5urYmanWPbz2f972Q9oi8x+8y4ny9SEY3wg0pUbzvKNTXpkxWyG1 +HE2C2zTfzuDDLpDIf2usWynt1wLVhpYC3k+7Yv2vOIK5dKkezh6PfdKmsbDae5DE +d22tTSYZ5KwNpIWrgQle26cRG5sqhAFdkpgGMF00Huz06cjUoTjs2sNSlXTRBOTP +CCy8UoRjBivQZkwHbddfsn+Z22ARPG8JDg/n4mEi8C0T6bJeQeirSPkBCkD6Djgq +7RddJ2eLYZII8l8r6A6x+6cnTkXHaV5g3LUwPvi8XEn9IUuT9WJNRje/vfYLycTQ +kP415CZMxDvsi1Ul4YsbL3enE89ryGMTpVZPogch/36DG5Sye28yISItNUy3urJa +OXbg7mh+MwPd4bQaW4CJk+AUweKaF4aV0SZFT+nCewL4xLdGdy889KazlW98NqtK +hOSxIg1jHkZq48ajuq2A+ns1yDKt1l0f9IYCz3mz/IXInokbkjPvHahJTJ+OMHXO +THD8e5gBzcK841jJk+H3EsIYOHsp66uy2IgEHN+9pAS6vI0xfrXOYuKzuSL3oxcV +FlVTimt4xokMMerdcW4KD+MC5NFEip4DUS4JKCyG0wRI3ffEs9Zcpxi3QSibrjLW +rz+hAgMBAAGjUzBRMB0GA1UdDgQWBBTP2AhZzEUUgtAFlkaMaq+RvY06fDAfBgNV +HSMEGDAWgBTP2AhZzEUUgtAFlkaMaq+RvY06fDAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQCbwtfo37j62Sudmt32PCfRN/r5ZNDNNA2JhR8uDUmX +xXfF5YfDvSKsNLiQKcDagu6a+0C+QnzXHXCBlXZFrTJ8NAVMlmqdHGwoFoYMfJZH +R1lCTidyFMoMLJ8GRGPJjzDkKnOeAqKMCtKvXoH2r12+JB2/ov4ooLREu/wPkEXT +OymkyWNP5XLQTKWqfEQyXXFpuwZ+m35Wkr0Fm92mZeJpVeIZPK7M7aK3zyoj7XJP +YLMsR/AQs8OULdpfNMddAuN3ndlYu03LZlsF6LG5bduaDDcESJ5hdJrgBa/NBKRU +IbS+q/6WAjYKMNRT/fPGew4wUzlWKi1Ihdk79oaqKKijE1b2JSJD1/SEYiBf+JPE +bXobUrMbBwFpdhT+YLMF9FsuPQKsUIONaWiO4QcQoY/rQwGxPP6fV8ZbBrUWJewj +MpSdU9foZNa/TmOAgfS/JxH+nXnG4+H1m8mdNBsxvsYmF2ZuGb/jdEeA2cuHIJDZ +FJeWwCFxzlCGZJaUsxsnZByADBuufUVaO/9gGs0YQC/JP1i9hK4DyZdKwZpXdLi2 +Nw27Qma4WEIZnMb6Rgk1nTV+7ALcOSIhGgFOOeDTuCGfnEcz2coai5fbD/K6Q7Xu +IRNyxHQjheZPdei2x912Ex/KqKGfaFaZJxrvCSKdhzxcTFIsO4JuZs+SDpRTKcI7 +Cw== +-----END CERTIFICATE----- diff --git a/apex/extservices/testing/Android.bp b/apex/extservices/testing/Android.bp new file mode 100644 index 000000000000..88a47246c824 --- /dev/null +++ b/apex/extservices/testing/Android.bp @@ -0,0 +1,25 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +apex_test { + name: "test_com.android.extservices", + visibility: [ + "//system/apex/tests", + ], + defaults: ["com.android.extservices-defaults"], + manifest: "test_manifest.json", + file_contexts: ":com.android.extservices-file_contexts", + // Test APEX, should never be installed + installable: false, +} diff --git a/apex/extservices/testing/test_manifest.json b/apex/extservices/testing/test_manifest.json new file mode 100644 index 000000000000..23a50e37bdd3 --- /dev/null +++ b/apex/extservices/testing/test_manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.extservices", + "version": 2147483647 +} diff --git a/apex/jobscheduler/OWNERS b/apex/jobscheduler/OWNERS new file mode 100644 index 000000000000..d004eed2a0db --- /dev/null +++ b/apex/jobscheduler/OWNERS @@ -0,0 +1,6 @@ +yamasani@google.com +omakoto@google.com +ctate@android.com +ctate@google.com +kwekua@google.com +suprabh@google.com
\ No newline at end of file diff --git a/apex/jobscheduler/README_js-mainline.md b/apex/jobscheduler/README_js-mainline.md new file mode 100644 index 000000000000..134ff3da4507 --- /dev/null +++ b/apex/jobscheduler/README_js-mainline.md @@ -0,0 +1,20 @@ +# Making Job Scheduler into a Mainline Module + +## Current structure + +- JS service side classes are put in `service-jobscheduler.jar`. +It's *not* included in services.jar, and instead it's put in the system server classpath, +which currently looks like the following: +`SYSTEMSERVERCLASSPATH=/system/framework/services.jar:/system/framework/ethernet-service.jar:/system/framework/com.android.location.provider.jar:/system/framework/service-jobscheduler.jar` + + `SYSTEMSERVERCLASSPATH` is generated from `PRODUCT_SYSTEM_SERVER_JARS`. + +- JS framework side classes are put in `framework-jobscheduler.jar`, +and the rest of the framework code is put in `framework-minus-apex.jar`, +as of http://ag/9145619. + + However these jar files are *not* put on the device. We still generate + `framework.jar` merging the two jar files, and this jar file is what's + put on the device and loaded by Zygote. + +The current structure is *not* the final design. diff --git a/apex/jobscheduler/framework/Android.bp b/apex/jobscheduler/framework/Android.bp new file mode 100644 index 000000000000..ec074262fb13 --- /dev/null +++ b/apex/jobscheduler/framework/Android.bp @@ -0,0 +1,30 @@ +filegroup { + name: "framework-jobscheduler-sources", + srcs: [ + "java/**/*.java", + "java/android/app/job/IJobCallback.aidl", + "java/android/app/job/IJobScheduler.aidl", + "java/android/app/job/IJobService.aidl", + "java/android/os/IDeviceIdleController.aidl", + ], + path: "java", +} + +java_library { + name: "framework-jobscheduler", + installable: false, + compile_dex: true, + sdk_version: "core_platform", + srcs: [ + ":framework-jobscheduler-sources", + ], + aidl: { + export_include_dirs: [ + "java", + ], + }, + libs: [ + "framework-minus-apex", + "unsupportedappusage", + ], +} diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java new file mode 100644 index 000000000000..f59e7a4ae6ec --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.app.job.IJobScheduler; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.app.job.JobSnapshot; +import android.app.job.JobWorkItem; +import android.os.RemoteException; + +import java.util.List; + + +/** + * Concrete implementation of the JobScheduler interface + * + * Note android.app.job is the better package to put this class, but we can't move it there + * because that'd break robolectric. Grr. + * + * @hide + */ +public class JobSchedulerImpl extends JobScheduler { + IJobScheduler mBinder; + + public JobSchedulerImpl(IJobScheduler binder) { + mBinder = binder; + } + + @Override + public int schedule(JobInfo job) { + try { + return mBinder.schedule(job); + } catch (RemoteException e) { + return JobScheduler.RESULT_FAILURE; + } + } + + @Override + public int enqueue(JobInfo job, JobWorkItem work) { + try { + return mBinder.enqueue(job, work); + } catch (RemoteException e) { + return JobScheduler.RESULT_FAILURE; + } + } + + @Override + public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag) { + try { + return mBinder.scheduleAsPackage(job, packageName, userId, tag); + } catch (RemoteException e) { + return JobScheduler.RESULT_FAILURE; + } + } + + @Override + public void cancel(int jobId) { + try { + mBinder.cancel(jobId); + } catch (RemoteException e) {} + + } + + @Override + public void cancelAll() { + try { + mBinder.cancelAll(); + } catch (RemoteException e) {} + + } + + @Override + public List<JobInfo> getAllPendingJobs() { + try { + return mBinder.getAllPendingJobs().getList(); + } catch (RemoteException e) { + return null; + } + } + + @Override + public JobInfo getPendingJob(int jobId) { + try { + return mBinder.getPendingJob(jobId); + } catch (RemoteException e) { + return null; + } + } + + @Override + public List<JobInfo> getStartedJobs() { + try { + return mBinder.getStartedJobs(); + } catch (RemoteException e) { + return null; + } + } + + @Override + public List<JobSnapshot> getAllJobSnapshots() { + try { + return mBinder.getAllJobSnapshots().getList(); + } catch (RemoteException e) { + return null; + } + } +} diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl new file mode 100644 index 000000000000..d281da037fde --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl @@ -0,0 +1,68 @@ +/** + * Copyright 2014, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.job; + +import android.app.job.JobWorkItem; + +/** + * The server side of the JobScheduler IPC protocols. The app-side implementation + * invokes on this interface to indicate completion of the (asynchronous) instructions + * issued by the server. + * + * In all cases, the 'who' parameter is the caller's service binder, used to track + * which Job Service instance is reporting. + * + * {@hide} + */ +interface IJobCallback { + /** + * Immediate callback to the system after sending a start signal, used to quickly detect ANR. + * + * @param jobId Unique integer used to identify this job. + * @param ongoing True to indicate that the client is processing the job. False if the job is + * complete + */ + @UnsupportedAppUsage + void acknowledgeStartMessage(int jobId, boolean ongoing); + /** + * Immediate callback to the system after sending a stop signal, used to quickly detect ANR. + * + * @param jobId Unique integer used to identify this job. + * @param reschedule Whether or not to reschedule this job. + */ + @UnsupportedAppUsage + void acknowledgeStopMessage(int jobId, boolean reschedule); + /* + * Called to deqeue next work item for the job. + */ + @UnsupportedAppUsage + JobWorkItem dequeueWork(int jobId); + /* + * Called to report that job has completed processing a work item. + */ + @UnsupportedAppUsage + boolean completeWork(int jobId, int workId); + /* + * Tell the job manager that the client is done with its execution, so that it can go on to + * the next one and stop attributing wakelock time to us etc. + * + * @param jobId Unique integer used to identify this job. + * @param reschedule Whether or not to reschedule this job. + */ + @UnsupportedAppUsage + void jobFinished(int jobId, boolean reschedule); +} diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl new file mode 100644 index 000000000000..3006f50e54fc --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.job; + +import android.app.job.JobInfo; +import android.app.job.JobSnapshot; +import android.app.job.JobWorkItem; +import android.content.pm.ParceledListSlice; + + /** + * IPC interface that supports the app-facing {@link #JobScheduler} api. + * {@hide} + */ +interface IJobScheduler { + int schedule(in JobInfo job); + int enqueue(in JobInfo job, in JobWorkItem work); + int scheduleAsPackage(in JobInfo job, String packageName, int userId, String tag); + void cancel(int jobId); + void cancelAll(); + ParceledListSlice getAllPendingJobs(); + JobInfo getPendingJob(int jobId); + List<JobInfo> getStartedJobs(); + ParceledListSlice getAllJobSnapshots(); +} diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl new file mode 100644 index 000000000000..22ad252b9639 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl @@ -0,0 +1,34 @@ +/** + * Copyright 2014, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.job; + +import android.app.job.JobParameters; + +/** + * Interface that the framework uses to communicate with application code that implements a + * JobService. End user code does not implement this interface directly; instead, the app's + * service implementation will extend android.app.job.JobService. + * {@hide} + */ +oneway interface IJobService { + /** Begin execution of application's job. */ + @UnsupportedAppUsage + void startJob(in JobParameters jobParams); + /** Stop execution of application's job. */ + @UnsupportedAppUsage + void stopJob(in JobParameters jobParams); +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.aidl b/apex/jobscheduler/framework/java/android/app/job/JobInfo.aidl new file mode 100644 index 000000000000..7b198a8ab14d --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.job; + +parcelable JobInfo; diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java new file mode 100644 index 000000000000..9f98f8efc774 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -0,0 +1,1596 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.app.job; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.util.TimeUtils.formatDuration; + +import android.annotation.BytesLong; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.ClipData; +import android.content.ComponentName; +import android.net.NetworkRequest; +import android.net.NetworkSpecifier; +import android.net.Uri; +import android.os.BaseBundle; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; + +/** + * Container of data passed to the {@link android.app.job.JobScheduler} fully encapsulating the + * parameters required to schedule work against the calling application. These are constructed + * using the {@link JobInfo.Builder}. + * The goal here is to provide the scheduler with high-level semantics about the work you want to + * accomplish. + * <p> Prior to Android version {@link Build.VERSION_CODES#Q}, you had to specify at least one + * constraint on the JobInfo object that you are creating. Otherwise, the builder would throw an + * exception when building. From Android version {@link Build.VERSION_CODES#Q} and onwards, it is + * valid to schedule jobs with no constraints. + */ +public class JobInfo implements Parcelable { + private static String TAG = "JobInfo"; + + /** @hide */ + @IntDef(prefix = { "NETWORK_TYPE_" }, value = { + NETWORK_TYPE_NONE, + NETWORK_TYPE_ANY, + NETWORK_TYPE_UNMETERED, + NETWORK_TYPE_NOT_ROAMING, + NETWORK_TYPE_CELLULAR, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface NetworkType {} + + /** Default. */ + public static final int NETWORK_TYPE_NONE = 0; + /** This job requires network connectivity. */ + public static final int NETWORK_TYPE_ANY = 1; + /** This job requires network connectivity that is unmetered. */ + public static final int NETWORK_TYPE_UNMETERED = 2; + /** This job requires network connectivity that is not roaming. */ + public static final int NETWORK_TYPE_NOT_ROAMING = 3; + /** This job requires network connectivity that is a cellular network. */ + public static final int NETWORK_TYPE_CELLULAR = 4; + + /** + * This job requires metered connectivity such as most cellular data + * networks. + * + * @deprecated Cellular networks may be unmetered, or Wi-Fi networks may be + * metered, so this isn't a good way of selecting a specific + * transport. Instead, use {@link #NETWORK_TYPE_CELLULAR} or + * {@link android.net.NetworkRequest.Builder#addTransportType(int)} + * if your job requires a specific network transport. + */ + @Deprecated + public static final int NETWORK_TYPE_METERED = NETWORK_TYPE_CELLULAR; + + /** Sentinel value indicating that bytes are unknown. */ + public static final int NETWORK_BYTES_UNKNOWN = -1; + + /** + * Amount of backoff a job has initially by default, in milliseconds. + */ + public static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 30000L; // 30 seconds. + + /** + * Maximum backoff we allow for a job, in milliseconds. + */ + public static final long MAX_BACKOFF_DELAY_MILLIS = 5 * 60 * 60 * 1000; // 5 hours. + + /** @hide */ + @IntDef(prefix = { "BACKOFF_POLICY_" }, value = { + BACKOFF_POLICY_LINEAR, + BACKOFF_POLICY_EXPONENTIAL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface BackoffPolicy {} + + /** + * Linearly back-off a failed job. See + * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} + * retry_time(current_time, num_failures) = + * current_time + initial_backoff_millis * num_failures, num_failures >= 1 + */ + public static final int BACKOFF_POLICY_LINEAR = 0; + + /** + * Exponentially back-off a failed job. See + * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} + * + * retry_time(current_time, num_failures) = + * current_time + initial_backoff_millis * 2 ^ (num_failures - 1), num_failures >= 1 + */ + public static final int BACKOFF_POLICY_EXPONENTIAL = 1; + + /* Minimum interval for a periodic job, in milliseconds. */ + private static final long MIN_PERIOD_MILLIS = 15 * 60 * 1000L; // 15 minutes + + /* Minimum flex for a periodic job, in milliseconds. */ + private static final long MIN_FLEX_MILLIS = 5 * 60 * 1000L; // 5 minutes + + /** + * Minimum backoff interval for a job, in milliseconds + * @hide + */ + public static final long MIN_BACKOFF_MILLIS = 10 * 1000L; // 10 seconds + + /** + * Query the minimum interval allowed for periodic scheduled jobs. Attempting + * to declare a smaller period than this when scheduling a job will result in a + * job that is still periodic, but will run with this effective period. + * + * @return The minimum available interval for scheduling periodic jobs, in milliseconds. + */ + public static final long getMinPeriodMillis() { + return MIN_PERIOD_MILLIS; + } + + /** + * Query the minimum flex time allowed for periodic scheduled jobs. Attempting + * to declare a shorter flex time than this when scheduling such a job will + * result in this amount as the effective flex time for the job. + * + * @return The minimum available flex time for scheduling periodic jobs, in milliseconds. + */ + public static final long getMinFlexMillis() { + return MIN_FLEX_MILLIS; + } + + /** + * Query the minimum automatic-reschedule backoff interval permitted for jobs. + * @hide + */ + public static final long getMinBackoffMillis() { + return MIN_BACKOFF_MILLIS; + } + + /** + * Default type of backoff. + * @hide + */ + public static final int DEFAULT_BACKOFF_POLICY = BACKOFF_POLICY_EXPONENTIAL; + + /** + * Default of {@link #getPriority}. + * @hide + */ + public static final int PRIORITY_DEFAULT = 0; + + /** + * Value of {@link #getPriority} for expedited syncs. + * @hide + */ + public static final int PRIORITY_SYNC_EXPEDITED = 10; + + /** + * Value of {@link #getPriority} for first time initialization syncs. + * @hide + */ + public static final int PRIORITY_SYNC_INITIALIZATION = 20; + + /** + * Value of {@link #getPriority} for a BFGS app (overrides the supplied + * JobInfo priority if it is smaller). + * @hide + */ + public static final int PRIORITY_BOUND_FOREGROUND_SERVICE = 30; + + /** @hide For backward compatibility. */ + @UnsupportedAppUsage + public static final int PRIORITY_FOREGROUND_APP = PRIORITY_BOUND_FOREGROUND_SERVICE; + + /** + * Value of {@link #getPriority} for a FG service app (overrides the supplied + * JobInfo priority if it is smaller). + * @hide + */ + @UnsupportedAppUsage + public static final int PRIORITY_FOREGROUND_SERVICE = 35; + + /** + * Value of {@link #getPriority} for the current top app (overrides the supplied + * JobInfo priority if it is smaller). + * @hide + */ + public static final int PRIORITY_TOP_APP = 40; + + /** + * Adjustment of {@link #getPriority} if the app has often (50% or more of the time) + * been running jobs. + * @hide + */ + public static final int PRIORITY_ADJ_OFTEN_RUNNING = -40; + + /** + * Adjustment of {@link #getPriority} if the app has always (90% or more of the time) + * been running jobs. + * @hide + */ + public static final int PRIORITY_ADJ_ALWAYS_RUNNING = -80; + + /** + * Indicates that the implementation of this job will be using + * {@link JobService#startForeground(int, android.app.Notification)} to run + * in the foreground. + * <p> + * When set, the internal scheduling of this job will ignore any background + * network restrictions for the requesting app. Note that this flag alone + * doesn't actually place your {@link JobService} in the foreground; you + * still need to post the notification yourself. + * <p> + * To use this flag, the caller must hold the + * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL} permission. + * + * @hide + */ + @UnsupportedAppUsage + public static final int FLAG_WILL_BE_FOREGROUND = 1 << 0; + + /** + * Allows this job to run despite doze restrictions as long as the app is in the foreground + * or on the temporary whitelist + * @hide + */ + public static final int FLAG_IMPORTANT_WHILE_FOREGROUND = 1 << 1; + + /** + * @hide + */ + public static final int FLAG_PREFETCH = 1 << 2; + + /** + * This job needs to be exempted from the app standby throttling. Only the system (UID 1000) + * can set it. Jobs with a time constrant must not have it. + * + * @hide + */ + public static final int FLAG_EXEMPT_FROM_APP_STANDBY = 1 << 3; + + /** + * @hide + */ + public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0; + + /** + * @hide + */ + public static final int CONSTRAINT_FLAG_BATTERY_NOT_LOW = 1 << 1; + + /** + * @hide + */ + public static final int CONSTRAINT_FLAG_DEVICE_IDLE = 1 << 2; + + /** + * @hide + */ + public static final int CONSTRAINT_FLAG_STORAGE_NOT_LOW = 1 << 3; + + @UnsupportedAppUsage + private final int jobId; + private final PersistableBundle extras; + private final Bundle transientExtras; + private final ClipData clipData; + private final int clipGrantFlags; + @UnsupportedAppUsage + private final ComponentName service; + private final int constraintFlags; + private final TriggerContentUri[] triggerContentUris; + private final long triggerContentUpdateDelay; + private final long triggerContentMaxDelay; + private final boolean hasEarlyConstraint; + private final boolean hasLateConstraint; + private final NetworkRequest networkRequest; + private final long networkDownloadBytes; + private final long networkUploadBytes; + private final long minLatencyMillis; + private final long maxExecutionDelayMillis; + private final boolean isPeriodic; + private final boolean isPersisted; + private final long intervalMillis; + private final long flexMillis; + private final long initialBackoffMillis; + private final int backoffPolicy; + private final int priority; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + private final int flags; + + /** + * Unique job id associated with this application (uid). This is the same job ID + * you supplied in the {@link Builder} constructor. + */ + public int getId() { + return jobId; + } + + /** + * @see JobInfo.Builder#setExtras(PersistableBundle) + */ + public @NonNull PersistableBundle getExtras() { + return extras; + } + + /** + * @see JobInfo.Builder#setTransientExtras(Bundle) + */ + public @NonNull Bundle getTransientExtras() { + return transientExtras; + } + + /** + * @see JobInfo.Builder#setClipData(ClipData, int) + */ + public @Nullable ClipData getClipData() { + return clipData; + } + + /** + * @see JobInfo.Builder#setClipData(ClipData, int) + */ + public int getClipGrantFlags() { + return clipGrantFlags; + } + + /** + * Name of the service endpoint that will be called back into by the JobScheduler. + */ + public @NonNull ComponentName getService() { + return service; + } + + /** @hide */ + public int getPriority() { + return priority; + } + + /** @hide */ + public int getFlags() { + return flags; + } + + /** @hide */ + public boolean isExemptedFromAppStandby() { + return ((flags & FLAG_EXEMPT_FROM_APP_STANDBY) != 0) && !isPeriodic(); + } + + /** + * @see JobInfo.Builder#setRequiresCharging(boolean) + */ + public boolean isRequireCharging() { + return (constraintFlags & CONSTRAINT_FLAG_CHARGING) != 0; + } + + /** + * @see JobInfo.Builder#setRequiresBatteryNotLow(boolean) + */ + public boolean isRequireBatteryNotLow() { + return (constraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0; + } + + /** + * @see JobInfo.Builder#setRequiresDeviceIdle(boolean) + */ + public boolean isRequireDeviceIdle() { + return (constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0; + } + + /** + * @see JobInfo.Builder#setRequiresStorageNotLow(boolean) + */ + public boolean isRequireStorageNotLow() { + return (constraintFlags & CONSTRAINT_FLAG_STORAGE_NOT_LOW) != 0; + } + + /** + * @hide + */ + public int getConstraintFlags() { + return constraintFlags; + } + + /** + * Which content: URIs must change for the job to be scheduled. Returns null + * if there are none required. + * @see JobInfo.Builder#addTriggerContentUri(TriggerContentUri) + */ + public @Nullable TriggerContentUri[] getTriggerContentUris() { + return triggerContentUris; + } + + /** + * When triggering on content URI changes, this is the delay from when a change + * is detected until the job is scheduled. + * @see JobInfo.Builder#setTriggerContentUpdateDelay(long) + */ + public long getTriggerContentUpdateDelay() { + return triggerContentUpdateDelay; + } + + /** + * When triggering on content URI changes, this is the maximum delay we will + * use before scheduling the job. + * @see JobInfo.Builder#setTriggerContentMaxDelay(long) + */ + public long getTriggerContentMaxDelay() { + return triggerContentMaxDelay; + } + + /** + * Return the basic description of the kind of network this job requires. + * + * @deprecated This method attempts to map {@link #getRequiredNetwork()} + * into the set of simple constants, which results in a loss of + * fidelity. Callers should move to using + * {@link #getRequiredNetwork()} directly. + * @see Builder#setRequiredNetworkType(int) + */ + @Deprecated + public @NetworkType int getNetworkType() { + if (networkRequest == null) { + return NETWORK_TYPE_NONE; + } else if (networkRequest.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)) { + return NETWORK_TYPE_UNMETERED; + } else if (networkRequest.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING)) { + return NETWORK_TYPE_NOT_ROAMING; + } else if (networkRequest.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) { + return NETWORK_TYPE_CELLULAR; + } else { + return NETWORK_TYPE_ANY; + } + } + + /** + * Return the detailed description of the kind of network this job requires, + * or {@code null} if no specific kind of network is required. + * + * @see Builder#setRequiredNetwork(NetworkRequest) + */ + public @Nullable NetworkRequest getRequiredNetwork() { + return networkRequest; + } + + /** + * Return the estimated size of download traffic that will be performed by + * this job, in bytes. + * + * @return Estimated size of download traffic, or + * {@link #NETWORK_BYTES_UNKNOWN} when unknown. + * @see Builder#setEstimatedNetworkBytes(long, long) + */ + public @BytesLong long getEstimatedNetworkDownloadBytes() { + return networkDownloadBytes; + } + + /** + * Return the estimated size of upload traffic that will be performed by + * this job, in bytes. + * + * @return Estimated size of upload traffic, or + * {@link #NETWORK_BYTES_UNKNOWN} when unknown. + * @see Builder#setEstimatedNetworkBytes(long, long) + */ + public @BytesLong long getEstimatedNetworkUploadBytes() { + return networkUploadBytes; + } + + /** + * Set for a job that does not recur periodically, to specify a delay after which the job + * will be eligible for execution. This value is not set if the job recurs periodically. + * @see JobInfo.Builder#setMinimumLatency(long) + */ + public long getMinLatencyMillis() { + return minLatencyMillis; + } + + /** + * @see JobInfo.Builder#setOverrideDeadline(long) + */ + public long getMaxExecutionDelayMillis() { + return maxExecutionDelayMillis; + } + + /** + * Track whether this job will repeat with a given period. + * @see JobInfo.Builder#setPeriodic(long) + * @see JobInfo.Builder#setPeriodic(long, long) + */ + public boolean isPeriodic() { + return isPeriodic; + } + + /** + * @see JobInfo.Builder#setPersisted(boolean) + */ + public boolean isPersisted() { + return isPersisted; + } + + /** + * Set to the interval between occurrences of this job. This value is <b>not</b> set if the + * job does not recur periodically. + * @see JobInfo.Builder#setPeriodic(long) + * @see JobInfo.Builder#setPeriodic(long, long) + */ + public long getIntervalMillis() { + return intervalMillis; + } + + /** + * Flex time for this job. Only valid if this is a periodic job. The job can + * execute at any time in a window of flex length at the end of the period. + * @see JobInfo.Builder#setPeriodic(long) + * @see JobInfo.Builder#setPeriodic(long, long) + */ + public long getFlexMillis() { + return flexMillis; + } + + /** + * The amount of time the JobScheduler will wait before rescheduling a failed job. This value + * will be increased depending on the backoff policy specified at job creation time. Defaults + * to 30 seconds, minimum is currently 10 seconds. + * @see JobInfo.Builder#setBackoffCriteria(long, int) + */ + public long getInitialBackoffMillis() { + return initialBackoffMillis; + } + + /** + * Return the backoff policy of this job. + * @see JobInfo.Builder#setBackoffCriteria(long, int) + */ + public @BackoffPolicy int getBackoffPolicy() { + return backoffPolicy; + } + + /** + * @see JobInfo.Builder#setImportantWhileForeground(boolean) + */ + public boolean isImportantWhileForeground() { + return (flags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0; + } + + /** + * @see JobInfo.Builder#setPrefetch(boolean) + */ + public boolean isPrefetch() { + return (flags & FLAG_PREFETCH) != 0; + } + + /** + * User can specify an early constraint of 0L, which is valid, so we keep track of whether the + * function was called at all. + * @hide + */ + public boolean hasEarlyConstraint() { + return hasEarlyConstraint; + } + + /** + * User can specify a late constraint of 0L, which is valid, so we keep track of whether the + * function was called at all. + * @hide + */ + public boolean hasLateConstraint() { + return hasLateConstraint; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof JobInfo)) { + return false; + } + JobInfo j = (JobInfo) o; + if (jobId != j.jobId) { + return false; + } + // XXX won't be correct if one is parcelled and the other not. + if (!BaseBundle.kindofEquals(extras, j.extras)) { + return false; + } + // XXX won't be correct if one is parcelled and the other not. + if (!BaseBundle.kindofEquals(transientExtras, j.transientExtras)) { + return false; + } + // XXX for now we consider two different clip data objects to be different, + // regardless of whether their contents are the same. + if (clipData != j.clipData) { + return false; + } + if (clipGrantFlags != j.clipGrantFlags) { + return false; + } + if (!Objects.equals(service, j.service)) { + return false; + } + if (constraintFlags != j.constraintFlags) { + return false; + } + if (!Arrays.equals(triggerContentUris, j.triggerContentUris)) { + return false; + } + if (triggerContentUpdateDelay != j.triggerContentUpdateDelay) { + return false; + } + if (triggerContentMaxDelay != j.triggerContentMaxDelay) { + return false; + } + if (hasEarlyConstraint != j.hasEarlyConstraint) { + return false; + } + if (hasLateConstraint != j.hasLateConstraint) { + return false; + } + if (!Objects.equals(networkRequest, j.networkRequest)) { + return false; + } + if (networkDownloadBytes != j.networkDownloadBytes) { + return false; + } + if (networkUploadBytes != j.networkUploadBytes) { + return false; + } + if (minLatencyMillis != j.minLatencyMillis) { + return false; + } + if (maxExecutionDelayMillis != j.maxExecutionDelayMillis) { + return false; + } + if (isPeriodic != j.isPeriodic) { + return false; + } + if (isPersisted != j.isPersisted) { + return false; + } + if (intervalMillis != j.intervalMillis) { + return false; + } + if (flexMillis != j.flexMillis) { + return false; + } + if (initialBackoffMillis != j.initialBackoffMillis) { + return false; + } + if (backoffPolicy != j.backoffPolicy) { + return false; + } + if (priority != j.priority) { + return false; + } + if (flags != j.flags) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int hashCode = jobId; + if (extras != null) { + hashCode = 31 * hashCode + extras.hashCode(); + } + if (transientExtras != null) { + hashCode = 31 * hashCode + transientExtras.hashCode(); + } + if (clipData != null) { + hashCode = 31 * hashCode + clipData.hashCode(); + } + hashCode = 31*hashCode + clipGrantFlags; + if (service != null) { + hashCode = 31 * hashCode + service.hashCode(); + } + hashCode = 31 * hashCode + constraintFlags; + if (triggerContentUris != null) { + hashCode = 31 * hashCode + Arrays.hashCode(triggerContentUris); + } + hashCode = 31 * hashCode + Long.hashCode(triggerContentUpdateDelay); + hashCode = 31 * hashCode + Long.hashCode(triggerContentMaxDelay); + hashCode = 31 * hashCode + Boolean.hashCode(hasEarlyConstraint); + hashCode = 31 * hashCode + Boolean.hashCode(hasLateConstraint); + if (networkRequest != null) { + hashCode = 31 * hashCode + networkRequest.hashCode(); + } + hashCode = 31 * hashCode + Long.hashCode(networkDownloadBytes); + hashCode = 31 * hashCode + Long.hashCode(networkUploadBytes); + hashCode = 31 * hashCode + Long.hashCode(minLatencyMillis); + hashCode = 31 * hashCode + Long.hashCode(maxExecutionDelayMillis); + hashCode = 31 * hashCode + Boolean.hashCode(isPeriodic); + hashCode = 31 * hashCode + Boolean.hashCode(isPersisted); + hashCode = 31 * hashCode + Long.hashCode(intervalMillis); + hashCode = 31 * hashCode + Long.hashCode(flexMillis); + hashCode = 31 * hashCode + Long.hashCode(initialBackoffMillis); + hashCode = 31 * hashCode + backoffPolicy; + hashCode = 31 * hashCode + priority; + hashCode = 31 * hashCode + flags; + return hashCode; + } + + private JobInfo(Parcel in) { + jobId = in.readInt(); + extras = in.readPersistableBundle(); + transientExtras = in.readBundle(); + if (in.readInt() != 0) { + clipData = ClipData.CREATOR.createFromParcel(in); + clipGrantFlags = in.readInt(); + } else { + clipData = null; + clipGrantFlags = 0; + } + service = in.readParcelable(null); + constraintFlags = in.readInt(); + triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR); + triggerContentUpdateDelay = in.readLong(); + triggerContentMaxDelay = in.readLong(); + if (in.readInt() != 0) { + networkRequest = NetworkRequest.CREATOR.createFromParcel(in); + } else { + networkRequest = null; + } + networkDownloadBytes = in.readLong(); + networkUploadBytes = in.readLong(); + minLatencyMillis = in.readLong(); + maxExecutionDelayMillis = in.readLong(); + isPeriodic = in.readInt() == 1; + isPersisted = in.readInt() == 1; + intervalMillis = in.readLong(); + flexMillis = in.readLong(); + initialBackoffMillis = in.readLong(); + backoffPolicy = in.readInt(); + hasEarlyConstraint = in.readInt() == 1; + hasLateConstraint = in.readInt() == 1; + priority = in.readInt(); + flags = in.readInt(); + } + + private JobInfo(JobInfo.Builder b) { + jobId = b.mJobId; + extras = b.mExtras.deepCopy(); + transientExtras = b.mTransientExtras.deepCopy(); + clipData = b.mClipData; + clipGrantFlags = b.mClipGrantFlags; + service = b.mJobService; + constraintFlags = b.mConstraintFlags; + triggerContentUris = b.mTriggerContentUris != null + ? b.mTriggerContentUris.toArray(new TriggerContentUri[b.mTriggerContentUris.size()]) + : null; + triggerContentUpdateDelay = b.mTriggerContentUpdateDelay; + triggerContentMaxDelay = b.mTriggerContentMaxDelay; + networkRequest = b.mNetworkRequest; + networkDownloadBytes = b.mNetworkDownloadBytes; + networkUploadBytes = b.mNetworkUploadBytes; + minLatencyMillis = b.mMinLatencyMillis; + maxExecutionDelayMillis = b.mMaxExecutionDelayMillis; + isPeriodic = b.mIsPeriodic; + isPersisted = b.mIsPersisted; + intervalMillis = b.mIntervalMillis; + flexMillis = b.mFlexMillis; + initialBackoffMillis = b.mInitialBackoffMillis; + backoffPolicy = b.mBackoffPolicy; + hasEarlyConstraint = b.mHasEarlyConstraint; + hasLateConstraint = b.mHasLateConstraint; + priority = b.mPriority; + flags = b.mFlags; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(jobId); + out.writePersistableBundle(extras); + out.writeBundle(transientExtras); + if (clipData != null) { + out.writeInt(1); + clipData.writeToParcel(out, flags); + out.writeInt(clipGrantFlags); + } else { + out.writeInt(0); + } + out.writeParcelable(service, flags); + out.writeInt(constraintFlags); + out.writeTypedArray(triggerContentUris, flags); + out.writeLong(triggerContentUpdateDelay); + out.writeLong(triggerContentMaxDelay); + if (networkRequest != null) { + out.writeInt(1); + networkRequest.writeToParcel(out, flags); + } else { + out.writeInt(0); + } + out.writeLong(networkDownloadBytes); + out.writeLong(networkUploadBytes); + out.writeLong(minLatencyMillis); + out.writeLong(maxExecutionDelayMillis); + out.writeInt(isPeriodic ? 1 : 0); + out.writeInt(isPersisted ? 1 : 0); + out.writeLong(intervalMillis); + out.writeLong(flexMillis); + out.writeLong(initialBackoffMillis); + out.writeInt(backoffPolicy); + out.writeInt(hasEarlyConstraint ? 1 : 0); + out.writeInt(hasLateConstraint ? 1 : 0); + out.writeInt(priority); + out.writeInt(this.flags); + } + + public static final @android.annotation.NonNull Creator<JobInfo> CREATOR = new Creator<JobInfo>() { + @Override + public JobInfo createFromParcel(Parcel in) { + return new JobInfo(in); + } + + @Override + public JobInfo[] newArray(int size) { + return new JobInfo[size]; + } + }; + + @Override + public String toString() { + return "(job:" + jobId + "/" + service.flattenToShortString() + ")"; + } + + /** + * Information about a content URI modification that a job would like to + * trigger on. + */ + public static final class TriggerContentUri implements Parcelable { + private final Uri mUri; + private final int mFlags; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "FLAG_" }, value = { + FLAG_NOTIFY_FOR_DESCENDANTS, + }) + public @interface Flags { } + + /** + * Flag for trigger: also trigger if any descendants of the given URI change. + * Corresponds to the <var>notifyForDescendants</var> of + * {@link android.content.ContentResolver#registerContentObserver}. + */ + public static final int FLAG_NOTIFY_FOR_DESCENDANTS = 1<<0; + + /** + * Create a new trigger description. + * @param uri The URI to observe. Must be non-null. + * @param flags Flags for the observer. + */ + public TriggerContentUri(@NonNull Uri uri, @Flags int flags) { + mUri = Objects.requireNonNull(uri); + mFlags = flags; + } + + /** + * Return the Uri this trigger was created for. + */ + public Uri getUri() { + return mUri; + } + + /** + * Return the flags supplied for the trigger. + */ + public @Flags int getFlags() { + return mFlags; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof TriggerContentUri)) { + return false; + } + TriggerContentUri t = (TriggerContentUri) o; + return Objects.equals(t.mUri, mUri) && t.mFlags == mFlags; + } + + @Override + public int hashCode() { + return (mUri == null ? 0 : mUri.hashCode()) ^ mFlags; + } + + private TriggerContentUri(Parcel in) { + mUri = Uri.CREATOR.createFromParcel(in); + mFlags = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + mUri.writeToParcel(out, flags); + out.writeInt(mFlags); + } + + public static final @android.annotation.NonNull Creator<TriggerContentUri> CREATOR = new Creator<TriggerContentUri>() { + @Override + public TriggerContentUri createFromParcel(Parcel in) { + return new TriggerContentUri(in); + } + + @Override + public TriggerContentUri[] newArray(int size) { + return new TriggerContentUri[size]; + } + }; + } + + /** Builder class for constructing {@link JobInfo} objects. */ + public static final class Builder { + private final int mJobId; + private final ComponentName mJobService; + private PersistableBundle mExtras = PersistableBundle.EMPTY; + private Bundle mTransientExtras = Bundle.EMPTY; + private ClipData mClipData; + private int mClipGrantFlags; + private int mPriority = PRIORITY_DEFAULT; + private int mFlags; + // Requirements. + private int mConstraintFlags; + private NetworkRequest mNetworkRequest; + private long mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN; + private long mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN; + private ArrayList<TriggerContentUri> mTriggerContentUris; + private long mTriggerContentUpdateDelay = -1; + private long mTriggerContentMaxDelay = -1; + private boolean mIsPersisted; + // One-off parameters. + private long mMinLatencyMillis; + private long mMaxExecutionDelayMillis; + // Periodic parameters. + private boolean mIsPeriodic; + private boolean mHasEarlyConstraint; + private boolean mHasLateConstraint; + private long mIntervalMillis; + private long mFlexMillis; + // Back-off parameters. + private long mInitialBackoffMillis = DEFAULT_INITIAL_BACKOFF_MILLIS; + private int mBackoffPolicy = DEFAULT_BACKOFF_POLICY; + /** Easy way to track whether the client has tried to set a back-off policy. */ + private boolean mBackoffPolicySet = false; + + /** + * Initialize a new Builder to construct a {@link JobInfo}. + * + * @param jobId Application-provided id for this job. Subsequent calls to cancel, or + * jobs created with the same jobId, will update the pre-existing job with + * the same id. This ID must be unique across all clients of the same uid + * (not just the same package). You will want to make sure this is a stable + * id across app updates, so probably not based on a resource ID. + * @param jobService The endpoint that you implement that will receive the callback from the + * JobScheduler. + */ + public Builder(int jobId, @NonNull ComponentName jobService) { + mJobService = jobService; + mJobId = jobId; + } + + /** @hide */ + @UnsupportedAppUsage + public Builder setPriority(int priority) { + mPriority = priority; + return this; + } + + /** @hide */ + @UnsupportedAppUsage + public Builder setFlags(int flags) { + mFlags = flags; + return this; + } + + /** + * Set optional extras. This is persisted, so we only allow primitive types. + * @param extras Bundle containing extras you want the scheduler to hold on to for you. + * @see JobInfo#getExtras() + */ + public Builder setExtras(@NonNull PersistableBundle extras) { + mExtras = extras; + return this; + } + + /** + * Set optional transient extras. + * + * <p>Because setting this property is not compatible with persisted + * jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called.</p> + * + * @param extras Bundle containing extras you want the scheduler to hold on to for you. + * @see JobInfo#getTransientExtras() + */ + public Builder setTransientExtras(@NonNull Bundle extras) { + mTransientExtras = extras; + return this; + } + + /** + * Set a {@link ClipData} associated with this Job. + * + * <p>The main purpose of providing a ClipData is to allow granting of + * URI permissions for data associated with the clip. The exact kind + * of permission grant to perform is specified through <var>grantFlags</var>. + * + * <p>If the ClipData contains items that are Intents, any + * grant flags in those Intents will be ignored. Only flags provided as an argument + * to this method are respected, and will be applied to all Uri or + * Intent items in the clip (or sub-items of the clip). + * + * <p>Because setting this property is not compatible with persisted + * jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called.</p> + * + * @param clip The new clip to set. May be null to clear the current clip. + * @param grantFlags The desired permissions to grant for any URIs. This should be + * a combination of {@link android.content.Intent#FLAG_GRANT_READ_URI_PERMISSION}, + * {@link android.content.Intent#FLAG_GRANT_WRITE_URI_PERMISSION}, and + * {@link android.content.Intent#FLAG_GRANT_PREFIX_URI_PERMISSION}. + * @see JobInfo#getClipData() + * @see JobInfo#getClipGrantFlags() + */ + public Builder setClipData(@Nullable ClipData clip, int grantFlags) { + mClipData = clip; + mClipGrantFlags = grantFlags; + return this; + } + + /** + * Set basic description of the kind of network your job requires. If + * you need more precise control over network capabilities, see + * {@link #setRequiredNetwork(NetworkRequest)}. + * <p> + * If your job doesn't need a network connection, you don't need to call + * this method, as the default value is {@link #NETWORK_TYPE_NONE}. + * <p> + * Calling this method defines network as a strict requirement for your + * job. If the network requested is not available your job will never + * run. See {@link #setOverrideDeadline(long)} to change this behavior. + * Calling this method will override any requirements previously defined + * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only + * want to call one of these methods. + * <p class="note"> + * When your job executes in + * {@link JobService#onStartJob(JobParameters)}, be sure to use the + * specific network returned by {@link JobParameters#getNetwork()}, + * otherwise you'll use the default network which may not meet this + * constraint. + * + * @see #setRequiredNetwork(NetworkRequest) + * @see JobInfo#getNetworkType() + * @see JobParameters#getNetwork() + */ + public Builder setRequiredNetworkType(@NetworkType int networkType) { + if (networkType == NETWORK_TYPE_NONE) { + return setRequiredNetwork(null); + } else { + final NetworkRequest.Builder builder = new NetworkRequest.Builder(); + + // All types require validated Internet + builder.addCapability(NET_CAPABILITY_INTERNET); + builder.addCapability(NET_CAPABILITY_VALIDATED); + builder.removeCapability(NET_CAPABILITY_NOT_VPN); + + if (networkType == NETWORK_TYPE_ANY) { + // No other capabilities + } else if (networkType == NETWORK_TYPE_UNMETERED) { + builder.addCapability(NET_CAPABILITY_NOT_METERED); + } else if (networkType == NETWORK_TYPE_NOT_ROAMING) { + builder.addCapability(NET_CAPABILITY_NOT_ROAMING); + } else if (networkType == NETWORK_TYPE_CELLULAR) { + builder.addTransportType(TRANSPORT_CELLULAR); + } + + return setRequiredNetwork(builder.build()); + } + } + + /** + * Set detailed description of the kind of network your job requires. + * <p> + * If your job doesn't need a network connection, you don't need to call + * this method, as the default is {@code null}. + * <p> + * Calling this method defines network as a strict requirement for your + * job. If the network requested is not available your job will never + * run. See {@link #setOverrideDeadline(long)} to change this behavior. + * Calling this method will override any requirements previously defined + * by {@link #setRequiredNetworkType(int)}; you typically only want to + * call one of these methods. + * <p class="note"> + * When your job executes in + * {@link JobService#onStartJob(JobParameters)}, be sure to use the + * specific network returned by {@link JobParameters#getNetwork()}, + * otherwise you'll use the default network which may not meet this + * constraint. + * + * @param networkRequest The detailed description of the kind of network + * this job requires, or {@code null} if no specific kind of + * network is required. Defining a {@link NetworkSpecifier} + * is only supported for jobs that aren't persisted. + * @see #setRequiredNetworkType(int) + * @see JobInfo#getRequiredNetwork() + * @see JobParameters#getNetwork() + */ + public Builder setRequiredNetwork(@Nullable NetworkRequest networkRequest) { + mNetworkRequest = networkRequest; + return this; + } + + /** + * Set the estimated size of network traffic that will be performed by + * this job, in bytes. + * <p> + * Apps are encouraged to provide values that are as accurate as + * possible, but when the exact size isn't available, an + * order-of-magnitude estimate can be provided instead. Here are some + * specific examples: + * <ul> + * <li>A job that is backing up a photo knows the exact size of that + * photo, so it should provide that size as the estimate. + * <li>A job that refreshes top news stories wouldn't know an exact + * size, but if the size is expected to be consistently around 100KB, it + * can provide that order-of-magnitude value as the estimate. + * <li>A job that synchronizes email could end up using an extreme range + * of data, from under 1KB when nothing has changed, to dozens of MB + * when there are new emails with attachments. Jobs that cannot provide + * reasonable estimates should use the sentinel value + * {@link JobInfo#NETWORK_BYTES_UNKNOWN}. + * </ul> + * Note that the system may choose to delay jobs with large network + * usage estimates when the device has a poor network connection, in + * order to save battery. + * <p> + * The values provided here only reflect the traffic that will be + * performed by the base job; if you're using {@link JobWorkItem} then + * you also need to define the network traffic used by each work item + * when constructing them. + * + * @param downloadBytes The estimated size of network traffic that will + * be downloaded by this job, in bytes. + * @param uploadBytes The estimated size of network traffic that will be + * uploaded by this job, in bytes. + * @see JobInfo#getEstimatedNetworkDownloadBytes() + * @see JobInfo#getEstimatedNetworkUploadBytes() + * @see JobWorkItem#JobWorkItem(android.content.Intent, long, long) + */ + public Builder setEstimatedNetworkBytes(@BytesLong long downloadBytes, + @BytesLong long uploadBytes) { + mNetworkDownloadBytes = downloadBytes; + mNetworkUploadBytes = uploadBytes; + return this; + } + + /** + * Specify that to run this job, the device must be charging (or be a + * non-battery-powered device connected to permanent power, such as Android TV + * devices). This defaults to {@code false}. + * + * <p class="note">For purposes of running jobs, a battery-powered device + * "charging" is not quite the same as simply being connected to power. If the + * device is so busy that the battery is draining despite a power connection, jobs + * with this constraint will <em>not</em> run. This can happen during some + * common use cases such as video chat, particularly if the device is plugged in + * to USB rather than to wall power. + * + * @param requiresCharging Pass {@code true} to require that the device be + * charging in order to run the job. + * @see JobInfo#isRequireCharging() + */ + public Builder setRequiresCharging(boolean requiresCharging) { + mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_CHARGING) + | (requiresCharging ? CONSTRAINT_FLAG_CHARGING : 0); + return this; + } + + /** + * Specify that to run this job, the device's battery level must not be low. + * This defaults to false. If true, the job will only run when the battery level + * is not low, which is generally the point where the user is given a "low battery" + * warning. + * @param batteryNotLow Whether or not the device's battery level must not be low. + * @see JobInfo#isRequireBatteryNotLow() + */ + public Builder setRequiresBatteryNotLow(boolean batteryNotLow) { + mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_BATTERY_NOT_LOW) + | (batteryNotLow ? CONSTRAINT_FLAG_BATTERY_NOT_LOW : 0); + return this; + } + + /** + * When set {@code true}, ensure that this job will not run if the device is in active use. + * The default state is {@code false}: that is, the for the job to be runnable even when + * someone is interacting with the device. + * + * <p>This state is a loose definition provided by the system. In general, it means that + * the device is not currently being used interactively, and has not been in use for some + * time. As such, it is a good time to perform resource heavy jobs. Bear in mind that + * battery usage will still be attributed to your application, and surfaced to the user in + * battery stats.</p> + * + * <p class="note">Despite the similar naming, this job constraint is <em>not</em> + * related to the system's "device idle" or "doze" states. This constraint only + * determines whether a job is allowed to run while the device is directly in use. + * + * @param requiresDeviceIdle Pass {@code true} to prevent the job from running + * while the device is being used interactively. + * @see JobInfo#isRequireDeviceIdle() + */ + public Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) { + mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_DEVICE_IDLE) + | (requiresDeviceIdle ? CONSTRAINT_FLAG_DEVICE_IDLE : 0); + return this; + } + + /** + * Specify that to run this job, the device's available storage must not be low. + * This defaults to false. If true, the job will only run when the device is not + * in a low storage state, which is generally the point where the user is given a + * "low storage" warning. + * @param storageNotLow Whether or not the device's available storage must not be low. + * @see JobInfo#isRequireStorageNotLow() + */ + public Builder setRequiresStorageNotLow(boolean storageNotLow) { + mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_STORAGE_NOT_LOW) + | (storageNotLow ? CONSTRAINT_FLAG_STORAGE_NOT_LOW : 0); + return this; + } + + /** + * Add a new content: URI that will be monitored with a + * {@link android.database.ContentObserver}, and will cause the job to execute if changed. + * If you have any trigger content URIs associated with a job, it will not execute until + * there has been a change report for one or more of them. + * + * <p>Note that trigger URIs can not be used in combination with + * {@link #setPeriodic(long)} or {@link #setPersisted(boolean)}. To continually monitor + * for content changes, you need to schedule a new JobInfo observing the same URIs + * before you finish execution of the JobService handling the most recent changes. + * Following this pattern will ensure you do not lose any content changes: while your + * job is running, the system will continue monitoring for content changes, and propagate + * any it sees over to the next job you schedule.</p> + * + * <p>Because setting this property is not compatible with periodic or + * persisted jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called.</p> + * + * <p>The following example shows how this feature can be used to monitor for changes + * in the photos on a device.</p> + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/PhotosContentJob.java + * job} + * + * @param uri The content: URI to monitor. + * @see JobInfo#getTriggerContentUris() + */ + public Builder addTriggerContentUri(@NonNull TriggerContentUri uri) { + if (mTriggerContentUris == null) { + mTriggerContentUris = new ArrayList<>(); + } + mTriggerContentUris.add(uri); + return this; + } + + /** + * Set the delay (in milliseconds) from when a content change is detected until + * the job is scheduled. If there are more changes during that time, the delay + * will be reset to start at the time of the most recent change. + * @param durationMs Delay after most recent content change, in milliseconds. + * @see JobInfo#getTriggerContentUpdateDelay() + */ + public Builder setTriggerContentUpdateDelay(long durationMs) { + mTriggerContentUpdateDelay = durationMs; + return this; + } + + /** + * Set the maximum total delay (in milliseconds) that is allowed from the first + * time a content change is detected until the job is scheduled. + * @param durationMs Delay after initial content change, in milliseconds. + * @see JobInfo#getTriggerContentMaxDelay() + */ + public Builder setTriggerContentMaxDelay(long durationMs) { + mTriggerContentMaxDelay = durationMs; + return this; + } + + /** + * Specify that this job should recur with the provided interval, not more than once per + * period. You have no control over when within this interval this job will be executed, + * only the guarantee that it will be executed at most once within this interval. + * Setting this function on the builder with {@link #setMinimumLatency(long)} or + * {@link #setOverrideDeadline(long)} will result in an error. + * @param intervalMillis Millisecond interval for which this job will repeat. + * @see JobInfo#getIntervalMillis() + * @see JobInfo#getFlexMillis() + */ + public Builder setPeriodic(long intervalMillis) { + return setPeriodic(intervalMillis, intervalMillis); + } + + /** + * Specify that this job should recur with the provided interval and flex. The job can + * execute at any time in a window of flex length at the end of the period. + * @param intervalMillis Millisecond interval for which this job will repeat. A minimum + * value of {@link #getMinPeriodMillis()} is enforced. + * @param flexMillis Millisecond flex for this job. Flex is clamped to be at least + * {@link #getMinFlexMillis()} or 5 percent of the period, whichever is + * higher. + * @see JobInfo#getIntervalMillis() + * @see JobInfo#getFlexMillis() + */ + public Builder setPeriodic(long intervalMillis, long flexMillis) { + final long minPeriod = getMinPeriodMillis(); + if (intervalMillis < minPeriod) { + Log.w(TAG, "Requested interval " + formatDuration(intervalMillis) + " for job " + + mJobId + " is too small; raising to " + formatDuration(minPeriod)); + intervalMillis = minPeriod; + } + + final long percentClamp = 5 * intervalMillis / 100; + final long minFlex = Math.max(percentClamp, getMinFlexMillis()); + if (flexMillis < minFlex) { + Log.w(TAG, "Requested flex " + formatDuration(flexMillis) + " for job " + mJobId + + " is too small; raising to " + formatDuration(minFlex)); + flexMillis = minFlex; + } + + mIsPeriodic = true; + mIntervalMillis = intervalMillis; + mFlexMillis = flexMillis; + mHasEarlyConstraint = mHasLateConstraint = true; + return this; + } + + /** + * Specify that this job should be delayed by the provided amount of time. + * Because it doesn't make sense setting this property on a periodic job, doing so will + * throw an {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called. + * @param minLatencyMillis Milliseconds before which this job will not be considered for + * execution. + * @see JobInfo#getMinLatencyMillis() + */ + public Builder setMinimumLatency(long minLatencyMillis) { + mMinLatencyMillis = minLatencyMillis; + mHasEarlyConstraint = true; + return this; + } + + /** + * Set deadline which is the maximum scheduling latency. The job will be run by this + * deadline even if other requirements are not met. Because it doesn't make sense setting + * this property on a periodic job, doing so will throw an + * {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called. + * @see JobInfo#getMaxExecutionDelayMillis() + */ + public Builder setOverrideDeadline(long maxExecutionDelayMillis) { + mMaxExecutionDelayMillis = maxExecutionDelayMillis; + mHasLateConstraint = true; + return this; + } + + /** + * Set up the back-off/retry policy. + * This defaults to some respectable values: {30 seconds, Exponential}. We cap back-off at + * 5hrs. + * Note that trying to set a backoff criteria for a job with + * {@link #setRequiresDeviceIdle(boolean)} will throw an exception when you call build(). + * This is because back-off typically does not make sense for these types of jobs. See + * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)} + * for more description of the return value for the case of a job executing while in idle + * mode. + * @param initialBackoffMillis Millisecond time interval to wait initially when job has + * failed. + * @see JobInfo#getInitialBackoffMillis() + * @see JobInfo#getBackoffPolicy() + */ + public Builder setBackoffCriteria(long initialBackoffMillis, + @BackoffPolicy int backoffPolicy) { + final long minBackoff = getMinBackoffMillis(); + if (initialBackoffMillis < minBackoff) { + Log.w(TAG, "Requested backoff " + formatDuration(initialBackoffMillis) + " for job " + + mJobId + " is too small; raising to " + formatDuration(minBackoff)); + initialBackoffMillis = minBackoff; + } + + mBackoffPolicySet = true; + mInitialBackoffMillis = initialBackoffMillis; + mBackoffPolicy = backoffPolicy; + return this; + } + + /** + * Setting this to true indicates that this job is important while the scheduling app + * is in the foreground or on the temporary whitelist for background restrictions. + * This means that the system will relax doze restrictions on this job during this time. + * + * Apps should use this flag only for short jobs that are essential for the app to function + * properly in the foreground. + * + * Note that once the scheduling app is no longer whitelisted from background restrictions + * and in the background, or the job failed due to unsatisfied constraints, + * this job should be expected to behave like other jobs without this flag. + * + * @param importantWhileForeground whether to relax doze restrictions for this job when the + * app is in the foreground. False by default. + * @see JobInfo#isImportantWhileForeground() + */ + public Builder setImportantWhileForeground(boolean importantWhileForeground) { + if (importantWhileForeground) { + mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND; + } else { + mFlags &= (~FLAG_IMPORTANT_WHILE_FOREGROUND); + } + return this; + } + + /** + * Setting this to true indicates that this job is designed to prefetch + * content that will make a material improvement to the experience of + * the specific user of this device. For example, fetching top headlines + * of interest to the current user. + * <p> + * The system may use this signal to relax the network constraints you + * originally requested, such as allowing a + * {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over a metered + * network when there is a surplus of metered data available. The system + * may also use this signal in combination with end user usage patterns + * to ensure data is prefetched before the user launches your app. + * @see JobInfo#isPrefetch() + */ + public Builder setPrefetch(boolean prefetch) { + if (prefetch) { + mFlags |= FLAG_PREFETCH; + } else { + mFlags &= (~FLAG_PREFETCH); + } + return this; + } + + /** + * Set whether or not to persist this job across device reboots. + * + * @param isPersisted True to indicate that the job will be written to + * disk and loaded at boot. + * @see JobInfo#isPersisted() + */ + @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED) + public Builder setPersisted(boolean isPersisted) { + mIsPersisted = isPersisted; + return this; + } + + /** + * @return The job object to hand to the JobScheduler. This object is immutable. + */ + public JobInfo build() { + // Check that network estimates require network type + if ((mNetworkDownloadBytes > 0 || mNetworkUploadBytes > 0) && mNetworkRequest == null) { + throw new IllegalArgumentException( + "Can't provide estimated network usage without requiring a network"); + } + // We can't serialize network specifiers + if (mIsPersisted && mNetworkRequest != null + && mNetworkRequest.networkCapabilities.getNetworkSpecifier() != null) { + throw new IllegalArgumentException( + "Network specifiers aren't supported for persistent jobs"); + } + // Check that a deadline was not set on a periodic job. + if (mIsPeriodic) { + if (mMaxExecutionDelayMillis != 0L) { + throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " + + "periodic job."); + } + if (mMinLatencyMillis != 0L) { + throw new IllegalArgumentException("Can't call setMinimumLatency() on a " + + "periodic job"); + } + if (mTriggerContentUris != null) { + throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " + + "periodic job"); + } + } + if (mIsPersisted) { + if (mTriggerContentUris != null) { + throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " + + "persisted job"); + } + if (!mTransientExtras.isEmpty()) { + throw new IllegalArgumentException("Can't call setTransientExtras() on a " + + "persisted job"); + } + if (mClipData != null) { + throw new IllegalArgumentException("Can't call setClipData() on a " + + "persisted job"); + } + } + if ((mFlags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0 && mHasEarlyConstraint) { + throw new IllegalArgumentException("An important while foreground job cannot " + + "have a time delay"); + } + if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) { + throw new IllegalArgumentException("An idle mode job will not respect any" + + " back-off policy, so calling setBackoffCriteria with" + + " setRequiresDeviceIdle is an error."); + } + return new JobInfo(this); + } + + /** + * @hide + */ + public String summarize() { + final String service = (mJobService != null) + ? mJobService.flattenToShortString() + : "null"; + return "JobInfo.Builder{job:" + mJobId + "/" + service + "}"; + } + } + + /** + * Convert a priority integer into a human readable string for debugging. + * @hide + */ + public static String getPriorityString(int priority) { + switch (priority) { + case PRIORITY_DEFAULT: + return PRIORITY_DEFAULT + " [DEFAULT]"; + case PRIORITY_SYNC_EXPEDITED: + return PRIORITY_SYNC_EXPEDITED + " [SYNC_EXPEDITED]"; + case PRIORITY_SYNC_INITIALIZATION: + return PRIORITY_SYNC_INITIALIZATION + " [SYNC_INITIALIZATION]"; + case PRIORITY_BOUND_FOREGROUND_SERVICE: + return PRIORITY_BOUND_FOREGROUND_SERVICE + " [BFGS_APP]"; + case PRIORITY_FOREGROUND_SERVICE: + return PRIORITY_FOREGROUND_SERVICE + " [FGS_APP]"; + case PRIORITY_TOP_APP: + return PRIORITY_TOP_APP + " [TOP_APP]"; + + // PRIORITY_ADJ_* are adjustments and not used as real priorities. + // No need to convert to strings. + } + return priority + " [UNKNOWN]"; + } +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.aidl b/apex/jobscheduler/framework/java/android/app/job/JobParameters.aidl new file mode 100644 index 000000000000..e7551b9ab9f2 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.aidl @@ -0,0 +1,19 @@ +/** + * Copyright 2014, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.job; + +parcelable JobParameters; diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java new file mode 100644 index 000000000000..62c90dfa8a86 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.app.job; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.ClipData; +import android.net.Network; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.os.RemoteException; + +/** + * Contains the parameters used to configure/identify your job. You do not create this object + * yourself, instead it is handed in to your application by the System. + */ +public class JobParameters implements Parcelable { + + /** @hide */ + public static final int REASON_CANCELED = JobProtoEnums.STOP_REASON_CANCELLED; // 0. + /** @hide */ + public static final int REASON_CONSTRAINTS_NOT_SATISFIED = + JobProtoEnums.STOP_REASON_CONSTRAINTS_NOT_SATISFIED; //1. + /** @hide */ + public static final int REASON_PREEMPT = JobProtoEnums.STOP_REASON_PREEMPT; // 2. + /** @hide */ + public static final int REASON_TIMEOUT = JobProtoEnums.STOP_REASON_TIMEOUT; // 3. + /** @hide */ + public static final int REASON_DEVICE_IDLE = JobProtoEnums.STOP_REASON_DEVICE_IDLE; // 4. + /** @hide */ + public static final int REASON_DEVICE_THERMAL = JobProtoEnums.STOP_REASON_DEVICE_THERMAL; // 5. + /** + * The job is in the {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} + * bucket. + * + * @hide + */ + public static final int REASON_RESTRICTED_BUCKET = + JobProtoEnums.STOP_REASON_RESTRICTED_BUCKET; // 6. + + /** + * All the stop reason codes. This should be regarded as an immutable array at runtime. + * + * Note the order of these values will affect "dumpsys batterystats", and we do not want to + * change the order of existing fields, so adding new fields is okay but do not remove or + * change existing fields. When deprecating a field, just replace that with "-1" in this array. + * + * @hide + */ + public static final int[] JOB_STOP_REASON_CODES = { + REASON_CANCELED, + REASON_CONSTRAINTS_NOT_SATISFIED, + REASON_PREEMPT, + REASON_TIMEOUT, + REASON_DEVICE_IDLE, + REASON_DEVICE_THERMAL, + REASON_RESTRICTED_BUCKET, + }; + + /** + * @hide + */ + // TODO(142420609): make it @SystemApi for mainline + @NonNull + public static String getReasonCodeDescription(int reasonCode) { + switch (reasonCode) { + case REASON_CANCELED: return "canceled"; + case REASON_CONSTRAINTS_NOT_SATISFIED: return "constraints"; + case REASON_PREEMPT: return "preempt"; + case REASON_TIMEOUT: return "timeout"; + case REASON_DEVICE_IDLE: return "device_idle"; + case REASON_DEVICE_THERMAL: return "thermal"; + case REASON_RESTRICTED_BUCKET: return "restricted_bucket"; + default: return "unknown:" + reasonCode; + } + } + + /** @hide */ + // @SystemApi TODO make it a system api for mainline + @NonNull + public static int[] getJobStopReasonCodes() { + return JOB_STOP_REASON_CODES; + } + + @UnsupportedAppUsage + private final int jobId; + private final PersistableBundle extras; + private final Bundle transientExtras; + private final ClipData clipData; + private final int clipGrantFlags; + @UnsupportedAppUsage + private final IBinder callback; + private final boolean overrideDeadlineExpired; + private final Uri[] mTriggeredContentUris; + private final String[] mTriggeredContentAuthorities; + private final Network network; + + private int stopReason; // Default value of stopReason is REASON_CANCELED + private String debugStopReason; // Human readable stop reason for debugging. + + /** @hide */ + public JobParameters(IBinder callback, int jobId, PersistableBundle extras, + Bundle transientExtras, ClipData clipData, int clipGrantFlags, + boolean overrideDeadlineExpired, Uri[] triggeredContentUris, + String[] triggeredContentAuthorities, Network network) { + this.jobId = jobId; + this.extras = extras; + this.transientExtras = transientExtras; + this.clipData = clipData; + this.clipGrantFlags = clipGrantFlags; + this.callback = callback; + this.overrideDeadlineExpired = overrideDeadlineExpired; + this.mTriggeredContentUris = triggeredContentUris; + this.mTriggeredContentAuthorities = triggeredContentAuthorities; + this.network = network; + } + + /** + * @return The unique id of this job, specified at creation time. + */ + public int getJobId() { + return jobId; + } + + /** + * Reason onStopJob() was called on this job. + * @hide + */ + public int getStopReason() { + return stopReason; + } + + /** + * Reason onStopJob() was called on this job. + * @hide + */ + public String getDebugStopReason() { + return debugStopReason; + } + + /** + * @return The extras you passed in when constructing this job with + * {@link android.app.job.JobInfo.Builder#setExtras(android.os.PersistableBundle)}. This will + * never be null. If you did not set any extras this will be an empty bundle. + */ + public @NonNull PersistableBundle getExtras() { + return extras; + } + + /** + * @return The transient extras you passed in when constructing this job with + * {@link android.app.job.JobInfo.Builder#setTransientExtras(android.os.Bundle)}. This will + * never be null. If you did not set any extras this will be an empty bundle. + */ + public @NonNull Bundle getTransientExtras() { + return transientExtras; + } + + /** + * @return The clip you passed in when constructing this job with + * {@link android.app.job.JobInfo.Builder#setClipData(ClipData, int)}. Will be null + * if it was not set. + */ + public @Nullable ClipData getClipData() { + return clipData; + } + + /** + * @return The clip grant flags you passed in when constructing this job with + * {@link android.app.job.JobInfo.Builder#setClipData(ClipData, int)}. Will be 0 + * if it was not set. + */ + public int getClipGrantFlags() { + return clipGrantFlags; + } + + /** + * For jobs with {@link android.app.job.JobInfo.Builder#setOverrideDeadline(long)} set, this + * provides an easy way to tell whether the job is being executed due to the deadline + * expiring. Note: If the job is running because its deadline expired, it implies that its + * constraints will not be met. + */ + public boolean isOverrideDeadlineExpired() { + return overrideDeadlineExpired; + } + + /** + * For jobs with {@link android.app.job.JobInfo.Builder#addTriggerContentUri} set, this + * reports which URIs have triggered the job. This will be null if either no URIs have + * triggered it (it went off due to a deadline or other reason), or the number of changed + * URIs is too large to report. Whether or not the number of URIs is too large, you can + * always use {@link #getTriggeredContentAuthorities()} to determine whether the job was + * triggered due to any content changes and the authorities they are associated with. + */ + public @Nullable Uri[] getTriggeredContentUris() { + return mTriggeredContentUris; + } + + /** + * For jobs with {@link android.app.job.JobInfo.Builder#addTriggerContentUri} set, this + * reports which content authorities have triggered the job. It will only be null if no + * authorities have triggered it -- that is, the job executed for some other reason, such + * as a deadline expiring. If this is non-null, you can use {@link #getTriggeredContentUris()} + * to retrieve the details of which URIs changed (as long as that has not exceeded the maximum + * number it can reported). + */ + public @Nullable String[] getTriggeredContentAuthorities() { + return mTriggeredContentAuthorities; + } + + /** + * Return the network that should be used to perform any network requests + * for this job. + * <p> + * Devices may have multiple active network connections simultaneously, or + * they may not have a default network route at all. To correctly handle all + * situations like this, your job should always use the network returned by + * this method instead of implicitly using the default network route. + * <p> + * Note that the system may relax the constraints you originally requested, + * such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over + * a metered network when there is a surplus of metered data available. + * + * @return the network that should be used to perform any network requests + * for this job, or {@code null} if this job didn't set any required + * network type. + * @see JobInfo.Builder#setRequiredNetworkType(int) + */ + public @Nullable Network getNetwork() { + return network; + } + + /** + * Dequeue the next pending {@link JobWorkItem} from these JobParameters associated with their + * currently running job. Calling this method when there is no more work available and all + * previously dequeued work has been completed will result in the system taking care of + * stopping the job for you -- + * you should not call {@link JobService#jobFinished(JobParameters, boolean)} yourself + * (otherwise you risk losing an upcoming JobWorkItem that is being enqueued at the same time). + * + * <p>Once you are done with the {@link JobWorkItem} returned by this method, you must call + * {@link #completeWork(JobWorkItem)} with it to inform the system that you are done + * executing the work. The job will not be finished until all dequeued work has been + * completed. You do not, however, have to complete each returned work item before deqeueing + * the next one -- you can use {@link #dequeueWork()} multiple times before completing + * previous work if you want to process work in parallel, and you can complete the work + * in whatever order you want.</p> + * + * <p>If the job runs to the end of its available time period before all work has been + * completed, it will stop as normal. You should return true from + * {@link JobService#onStopJob(JobParameters)} in order to have the job rescheduled, and by + * doing so any pending as well as remaining uncompleted work will be re-queued + * for the next time the job runs.</p> + * + * <p>This example shows how to construct a JobService that will serially dequeue and + * process work that is available for it:</p> + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/JobWorkService.java + * service} + * + * @return Returns a new {@link JobWorkItem} if there is one pending, otherwise null. + * If null is returned, the system will also stop the job if all work has also been completed. + * (This means that for correct operation, you must always call dequeueWork() after you have + * completed other work, to check either for more work or allow the system to stop the job.) + */ + public @Nullable JobWorkItem dequeueWork() { + try { + return getCallback().dequeueWork(getJobId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Report the completion of executing a {@link JobWorkItem} previously returned by + * {@link #dequeueWork()}. This tells the system you are done with the + * work associated with that item, so it will not be returned again. Note that if this + * is the last work in the queue, completing it here will <em>not</em> finish the overall + * job -- for that to happen, you still need to call {@link #dequeueWork()} + * again. + * + * <p>If you are enqueueing work into a job, you must call this method for each piece + * of work you process. Do <em>not</em> call + * {@link JobService#jobFinished(JobParameters, boolean)} + * or else you can lose work in your queue.</p> + * + * @param work The work you have completed processing, as previously returned by + * {@link #dequeueWork()} + */ + public void completeWork(@NonNull JobWorkItem work) { + try { + if (!getCallback().completeWork(getJobId(), work.getWorkId())) { + throw new IllegalArgumentException("Given work is not active: " + work); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide */ + @UnsupportedAppUsage + public IJobCallback getCallback() { + return IJobCallback.Stub.asInterface(callback); + } + + private JobParameters(Parcel in) { + jobId = in.readInt(); + extras = in.readPersistableBundle(); + transientExtras = in.readBundle(); + if (in.readInt() != 0) { + clipData = ClipData.CREATOR.createFromParcel(in); + clipGrantFlags = in.readInt(); + } else { + clipData = null; + clipGrantFlags = 0; + } + callback = in.readStrongBinder(); + overrideDeadlineExpired = in.readInt() == 1; + mTriggeredContentUris = in.createTypedArray(Uri.CREATOR); + mTriggeredContentAuthorities = in.createStringArray(); + if (in.readInt() != 0) { + network = Network.CREATOR.createFromParcel(in); + } else { + network = null; + } + stopReason = in.readInt(); + debugStopReason = in.readString(); + } + + /** @hide */ + public void setStopReason(int reason, String debugStopReason) { + stopReason = reason; + this.debugStopReason = debugStopReason; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(jobId); + dest.writePersistableBundle(extras); + dest.writeBundle(transientExtras); + if (clipData != null) { + dest.writeInt(1); + clipData.writeToParcel(dest, flags); + dest.writeInt(clipGrantFlags); + } else { + dest.writeInt(0); + } + dest.writeStrongBinder(callback); + dest.writeInt(overrideDeadlineExpired ? 1 : 0); + dest.writeTypedArray(mTriggeredContentUris, flags); + dest.writeStringArray(mTriggeredContentAuthorities); + if (network != null) { + dest.writeInt(1); + network.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } + dest.writeInt(stopReason); + dest.writeString(debugStopReason); + } + + public static final @android.annotation.NonNull Creator<JobParameters> CREATOR = new Creator<JobParameters>() { + @Override + public JobParameters createFromParcel(Parcel in) { + return new JobParameters(in); + } + + @Override + public JobParameters[] newArray(int size) { + return new JobParameters[size]; + } + }; +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java new file mode 100644 index 000000000000..42725c51fd87 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.app.job; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.content.ClipData; +import android.content.Context; +import android.os.Bundle; +import android.os.PersistableBundle; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +/** + * This is an API for scheduling various types of jobs against the framework that will be executed + * in your application's own process. + * <p> + * See {@link android.app.job.JobInfo} for more description of the types of jobs that can be run + * and how to construct them. You will construct these JobInfo objects and pass them to the + * JobScheduler with {@link #schedule(JobInfo)}. When the criteria declared are met, the + * system will execute this job on your application's {@link android.app.job.JobService}. + * You identify the service component that implements the logic for your job when you + * construct the JobInfo using + * {@link android.app.job.JobInfo.Builder#Builder(int,android.content.ComponentName)}. + * </p> + * <p> + * The framework will be intelligent about when it executes jobs, and attempt to batch + * and defer them as much as possible. Typically if you don't specify a deadline on a job, it + * can be run at any moment depending on the current state of the JobScheduler's internal queue. + * <p> + * While a job is running, the system holds a wakelock on behalf of your app. For this reason, + * you do not need to take any action to guarantee that the device stays awake for the + * duration of the job. + * </p> + * <p>You do not + * instantiate this class directly; instead, retrieve it through + * {@link android.content.Context#getSystemService + * Context.getSystemService(Context.JOB_SCHEDULER_SERVICE)}. + * + * <p class="caution"><strong>Note:</strong> Beginning with API 30 + * ({@link android.os.Build.VERSION_CODES#R}), JobScheduler will throttle runaway applications. + * Calling {@link #schedule(JobInfo)} and other such methods with very high frequency can have a + * high cost and so, to make sure the system doesn't get overwhelmed, JobScheduler will begin + * to throttle apps, regardless of target SDK version. + */ +@SystemService(Context.JOB_SCHEDULER_SERVICE) +public abstract class JobScheduler { + /** @hide */ + @IntDef(prefix = { "RESULT_" }, value = { + RESULT_FAILURE, + RESULT_SUCCESS, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Result {} + + /** + * Returned from {@link #schedule(JobInfo)} if a job wasn't scheduled successfully. Scheduling + * can fail for a variety of reasons, including, but not limited to: + * <ul> + * <li>an invalid parameter was supplied (eg. the run-time for your job is too short, or the + * system can't resolve the requisite {@link JobService} in your package)</li> + * <li>the app has too many jobs scheduled</li> + * <li>the app has tried to schedule too many jobs in a short amount of time</li> + * </ul> + * Attempting to schedule the job again immediately after receiving this result will not + * guarantee a successful schedule. + */ + public static final int RESULT_FAILURE = 0; + /** + * Returned from {@link #schedule(JobInfo)} if this job has been successfully scheduled. + */ + public static final int RESULT_SUCCESS = 1; + + /** + * Schedule a job to be executed. Will replace any currently scheduled job with the same + * ID with the new information in the {@link JobInfo}. If a job with the given ID is currently + * running, it will be stopped. + * + * <p class="caution"><strong>Note:</strong> Scheduling a job can have a high cost, even if it's + * rescheduling the same job and the job didn't execute, especially on platform versions before + * version {@link android.os.Build.VERSION_CODES#Q}. As such, the system may throttle calls to + * this API if calls are made too frequently in a short amount of time. + * + * @param job The job you wish scheduled. See + * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs + * you can schedule. + * @return the result of the schedule request. + */ + public abstract @Result int schedule(@NonNull JobInfo job); + + /** + * Similar to {@link #schedule}, but allows you to enqueue work for a new <em>or existing</em> + * job. If a job with the same ID is already scheduled, it will be replaced with the + * new {@link JobInfo}, but any previously enqueued work will remain and be dispatched the + * next time it runs. If a job with the same ID is already running, the new work will be + * enqueued for it. + * + * <p>The work you enqueue is later retrieved through + * {@link JobParameters#dequeueWork() JobParameters.dequeueWork}. Be sure to see there + * about how to process work; the act of enqueueing work changes how you should handle the + * overall lifecycle of an executing job.</p> + * + * <p>It is strongly encouraged that you use the same {@link JobInfo} for all work you + * enqueue. This will allow the system to optimally schedule work along with any pending + * and/or currently running work. If the JobInfo changes from the last time the job was + * enqueued, the system will need to update the associated JobInfo, which can cause a disruption + * in execution. In particular, this can result in any currently running job that is processing + * previous work to be stopped and restarted with the new JobInfo.</p> + * + * <p>It is recommended that you avoid using + * {@link JobInfo.Builder#setExtras(PersistableBundle)} or + * {@link JobInfo.Builder#setTransientExtras(Bundle)} with a JobInfo you are using to + * enqueue work. The system will try to compare these extras with the previous JobInfo, + * but there are situations where it may get this wrong and count the JobInfo as changing. + * (That said, you should be relatively safe with a simple set of consistent data in these + * fields.) You should never use {@link JobInfo.Builder#setClipData(ClipData, int)} with + * work you are enqueue, since currently this will always be treated as a different JobInfo, + * even if the ClipData contents are exactly the same.</p> + * + * @param job The job you wish to enqueue work for. See + * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs + * you can schedule. + * @param work New work to enqueue. This will be available later when the job starts running. + * @return the result of the enqueue request. + */ + public abstract @Result int enqueue(@NonNull JobInfo job, @NonNull JobWorkItem work); + + /** + * + * @param job The job to be scheduled. + * @param packageName The package on behalf of which the job is to be scheduled. This will be + * used to track battery usage and appIdleState. + * @param userId User on behalf of whom this job is to be scheduled. + * @param tag Debugging tag for dumps associated with this job (instead of the service class) + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) + public abstract @Result int scheduleAsPackage(@NonNull JobInfo job, @NonNull String packageName, + int userId, String tag); + + /** + * Cancel the specified job. If the job is currently executing, it is stopped + * immediately and the return value from its {@link JobService#onStopJob(JobParameters)} + * method is ignored. + * + * @param jobId unique identifier for the job to be canceled, as supplied to + * {@link JobInfo.Builder#Builder(int, android.content.ComponentName) + * JobInfo.Builder(int, android.content.ComponentName)}. + */ + public abstract void cancel(int jobId); + + /** + * Cancel <em>all</em> jobs that have been scheduled by the calling application. + */ + public abstract void cancelAll(); + + /** + * Retrieve all jobs that have been scheduled by the calling application. + * + * @return a list of all of the app's scheduled jobs. This includes jobs that are + * currently started as well as those that are still waiting to run. + */ + public abstract @NonNull List<JobInfo> getAllPendingJobs(); + + /** + * Look up the description of a scheduled job. + * + * @return The {@link JobInfo} description of the given scheduled job, or {@code null} + * if the supplied job ID does not correspond to any job. + */ + public abstract @Nullable JobInfo getPendingJob(int jobId); + + /** + * <b>For internal system callers only!</b> + * Returns a list of all currently-executing jobs. + * @hide + */ + public abstract List<JobInfo> getStartedJobs(); + + /** + * <b>For internal system callers only!</b> + * Returns a snapshot of the state of all jobs known to the system. + * + * <p class="note">This is a slow operation, so it should be called sparingly. + * @hide + */ + public abstract List<JobSnapshot> getAllJobSnapshots(); +}
\ No newline at end of file diff --git a/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java b/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java new file mode 100644 index 000000000000..0c4fcb4ec1b0 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.job; + +import android.annotation.SystemApi; +import android.app.JobSchedulerImpl; +import android.app.SystemServiceRegistry; +import android.content.Context; +import android.os.DeviceIdleManager; +import android.os.IDeviceIdleController; +import android.os.PowerWhitelistManager; + +/** + * Class holding initialization code for the job scheduler module. + * + * @hide + */ +@SystemApi +public class JobSchedulerFrameworkInitializer { + private JobSchedulerFrameworkInitializer() { + } + + /** + * Called by {@link SystemServiceRegistry}'s static initializer and registers + * {@link JobScheduler} and other services to {@link Context}, so + * {@link Context#getSystemService} can return them. + * + * <p>If this is called from other places, it throws a {@link IllegalStateException). + */ + public static void registerServiceWrappers() { + SystemServiceRegistry.registerStaticService( + Context.JOB_SCHEDULER_SERVICE, JobScheduler.class, + (b) -> new JobSchedulerImpl(IJobScheduler.Stub.asInterface(b))); + SystemServiceRegistry.registerContextAwareService( + Context.DEVICE_IDLE_CONTROLLER, DeviceIdleManager.class, + (context, b) -> new DeviceIdleManager( + context, IDeviceIdleController.Stub.asInterface(b))); + SystemServiceRegistry.registerContextAwareService( + Context.POWER_WHITELIST_MANAGER, PowerWhitelistManager.class, + PowerWhitelistManager::new); + } +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java new file mode 100644 index 000000000000..61afadab9b0c --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.app.job; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +/** + * <p>Entry point for the callback from the {@link android.app.job.JobScheduler}.</p> + * <p>This is the base class that handles asynchronous requests that were previously scheduled. You + * are responsible for overriding {@link JobService#onStartJob(JobParameters)}, which is where + * you will implement your job logic.</p> + * <p>This service executes each incoming job on a {@link android.os.Handler} running on your + * application's main thread. This means that you <b>must</b> offload your execution logic to + * another thread/handler/{@link android.os.AsyncTask} of your choosing. Not doing so will result + * in blocking any future callbacks from the JobManager - specifically + * {@link #onStopJob(android.app.job.JobParameters)}, which is meant to inform you that the + * scheduling requirements are no longer being met.</p> + */ +public abstract class JobService extends Service { + private static final String TAG = "JobService"; + + /** + * Job services must be protected with this permission: + * + * <pre class="prettyprint"> + * <service android:name="MyJobService" + * android:permission="android.permission.BIND_JOB_SERVICE" > + * ... + * </service> + * </pre> + * + * <p>If a job service is declared in the manifest but not protected with this + * permission, that service will be ignored by the system. + */ + public static final String PERMISSION_BIND = + "android.permission.BIND_JOB_SERVICE"; + + private JobServiceEngine mEngine; + + /** @hide */ + public final IBinder onBind(Intent intent) { + if (mEngine == null) { + mEngine = new JobServiceEngine(this) { + @Override + public boolean onStartJob(JobParameters params) { + return JobService.this.onStartJob(params); + } + + @Override + public boolean onStopJob(JobParameters params) { + return JobService.this.onStopJob(params); + } + }; + } + return mEngine.getBinder(); + } + + /** + * Call this to inform the JobScheduler that the job has finished its work. When the + * system receives this message, it releases the wakelock being held for the job. + * <p> + * You can request that the job be scheduled again by passing {@code true} as + * the <code>wantsReschedule</code> parameter. This will apply back-off policy + * for the job; this policy can be adjusted through the + * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} method + * when the job is originally scheduled. The job's initial + * requirements are preserved when jobs are rescheduled, regardless of backed-off + * policy. + * <p class="note"> + * A job running while the device is dozing will not be rescheduled with the normal back-off + * policy. Instead, the job will be re-added to the queue and executed again during + * a future idle maintenance window. + * </p> + * + * @param params The parameters identifying this job, as supplied to + * the job in the {@link #onStartJob(JobParameters)} callback. + * @param wantsReschedule {@code true} if this job should be rescheduled according + * to the back-off criteria specified when it was first scheduled; {@code false} + * otherwise. + */ + public final void jobFinished(JobParameters params, boolean wantsReschedule) { + mEngine.jobFinished(params, wantsReschedule); + } + + /** + * Called to indicate that the job has begun executing. Override this method with the + * logic for your job. Like all other component lifecycle callbacks, this method executes + * on your application's main thread. + * <p> + * Return {@code true} from this method if your job needs to continue running. If you + * do this, the job remains active until you call + * {@link #jobFinished(JobParameters, boolean)} to tell the system that it has completed + * its work, or until the job's required constraints are no longer satisfied. For + * example, if the job was scheduled using + * {@link JobInfo.Builder#setRequiresCharging(boolean) setRequiresCharging(true)}, + * it will be immediately halted by the system if the user unplugs the device from power, + * the job's {@link #onStopJob(JobParameters)} callback will be invoked, and the app + * will be expected to shut down all ongoing work connected with that job. + * <p> + * The system holds a wakelock on behalf of your app as long as your job is executing. + * This wakelock is acquired before this method is invoked, and is not released until either + * you call {@link #jobFinished(JobParameters, boolean)}, or after the system invokes + * {@link #onStopJob(JobParameters)} to notify your job that it is being shut down + * prematurely. + * <p> + * Returning {@code false} from this method means your job is already finished. The + * system's wakelock for the job will be released, and {@link #onStopJob(JobParameters)} + * will not be invoked. + * + * @param params Parameters specifying info about this job, including the optional + * extras configured with {@link JobInfo.Builder#setExtras(android.os.PersistableBundle). + * This object serves to identify this specific running job instance when calling + * {@link #jobFinished(JobParameters, boolean)}. + * @return {@code true} if your service will continue running, using a separate thread + * when appropriate. {@code false} means that this job has completed its work. + */ + public abstract boolean onStartJob(JobParameters params); + + /** + * This method is called if the system has determined that you must stop execution of your job + * even before you've had a chance to call {@link #jobFinished(JobParameters, boolean)}. + * + * <p>This will happen if the requirements specified at schedule time are no longer met. For + * example you may have requested WiFi with + * {@link android.app.job.JobInfo.Builder#setRequiredNetworkType(int)}, yet while your + * job was executing the user toggled WiFi. Another example is if you had specified + * {@link android.app.job.JobInfo.Builder#setRequiresDeviceIdle(boolean)}, and the phone left its + * idle maintenance window. You are solely responsible for the behavior of your application + * upon receipt of this message; your app will likely start to misbehave if you ignore it. + * <p> + * Once this method returns, the system releases the wakelock that it is holding on + * behalf of the job.</p> + * + * @param params The parameters identifying this job, as supplied to + * the job in the {@link #onStartJob(JobParameters)} callback. + * @return {@code true} to indicate to the JobManager whether you'd like to reschedule + * this job based on the retry criteria provided at job creation-time; or {@code false} + * to end the job entirely. Regardless of the value returned, your job must stop executing. + */ + public abstract boolean onStopJob(JobParameters params); +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java new file mode 100644 index 000000000000..ab94da843635 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.app.job; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +import java.lang.ref.WeakReference; + +/** + * Helper for implementing a {@link android.app.Service} that interacts with + * {@link JobScheduler}. This is not intended for use by regular applications, but + * allows frameworks built on top of the platform to create their own + * {@link android.app.Service} that interact with {@link JobScheduler} as well as + * add in additional functionality. If you just want to execute jobs normally, you + * should instead be looking at {@link JobService}. + */ +public abstract class JobServiceEngine { + private static final String TAG = "JobServiceEngine"; + + /** + * Identifier for a message that will result in a call to + * {@link #onStartJob(android.app.job.JobParameters)}. + */ + private static final int MSG_EXECUTE_JOB = 0; + /** + * Message that will result in a call to {@link #onStopJob(android.app.job.JobParameters)}. + */ + private static final int MSG_STOP_JOB = 1; + /** + * Message that the client has completed execution of this job. + */ + private static final int MSG_JOB_FINISHED = 2; + + private final IJobService mBinder; + + /** + * Handler we post jobs to. Responsible for calling into the client logic, and handling the + * callback to the system. + */ + JobHandler mHandler; + + static final class JobInterface extends IJobService.Stub { + final WeakReference<JobServiceEngine> mService; + + JobInterface(JobServiceEngine service) { + mService = new WeakReference<>(service); + } + + @Override + public void startJob(JobParameters jobParams) throws RemoteException { + JobServiceEngine service = mService.get(); + if (service != null) { + Message m = Message.obtain(service.mHandler, MSG_EXECUTE_JOB, jobParams); + m.sendToTarget(); + } + } + + @Override + public void stopJob(JobParameters jobParams) throws RemoteException { + JobServiceEngine service = mService.get(); + if (service != null) { + Message m = Message.obtain(service.mHandler, MSG_STOP_JOB, jobParams); + m.sendToTarget(); + } + } + } + + /** + * Runs on application's main thread - callbacks are meant to offboard work to some other + * (app-specified) mechanism. + * @hide + */ + class JobHandler extends Handler { + JobHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + final JobParameters params = (JobParameters) msg.obj; + switch (msg.what) { + case MSG_EXECUTE_JOB: + try { + boolean workOngoing = JobServiceEngine.this.onStartJob(params); + ackStartMessage(params, workOngoing); + } catch (Exception e) { + Log.e(TAG, "Error while executing job: " + params.getJobId()); + throw new RuntimeException(e); + } + break; + case MSG_STOP_JOB: + try { + boolean ret = JobServiceEngine.this.onStopJob(params); + ackStopMessage(params, ret); + } catch (Exception e) { + Log.e(TAG, "Application unable to handle onStopJob.", e); + throw new RuntimeException(e); + } + break; + case MSG_JOB_FINISHED: + final boolean needsReschedule = (msg.arg2 == 1); + IJobCallback callback = params.getCallback(); + if (callback != null) { + try { + callback.jobFinished(params.getJobId(), needsReschedule); + } catch (RemoteException e) { + Log.e(TAG, "Error reporting job finish to system: binder has gone" + + "away."); + } + } else { + Log.e(TAG, "finishJob() called for a nonexistent job id."); + } + break; + default: + Log.e(TAG, "Unrecognised message received."); + break; + } + } + + private void ackStartMessage(JobParameters params, boolean workOngoing) { + final IJobCallback callback = params.getCallback(); + final int jobId = params.getJobId(); + if (callback != null) { + try { + callback.acknowledgeStartMessage(jobId, workOngoing); + } catch(RemoteException e) { + Log.e(TAG, "System unreachable for starting job."); + } + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Attempting to ack a job that has already been processed."); + } + } + } + + private void ackStopMessage(JobParameters params, boolean reschedule) { + final IJobCallback callback = params.getCallback(); + final int jobId = params.getJobId(); + if (callback != null) { + try { + callback.acknowledgeStopMessage(jobId, reschedule); + } catch(RemoteException e) { + Log.e(TAG, "System unreachable for stopping job."); + } + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Attempting to ack a job that has already been processed."); + } + } + } + } + + /** + * Create a new engine, ready for use. + * + * @param service The {@link Service} that is creating this engine and in which it will run. + */ + public JobServiceEngine(Service service) { + mBinder = new JobInterface(this); + mHandler = new JobHandler(service.getMainLooper()); + } + + /** + * Retrieve the engine's IPC interface that should be returned by + * {@link Service#onBind(Intent)}. + */ + public final IBinder getBinder() { + return mBinder.asBinder(); + } + + /** + * Engine's report that a job has started. See + * {@link JobService#onStartJob(JobParameters) JobService.onStartJob} for more information. + */ + public abstract boolean onStartJob(JobParameters params); + + /** + * Engine's report that a job has stopped. See + * {@link JobService#onStopJob(JobParameters) JobService.onStopJob} for more information. + */ + public abstract boolean onStopJob(JobParameters params); + + /** + * Call in to engine to report that a job has finished executing. See + * {@link JobService#jobFinished(JobParameters, boolean)} JobService.jobFinished} for more + * information. + */ + public void jobFinished(JobParameters params, boolean needsReschedule) { + if (params == null) { + throw new NullPointerException("params"); + } + Message m = Message.obtain(mHandler, MSG_JOB_FINISHED, params); + m.arg2 = needsReschedule ? 1 : 0; + m.sendToTarget(); + } +}
\ No newline at end of file diff --git a/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.aidl b/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.aidl new file mode 100644 index 000000000000..d40f4e39ea2e --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.job; + +parcelable JobSnapshot; diff --git a/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.java b/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.java new file mode 100644 index 000000000000..2c58908a6064 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.job; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Current-state snapshot of a scheduled job. These snapshots are not used in apps; + * they exist only within the system process across the local call surface where JobStatus + * is not directly accessible at build time. + * + * Constraints that the underlying job does not require are always reported as + * being currently satisfied. + * @hide + */ +public class JobSnapshot implements Parcelable { + private final JobInfo mJob; + private final int mSatisfiedConstraints; + private final boolean mIsRunnable; + + public JobSnapshot(JobInfo info, int satisfiedMask, boolean runnable) { + mJob = info; + mSatisfiedConstraints = satisfiedMask; + mIsRunnable = runnable; + } + + public JobSnapshot(Parcel in) { + mJob = JobInfo.CREATOR.createFromParcel(in); + mSatisfiedConstraints = in.readInt(); + mIsRunnable = in.readBoolean(); + } + + private boolean satisfied(int flag) { + return (mSatisfiedConstraints & flag) != 0; + } + + /** + * Returning JobInfo bound to this snapshot + * @return JobInfo of this snapshot + */ + public JobInfo getJobInfo() { + return mJob; + } + + /** + * Is this job actually runnable at this moment? + */ + public boolean isRunnable() { + return mIsRunnable; + } + + /** + * @see JobInfo.Builder#setRequiresCharging(boolean) + */ + public boolean isChargingSatisfied() { + return !mJob.isRequireCharging() + || satisfied(JobInfo.CONSTRAINT_FLAG_CHARGING); + } + + /** + * @see JobInfo.Builder#setRequiresBatteryNotLow(boolean) + */ + public boolean isBatteryNotLowSatisfied() { + return !mJob.isRequireBatteryNotLow() + || satisfied(JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW); + } + + /** + * @see JobInfo.Builder#setRequiresDeviceIdle(boolean) + */ + public boolean isRequireDeviceIdleSatisfied() { + return !mJob.isRequireDeviceIdle() + || satisfied(JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE); + } + + public boolean isRequireStorageNotLowSatisfied() { + return !mJob.isRequireStorageNotLow() + || satisfied(JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + mJob.writeToParcel(out, flags); + out.writeInt(mSatisfiedConstraints); + out.writeBoolean(mIsRunnable); + } + + public static final @android.annotation.NonNull Creator<JobSnapshot> CREATOR = new Creator<JobSnapshot>() { + @Override + public JobSnapshot createFromParcel(Parcel in) { + return new JobSnapshot(in); + } + + @Override + public JobSnapshot[] newArray(int size) { + return new JobSnapshot[size]; + } + }; +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.aidl b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.aidl new file mode 100644 index 000000000000..e8fe47d07865 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.job; + +/** @hide */ +parcelable JobWorkItem; diff --git a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java new file mode 100644 index 000000000000..0c45cbf6dc11 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.job; + +import static android.app.job.JobInfo.NETWORK_BYTES_UNKNOWN; + +import android.annotation.BytesLong; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.Intent; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A unit of work that can be enqueued for a job using + * {@link JobScheduler#enqueue JobScheduler.enqueue}. See + * {@link JobParameters#dequeueWork() JobParameters.dequeueWork} for more details. + */ +final public class JobWorkItem implements Parcelable { + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + final Intent mIntent; + final long mNetworkDownloadBytes; + final long mNetworkUploadBytes; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + int mDeliveryCount; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + int mWorkId; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + Object mGrants; + + /** + * Create a new piece of work, which can be submitted to + * {@link JobScheduler#enqueue JobScheduler.enqueue}. + * + * @param intent The general Intent describing this work. + */ + public JobWorkItem(Intent intent) { + mIntent = intent; + mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN; + mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN; + } + + /** + * Create a new piece of work, which can be submitted to + * {@link JobScheduler#enqueue JobScheduler.enqueue}. + * <p> + * See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for + * details about how to estimate network traffic. + * + * @param intent The general Intent describing this work. + * @param downloadBytes The estimated size of network traffic that will be + * downloaded by this job work item, in bytes. + * @param uploadBytes The estimated size of network traffic that will be + * uploaded by this job work item, in bytes. + */ + public JobWorkItem(Intent intent, @BytesLong long downloadBytes, @BytesLong long uploadBytes) { + mIntent = intent; + mNetworkDownloadBytes = downloadBytes; + mNetworkUploadBytes = uploadBytes; + } + + /** + * Return the Intent associated with this work. + */ + public Intent getIntent() { + return mIntent; + } + + /** + * Return the estimated size of download traffic that will be performed by + * this job, in bytes. + * + * @return Estimated size of download traffic, or + * {@link JobInfo#NETWORK_BYTES_UNKNOWN} when unknown. + */ + public @BytesLong long getEstimatedNetworkDownloadBytes() { + return mNetworkDownloadBytes; + } + + /** + * Return the estimated size of upload traffic that will be performed by + * this job work item, in bytes. + * + * @return Estimated size of upload traffic, or + * {@link JobInfo#NETWORK_BYTES_UNKNOWN} when unknown. + */ + public @BytesLong long getEstimatedNetworkUploadBytes() { + return mNetworkUploadBytes; + } + + /** + * Return the count of the number of times this work item has been delivered + * to the job. The value will be > 1 if it has been redelivered because the job + * was stopped or crashed while it had previously been delivered but before the + * job had called {@link JobParameters#completeWork JobParameters.completeWork} for it. + */ + public int getDeliveryCount() { + return mDeliveryCount; + } + + /** + * @hide + */ + public void bumpDeliveryCount() { + mDeliveryCount++; + } + + /** + * @hide + */ + public void setWorkId(int id) { + mWorkId = id; + } + + /** + * @hide + */ + public int getWorkId() { + return mWorkId; + } + + /** + * @hide + */ + public void setGrants(Object grants) { + mGrants = grants; + } + + /** + * @hide + */ + public Object getGrants() { + return mGrants; + } + + public String toString() { + StringBuilder sb = new StringBuilder(64); + sb.append("JobWorkItem{id="); + sb.append(mWorkId); + sb.append(" intent="); + sb.append(mIntent); + if (mNetworkDownloadBytes != NETWORK_BYTES_UNKNOWN) { + sb.append(" downloadBytes="); + sb.append(mNetworkDownloadBytes); + } + if (mNetworkUploadBytes != NETWORK_BYTES_UNKNOWN) { + sb.append(" uploadBytes="); + sb.append(mNetworkUploadBytes); + } + if (mDeliveryCount != 0) { + sb.append(" dcount="); + sb.append(mDeliveryCount); + } + sb.append("}"); + return sb.toString(); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + if (mIntent != null) { + out.writeInt(1); + mIntent.writeToParcel(out, 0); + } else { + out.writeInt(0); + } + out.writeLong(mNetworkDownloadBytes); + out.writeLong(mNetworkUploadBytes); + out.writeInt(mDeliveryCount); + out.writeInt(mWorkId); + } + + public static final @android.annotation.NonNull Parcelable.Creator<JobWorkItem> CREATOR + = new Parcelable.Creator<JobWorkItem>() { + public JobWorkItem createFromParcel(Parcel in) { + return new JobWorkItem(in); + } + + public JobWorkItem[] newArray(int size) { + return new JobWorkItem[size]; + } + }; + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + JobWorkItem(Parcel in) { + if (in.readInt() != 0) { + mIntent = Intent.CREATOR.createFromParcel(in); + } else { + mIntent = null; + } + mNetworkDownloadBytes = in.readLong(); + mNetworkUploadBytes = in.readLong(); + mDeliveryCount = in.readInt(); + mWorkId = in.readInt(); + } +} diff --git a/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java new file mode 100644 index 000000000000..f863718d6ce7 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.annotation.NonNull; +import android.annotation.SystemService; +import android.annotation.TestApi; +import android.content.Context; + +/** + * Access to the service that keeps track of device idleness and drives low power mode based on + * that. + * + * @hide + */ +@TestApi +@SystemService(Context.DEVICE_IDLE_CONTROLLER) +public class DeviceIdleManager { + private final Context mContext; + private final IDeviceIdleController mService; + + /** + * @hide + */ + public DeviceIdleManager(@NonNull Context context, @NonNull IDeviceIdleController service) { + mContext = context; + mService = service; + } + + IDeviceIdleController getService() { + return mService; + } + + /** + * @return package names the system has white-listed to opt out of power save restrictions, + * except for device idle mode. + */ + public @NonNull String[] getSystemPowerWhitelistExceptIdle() { + try { + return mService.getSystemPowerWhitelistExceptIdle(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @return package names the system has white-listed to opt out of power save restrictions for + * all modes. + */ + public @NonNull String[] getSystemPowerWhitelist() { + try { + return mService.getSystemPowerWhitelist(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl b/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl new file mode 100644 index 000000000000..643d47ca5c6a --- /dev/null +++ b/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.os.UserHandle; + +/** @hide */ +interface IDeviceIdleController { + void addPowerSaveWhitelistApp(String name); + int addPowerSaveWhitelistApps(in List<String> packageNames); + void removePowerSaveWhitelistApp(String name); + /* Removes an app from the system whitelist. Calling restoreSystemPowerWhitelistApp will add + the app back into the system whitelist */ + void removeSystemPowerWhitelistApp(String name); + void restoreSystemPowerWhitelistApp(String name); + String[] getRemovedSystemPowerWhitelistApps(); + String[] getSystemPowerWhitelistExceptIdle(); + String[] getSystemPowerWhitelist(); + String[] getUserPowerWhitelist(); + @UnsupportedAppUsage + String[] getFullPowerWhitelistExceptIdle(); + String[] getFullPowerWhitelist(); + int[] getAppIdWhitelistExceptIdle(); + int[] getAppIdWhitelist(); + int[] getAppIdUserWhitelist(); + @UnsupportedAppUsage + int[] getAppIdTempWhitelist(); + boolean isPowerSaveWhitelistExceptIdleApp(String name); + boolean isPowerSaveWhitelistApp(String name); + @UnsupportedAppUsage + void addPowerSaveTempWhitelistApp(String name, long duration, int userId, String reason); + long addPowerSaveTempWhitelistAppForMms(String name, int userId, String reason); + long addPowerSaveTempWhitelistAppForSms(String name, int userId, String reason); + long whitelistAppTemporarily(String name, int userId, String reason); + void exitIdle(String reason); + int setPreIdleTimeoutMode(int Mode); + void resetPreIdleTimeoutMode(); +} diff --git a/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java b/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java new file mode 100644 index 000000000000..d54d857ffcd6 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.annotation.TestApi; +import android.content.Context; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.List; + +/** + * Interface to access and modify the power save whitelist. + * + * @hide + */ +@SystemApi +@TestApi +@SystemService(Context.POWER_WHITELIST_MANAGER) +public class PowerWhitelistManager { + private final Context mContext; + // Proxy to DeviceIdleController for now + // TODO: migrate to PowerWhitelistController + private final IDeviceIdleController mService; + + /** + * Indicates that an unforeseen event has occurred and the app should be whitelisted to handle + * it. + */ + public static final int EVENT_UNSPECIFIED = 0; + + /** + * Indicates that an SMS event has occurred and the app should be whitelisted to handle it. + */ + public static final int EVENT_SMS = 1; + + /** + * Indicates that an MMS event has occurred and the app should be whitelisted to handle it. + */ + public static final int EVENT_MMS = 2; + + /** + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"EVENT_"}, value = { + EVENT_UNSPECIFIED, + EVENT_SMS, + EVENT_MMS, + }) + public @interface WhitelistEvent { + } + + /** + * @hide + */ + public PowerWhitelistManager(@NonNull Context context) { + mContext = context; + mService = context.getSystemService(DeviceIdleManager.class).getService(); + } + + /** + * Add the specified package to the permanent power save whitelist. + */ + @RequiresPermission(android.Manifest.permission.DEVICE_POWER) + public void addToWhitelist(@NonNull String packageName) { + addToWhitelist(Collections.singletonList(packageName)); + } + + /** + * Add the specified packages to the permanent power save whitelist. + */ + @RequiresPermission(android.Manifest.permission.DEVICE_POWER) + public void addToWhitelist(@NonNull List<String> packageNames) { + try { + mService.addPowerSaveWhitelistApps(packageNames); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get a list of app IDs of app that are whitelisted. This does not include temporarily + * whitelisted apps. + * + * @param includingIdle Set to true if the app should be whitelisted from device idle as well + * as other power save restrictions + * @hide + */ + @NonNull + public int[] getWhitelistedAppIds(boolean includingIdle) { + try { + if (includingIdle) { + return mService.getAppIdWhitelist(); + } else { + return mService.getAppIdWhitelistExceptIdle(); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns true if the app is whitelisted from power save restrictions. This does not include + * temporarily whitelisted apps. + * + * @param includingIdle Set to true if the app should be whitelisted from device + * idle as well as other power save restrictions + * @hide + */ + public boolean isWhitelisted(@NonNull String packageName, boolean includingIdle) { + try { + if (includingIdle) { + return mService.isPowerSaveWhitelistApp(packageName); + } else { + return mService.isPowerSaveWhitelistExceptIdleApp(packageName); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Add an app to the temporary whitelist for a short amount of time. + * + * @param packageName The package to add to the temp whitelist + * @param durationMs How long to keep the app on the temp whitelist for (in milliseconds) + */ + @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST) + public void whitelistAppTemporarily(@NonNull String packageName, long durationMs) { + String reason = "from:" + UserHandle.formatUid(Binder.getCallingUid()); + try { + mService.addPowerSaveTempWhitelistApp(packageName, durationMs, mContext.getUserId(), + reason); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Add an app to the temporary whitelist for a short amount of time for a specific reason. + * + * @param packageName The package to add to the temp whitelist + * @param event The reason to add the app to the temp whitelist + * @param reason A human-readable reason explaining why the app is temp whitelisted. Only used + * for logging purposes + * @return The duration (in milliseconds) that the app is whitelisted for + */ + @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST) + public long whitelistAppTemporarilyForEvent(@NonNull String packageName, + @WhitelistEvent int event, @NonNull String reason) { + try { + switch (event) { + case EVENT_MMS: + return mService.addPowerSaveTempWhitelistAppForMms( + packageName, mContext.getUserId(), reason); + case EVENT_SMS: + return mService.addPowerSaveTempWhitelistAppForSms( + packageName, mContext.getUserId(), reason); + case EVENT_UNSPECIFIED: + default: + return mService.whitelistAppTemporarily( + packageName, mContext.getUserId(), reason); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java new file mode 100644 index 000000000000..6475f5706a6d --- /dev/null +++ b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import com.android.server.deviceidle.IDeviceIdleConstraint; + +public interface DeviceIdleInternal { + void onConstraintStateChanged(IDeviceIdleConstraint constraint, boolean active); + + void registerDeviceIdleConstraint(IDeviceIdleConstraint constraint, String name, + @IDeviceIdleConstraint.MinimumState int minState); + + void unregisterDeviceIdleConstraint(IDeviceIdleConstraint constraint); + + void exitIdle(String reason); + + // duration in milliseconds + void addPowerSaveTempWhitelistApp(int callingUid, String packageName, + long duration, int userId, boolean sync, String reason); + + // duration in milliseconds + void addPowerSaveTempWhitelistAppDirect(int uid, long duration, boolean sync, + String reason); + + // duration in milliseconds + long getNotificationWhitelistDuration(); + + void setJobsActive(boolean active); + + // Up-call from alarm manager. + void setAlarmsActive(boolean active); + + boolean isAppOnWhitelist(int appid); + + int[] getPowerSaveWhitelistUserAppIds(); + + int[] getPowerSaveTempWhitelistAppIds(); + + /** + * Listener to be notified when DeviceIdleController determines that the device has moved or is + * stationary. + */ + interface StationaryListener { + void onDeviceStationaryChanged(boolean isStationary); + } + + /** + * Registers a listener that will be notified when the system has detected that the device is + * stationary or in motion. + */ + void registerStationaryListener(StationaryListener listener); + + /** + * Unregisters a registered stationary listener from being notified when the system has detected + * that the device is stationary or in motion. + */ + void unregisterStationaryListener(StationaryListener listener); +} diff --git a/apex/jobscheduler/framework/java/com/android/server/deviceidle/ConstraintController.java b/apex/jobscheduler/framework/java/com/android/server/deviceidle/ConstraintController.java new file mode 100644 index 000000000000..6d52f7188d99 --- /dev/null +++ b/apex/jobscheduler/framework/java/com/android/server/deviceidle/ConstraintController.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.deviceidle; + +/** + * Device idle constraints for a specific form factor or use-case. + */ +public interface ConstraintController { + /** + * Begin any general continuing work and register all constraints. + */ + void start(); + + /** + * Unregister all constraints and stop any general work. + */ + void stop(); +} diff --git a/apex/jobscheduler/framework/java/com/android/server/deviceidle/IDeviceIdleConstraint.java b/apex/jobscheduler/framework/java/com/android/server/deviceidle/IDeviceIdleConstraint.java new file mode 100644 index 000000000000..f1f957307716 --- /dev/null +++ b/apex/jobscheduler/framework/java/com/android/server/deviceidle/IDeviceIdleConstraint.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.deviceidle; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Implemented by OEM and/or Form Factor. System ones are built into the + * image regardless of build flavour but may still be switched off at run time. + * Individual feature flags at build time control which are used. We may + * also explore a local override for quick testing. + */ +public interface IDeviceIdleConstraint { + + /** + * A state for this constraint to block descent from. + * + * <p>These states are a subset of the states in DeviceIdleController that make sense for + * constraints to be able to block on. For example, {@link #SENSING_OR_ABOVE} clearly has + * defined "above" and "below" states. However, a hypothetical {@code QUICK_DOZE_OR_ABOVE} + * state would not have clear semantics as to what transitions should be blocked and which + * should be allowed. + */ + @IntDef(flag = false, value = { + ACTIVE, + SENSING_OR_ABOVE, + }) + @Retention(RetentionPolicy.SOURCE) + @interface MinimumState {} + + int ACTIVE = 0; + int SENSING_OR_ABOVE = 1; + + /** + * Begin tracking events for this constraint. + * + * <p>The device idle controller has reached a point where it is waiting for the all-clear + * from this tracker (possibly among others) in order to continue with progression into + * idle state. It will not proceed until one of the following happens: + * <ul> + * <li>The constraint reports inactive with {@code .setActive(false)}.</li> + * <li>The constraint is unregistered with {@code .unregisterDeviceIdleConstraint(this)}.</li> + * <li>A transition timeout in DeviceIdleController fires. + * </ul> + */ + void startMonitoring(); + + /** Stop checking for new events and do not call into LocalService with updates any more. */ + void stopMonitoring(); +} diff --git a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java new file mode 100644 index 000000000000..7833a037463c --- /dev/null +++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job; + +import android.annotation.NonNull; +import android.app.job.JobInfo; +import android.util.proto.ProtoOutputStream; + +import java.util.List; + +/** + * JobScheduler local system service interface. + * {@hide} Only for use within the system server. + */ +public interface JobSchedulerInternal { + + /** + * Returns a list of pending jobs scheduled by the system service. + */ + List<JobInfo> getSystemScheduledPendingJobs(); + + /** + * Cancel the jobs for a given uid (e.g. when app data is cleared) + */ + void cancelJobsForUid(int uid, String reason); + + /** + * These are for activity manager to communicate to use what is currently performing backups. + */ + void addBackingUpUid(int uid); + void removeBackingUpUid(int uid); + void clearAllBackingUpUids(); + + /** Returns the package responsible for backing up media on the device. */ + @NonNull + String getMediaBackupPackage(); + + /** + * The user has started interacting with the app. Take any appropriate action. + */ + void reportAppUsage(String packageName, int userId); + + /** + * Report a snapshot of sync-related jobs back to the sync manager + */ + JobStorePersistStats getPersistStats(); + + /** + * Stats about the first load after boot and the most recent save. + */ + public class JobStorePersistStats { + public int countAllJobsLoaded = -1; + public int countSystemServerJobsLoaded = -1; + public int countSystemSyncManagerJobsLoaded = -1; + + public int countAllJobsSaved = -1; + public int countSystemServerJobsSaved = -1; + public int countSystemSyncManagerJobsSaved = -1; + + public JobStorePersistStats() { + } + + public JobStorePersistStats(JobStorePersistStats source) { + countAllJobsLoaded = source.countAllJobsLoaded; + countSystemServerJobsLoaded = source.countSystemServerJobsLoaded; + countSystemSyncManagerJobsLoaded = source.countSystemSyncManagerJobsLoaded; + + countAllJobsSaved = source.countAllJobsSaved; + countSystemServerJobsSaved = source.countSystemServerJobsSaved; + countSystemSyncManagerJobsSaved = source.countSystemSyncManagerJobsSaved; + } + + @Override + public String toString() { + return "FirstLoad: " + + countAllJobsLoaded + "/" + + countSystemServerJobsLoaded + "/" + + countSystemSyncManagerJobsLoaded + + " LastSave: " + + countAllJobsSaved + "/" + + countSystemServerJobsSaved + "/" + + countSystemSyncManagerJobsSaved; + } + + /** + * Write the persist stats to the specified field. + */ + public void dumpDebug(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + final long flToken = proto.start(JobStorePersistStatsProto.FIRST_LOAD); + proto.write(JobStorePersistStatsProto.Stats.NUM_TOTAL_JOBS, countAllJobsLoaded); + proto.write(JobStorePersistStatsProto.Stats.NUM_SYSTEM_SERVER_JOBS, + countSystemServerJobsLoaded); + proto.write(JobStorePersistStatsProto.Stats.NUM_SYSTEM_SYNC_MANAGER_JOBS, + countSystemSyncManagerJobsLoaded); + proto.end(flToken); + + final long lsToken = proto.start(JobStorePersistStatsProto.LAST_SAVE); + proto.write(JobStorePersistStatsProto.Stats.NUM_TOTAL_JOBS, countAllJobsSaved); + proto.write(JobStorePersistStatsProto.Stats.NUM_SYSTEM_SERVER_JOBS, + countSystemServerJobsSaved); + proto.write(JobStorePersistStatsProto.Stats.NUM_SYSTEM_SYNC_MANAGER_JOBS, + countSystemSyncManagerJobsSaved); + proto.end(lsToken); + + proto.end(token); + } + } +} diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java new file mode 100644 index 000000000000..e15f0f37fc62 --- /dev/null +++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java @@ -0,0 +1,168 @@ +package com.android.server.usage; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.usage.AppStandbyInfo; +import android.app.usage.UsageEvents; +import android.app.usage.UsageStatsManager.StandbyBuckets; +import android.app.usage.UsageStatsManager.SystemForcedReasons; +import android.content.Context; +import android.os.Looper; + +import com.android.internal.util.IndentingPrintWriter; + +import java.io.PrintWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Set; + +public interface AppStandbyInternal { + /** + * TODO AppStandbyController should probably be a binder service, and then we shouldn't need + * this method. + */ + static AppStandbyInternal newAppStandbyController(ClassLoader loader, Context context, + Looper looper) { + try { + final Class<?> clazz = Class.forName("com.android.server.usage.AppStandbyController", + true, loader); + final Constructor<?> ctor = clazz.getConstructor(Context.class, Looper.class); + return (AppStandbyInternal) ctor.newInstance(context, looper); + } catch (NoSuchMethodException | InstantiationException + | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) { + throw new RuntimeException("Unable to instantiate AppStandbyController!", e); + } + } + + /** + * Listener interface for notifications that an app's idle state changed. + */ + abstract static class AppIdleStateChangeListener { + + /** Callback to inform listeners that the idle state has changed to a new bucket. */ + public abstract void onAppIdleStateChanged(String packageName, @UserIdInt int userId, + boolean idle, int bucket, int reason); + + /** + * Callback to inform listeners that the parole state has changed. This means apps are + * allowed to do work even if they're idle or in a low bucket. + */ + public void onParoleStateChanged(boolean isParoleOn) { + // No-op by default + } + + /** + * Optional callback to inform the listener that the app has transitioned into + * an active state due to user interaction. + */ + public void onUserInteractionStarted(String packageName, @UserIdInt int userId) { + // No-op by default + } + } + + void onBootPhase(int phase); + + void postCheckIdleStates(int userId); + + /** + * We send a different message to check idle states once, otherwise we would end up + * scheduling a series of repeating checkIdleStates each time we fired off one. + */ + void postOneTimeCheckIdleStates(); + + void reportEvent(UsageEvents.Event event, int userId); + + void setLastJobRunTime(String packageName, int userId, long elapsedRealtime); + + long getTimeSinceLastJobRun(String packageName, int userId); + + void onUserRemoved(int userId); + + void addListener(AppIdleStateChangeListener listener); + + void removeListener(AppIdleStateChangeListener listener); + + int getAppId(String packageName); + + /** + * @see #isAppIdleFiltered(String, int, int, long) + */ + boolean isAppIdleFiltered(String packageName, int userId, long elapsedRealtime, + boolean shouldObfuscateInstantApps); + + /** + * Checks if an app has been idle for a while and filters out apps that are excluded. + * It returns false if the current system state allows all apps to be considered active. + * This happens if the device is plugged in or otherwise temporarily allowed to make exceptions. + * Called by interface impls. + */ + boolean isAppIdleFiltered(String packageName, int appId, int userId, + long elapsedRealtime); + + /** + * @return true if currently app idle parole mode is on. + */ + boolean isInParole(); + + int[] getIdleUidsForUser(int userId); + + void setAppIdleAsync(String packageName, boolean idle, int userId); + + @StandbyBuckets + int getAppStandbyBucket(String packageName, int userId, + long elapsedRealtime, boolean shouldObfuscateInstantApps); + + List<AppStandbyInfo> getAppStandbyBuckets(int userId); + + /** + * Changes an app's standby bucket to the provided value. The caller can only set the standby + * bucket for a different app than itself. + * If attempting to automatically place an app in the RESTRICTED bucket, use + * {@link #restrictApp(String, int, int)} instead. + */ + void setAppStandbyBucket(@NonNull String packageName, int bucket, int userId, int callingUid, + int callingPid); + + /** + * Changes the app standby bucket for multiple apps at once. + */ + void setAppStandbyBuckets(@NonNull List<AppStandbyInfo> appBuckets, int userId, int callingUid, + int callingPid); + + /** + * Put the specified app in the + * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} + * bucket. If it has been used by the user recently, the restriction will delayed until an + * appropriate time. + * + * @param restrictReason The restrictReason for restricting the app. Should be one of the + * UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_* reasons. + */ + void restrictApp(@NonNull String packageName, int userId, + @SystemForcedReasons int restrictReason); + + void addActiveDeviceAdmin(String adminPkg, int userId); + + void setActiveAdminApps(Set<String> adminPkgs, int userId); + + void onAdminDataAvailable(); + + void clearCarrierPrivilegedApps(); + + void flushToDisk(); + + void initializeDefaultsForSystemApps(int userId); + + void postReportContentProviderUsage(String name, String packageName, int userId); + + void postReportSyncScheduled(String packageName, int userId, boolean exempted); + + void postReportExemptedSyncStart(String packageName, int userId); + + void dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs); + + void dumpState(String[] args, PrintWriter pw); + + boolean isAppIdleEnabled(); +} diff --git a/apex/jobscheduler/service/Android.bp b/apex/jobscheduler/service/Android.bp new file mode 100644 index 000000000000..69a9fd844729 --- /dev/null +++ b/apex/jobscheduler/service/Android.bp @@ -0,0 +1,16 @@ +// Job Scheduler Service jar, which will eventually be put in the jobscheduler mainline apex. +// service-jobscheduler needs to be added to PRODUCT_SYSTEM_SERVER_JARS. +java_library { + name: "service-jobscheduler", + installable: true, + + srcs: [ + "java/**/*.java", + ], + + libs: [ + "app-compat-annotations", + "framework", + "services.core", + ], +} diff --git a/apex/jobscheduler/service/java/com/android/server/AnyMotionDetector.java b/apex/jobscheduler/service/java/com/android/server/AnyMotionDetector.java new file mode 100644 index 000000000000..316306df4f48 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/AnyMotionDetector.java @@ -0,0 +1,520 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.SystemClock; +import android.util.Slog; + +/** + * Determines if the device has been set upon a stationary object. + */ +public class AnyMotionDetector { + interface DeviceIdleCallback { + public void onAnyMotionResult(int result); + } + + private static final String TAG = "AnyMotionDetector"; + + private static final boolean DEBUG = false; + + /** Stationary status is unknown due to insufficient orientation measurements. */ + public static final int RESULT_UNKNOWN = -1; + + /** Device is stationary, e.g. still on a table. */ + public static final int RESULT_STATIONARY = 0; + + /** Device has been moved. */ + public static final int RESULT_MOVED = 1; + + /** Orientation measurements are being performed or are planned. */ + private static final int STATE_INACTIVE = 0; + + /** No orientation measurements are being performed or are planned. */ + private static final int STATE_ACTIVE = 1; + + /** Current measurement state. */ + private int mState; + + /** Threshold energy above which the device is considered moving. */ + private final float THRESHOLD_ENERGY = 5f; + + /** The duration of the accelerometer orientation measurement. */ + private static final long ORIENTATION_MEASUREMENT_DURATION_MILLIS = 2500; + + /** The maximum duration we will collect accelerometer data. */ + private static final long ACCELEROMETER_DATA_TIMEOUT_MILLIS = 3000; + + /** The interval between accelerometer orientation measurements. */ + private static final long ORIENTATION_MEASUREMENT_INTERVAL_MILLIS = 5000; + + /** The maximum duration we will hold a wakelock to determine stationary status. */ + private static final long WAKELOCK_TIMEOUT_MILLIS = 30000; + + /** + * The duration in milliseconds after which an orientation measurement is considered + * too stale to be used. + */ + private static final int STALE_MEASUREMENT_TIMEOUT_MILLIS = 2 * 60 * 1000; + + /** The accelerometer sampling interval. */ + private static final int SAMPLING_INTERVAL_MILLIS = 40; + + private final Handler mHandler; + private final Object mLock = new Object(); + private Sensor mAccelSensor; + private SensorManager mSensorManager; + private PowerManager.WakeLock mWakeLock; + + /** Threshold angle in degrees beyond which the device is considered moving. */ + private final float mThresholdAngle; + + /** The minimum number of samples required to detect AnyMotion. */ + private int mNumSufficientSamples; + + /** True if an orientation measurement is in progress. */ + private boolean mMeasurementInProgress; + + /** True if sendMessageDelayed() for the mMeasurementTimeout callback has been scheduled */ + private boolean mMeasurementTimeoutIsActive; + + /** True if sendMessageDelayed() for the mWakelockTimeout callback has been scheduled */ + private boolean mWakelockTimeoutIsActive; + + /** True if sendMessageDelayed() for the mSensorRestart callback has been scheduled */ + private boolean mSensorRestartIsActive; + + /** The most recent gravity vector. */ + private Vector3 mCurrentGravityVector = null; + + /** The second most recent gravity vector. */ + private Vector3 mPreviousGravityVector = null; + + /** Running sum of squared errors. */ + private RunningSignalStats mRunningStats; + + private DeviceIdleCallback mCallback = null; + + public AnyMotionDetector(PowerManager pm, Handler handler, SensorManager sm, + DeviceIdleCallback callback, float thresholdAngle) { + if (DEBUG) Slog.d(TAG, "AnyMotionDetector instantiated."); + synchronized (mLock) { + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.setReferenceCounted(false); + mHandler = handler; + mSensorManager = sm; + mAccelSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mMeasurementInProgress = false; + mMeasurementTimeoutIsActive = false; + mWakelockTimeoutIsActive = false; + mSensorRestartIsActive = false; + mState = STATE_INACTIVE; + mCallback = callback; + mThresholdAngle = thresholdAngle; + mRunningStats = new RunningSignalStats(); + mNumSufficientSamples = (int) Math.ceil( + ((double)ORIENTATION_MEASUREMENT_DURATION_MILLIS / SAMPLING_INTERVAL_MILLIS)); + if (DEBUG) Slog.d(TAG, "mNumSufficientSamples = " + mNumSufficientSamples); + } + } + + /** + * If we do not have an accelerometer, we are not going to collect much data. + */ + public boolean hasSensor() { + return mAccelSensor != null; + } + + /* + * Acquire accel data until we determine AnyMotion status. + */ + public void checkForAnyMotion() { + if (DEBUG) { + Slog.d(TAG, "checkForAnyMotion(). mState = " + mState); + } + if (mState != STATE_ACTIVE) { + synchronized (mLock) { + mState = STATE_ACTIVE; + if (DEBUG) { + Slog.d(TAG, "Moved from STATE_INACTIVE to STATE_ACTIVE."); + } + mCurrentGravityVector = null; + mPreviousGravityVector = null; + mWakeLock.acquire(); + Message wakelockTimeoutMsg = Message.obtain(mHandler, mWakelockTimeout); + mHandler.sendMessageDelayed(wakelockTimeoutMsg, WAKELOCK_TIMEOUT_MILLIS); + mWakelockTimeoutIsActive = true; + startOrientationMeasurementLocked(); + } + } + } + + public void stop() { + synchronized (mLock) { + if (mState == STATE_ACTIVE) { + mState = STATE_INACTIVE; + if (DEBUG) Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE."); + } + mHandler.removeCallbacks(mMeasurementTimeout); + mHandler.removeCallbacks(mSensorRestart); + mMeasurementTimeoutIsActive = false; + mSensorRestartIsActive = false; + if (mMeasurementInProgress) { + mMeasurementInProgress = false; + mSensorManager.unregisterListener(mListener); + } + mCurrentGravityVector = null; + mPreviousGravityVector = null; + if (mWakeLock.isHeld()) { + mHandler.removeCallbacks(mWakelockTimeout); + mWakelockTimeoutIsActive = false; + mWakeLock.release(); + } + } + } + + private void startOrientationMeasurementLocked() { + if (DEBUG) Slog.d(TAG, "startOrientationMeasurementLocked: mMeasurementInProgress=" + + mMeasurementInProgress + ", (mAccelSensor != null)=" + (mAccelSensor != null)); + if (!mMeasurementInProgress && mAccelSensor != null) { + if (mSensorManager.registerListener(mListener, mAccelSensor, + SAMPLING_INTERVAL_MILLIS * 1000)) { + mMeasurementInProgress = true; + mRunningStats.reset(); + } + Message measurementTimeoutMsg = Message.obtain(mHandler, mMeasurementTimeout); + mHandler.sendMessageDelayed(measurementTimeoutMsg, ACCELEROMETER_DATA_TIMEOUT_MILLIS); + mMeasurementTimeoutIsActive = true; + } + } + + private int stopOrientationMeasurementLocked() { + if (DEBUG) Slog.d(TAG, "stopOrientationMeasurement. mMeasurementInProgress=" + + mMeasurementInProgress); + int status = RESULT_UNKNOWN; + if (mMeasurementInProgress) { + mHandler.removeCallbacks(mMeasurementTimeout); + mMeasurementTimeoutIsActive = false; + mSensorManager.unregisterListener(mListener); + mMeasurementInProgress = false; + mPreviousGravityVector = mCurrentGravityVector; + mCurrentGravityVector = mRunningStats.getRunningAverage(); + if (mRunningStats.getSampleCount() == 0) { + Slog.w(TAG, "No accelerometer data acquired for orientation measurement."); + } + if (DEBUG) { + Slog.d(TAG, "mRunningStats = " + mRunningStats.toString()); + String currentGravityVectorString = (mCurrentGravityVector == null) ? + "null" : mCurrentGravityVector.toString(); + String previousGravityVectorString = (mPreviousGravityVector == null) ? + "null" : mPreviousGravityVector.toString(); + Slog.d(TAG, "mCurrentGravityVector = " + currentGravityVectorString); + Slog.d(TAG, "mPreviousGravityVector = " + previousGravityVectorString); + } + status = getStationaryStatus(); + mRunningStats.reset(); + if (DEBUG) Slog.d(TAG, "getStationaryStatus() returned " + status); + if (status != RESULT_UNKNOWN) { + if (mWakeLock.isHeld()) { + mHandler.removeCallbacks(mWakelockTimeout); + mWakelockTimeoutIsActive = false; + mWakeLock.release(); + } + if (DEBUG) { + Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE. status = " + status); + } + mState = STATE_INACTIVE; + } else { + /* + * Unknown due to insufficient measurements. Schedule another orientation + * measurement. + */ + if (DEBUG) Slog.d(TAG, "stopOrientationMeasurementLocked(): another measurement" + + " scheduled in " + ORIENTATION_MEASUREMENT_INTERVAL_MILLIS + + " milliseconds."); + Message msg = Message.obtain(mHandler, mSensorRestart); + mHandler.sendMessageDelayed(msg, ORIENTATION_MEASUREMENT_INTERVAL_MILLIS); + mSensorRestartIsActive = true; + } + } + return status; + } + + /* + * Updates mStatus to the current AnyMotion status. + */ + public int getStationaryStatus() { + if ((mPreviousGravityVector == null) || (mCurrentGravityVector == null)) { + return RESULT_UNKNOWN; + } + Vector3 previousGravityVectorNormalized = mPreviousGravityVector.normalized(); + Vector3 currentGravityVectorNormalized = mCurrentGravityVector.normalized(); + float angle = previousGravityVectorNormalized.angleBetween(currentGravityVectorNormalized); + if (DEBUG) Slog.d(TAG, "getStationaryStatus: angle = " + angle + + " energy = " + mRunningStats.getEnergy()); + if ((angle < mThresholdAngle) && (mRunningStats.getEnergy() < THRESHOLD_ENERGY)) { + return RESULT_STATIONARY; + } else if (Float.isNaN(angle)) { + /** + * Floating point rounding errors have caused the angle calcuation's dot product to + * exceed 1.0. In such case, we report RESULT_MOVED to prevent devices from rapidly + * retrying this measurement. + */ + return RESULT_MOVED; + } + long diffTime = mCurrentGravityVector.timeMillisSinceBoot - + mPreviousGravityVector.timeMillisSinceBoot; + if (diffTime > STALE_MEASUREMENT_TIMEOUT_MILLIS) { + if (DEBUG) Slog.d(TAG, "getStationaryStatus: mPreviousGravityVector is too stale at " + + diffTime + " ms ago. Returning RESULT_UNKNOWN."); + return RESULT_UNKNOWN; + } + return RESULT_MOVED; + } + + private final SensorEventListener mListener = new SensorEventListener() { + @Override + public void onSensorChanged(SensorEvent event) { + int status = RESULT_UNKNOWN; + synchronized (mLock) { + Vector3 accelDatum = new Vector3(SystemClock.elapsedRealtime(), event.values[0], + event.values[1], event.values[2]); + mRunningStats.accumulate(accelDatum); + + // If we have enough samples, stop accelerometer data acquisition. + if (mRunningStats.getSampleCount() >= mNumSufficientSamples) { + status = stopOrientationMeasurementLocked(); + } + } + if (status != RESULT_UNKNOWN) { + mHandler.removeCallbacks(mWakelockTimeout); + mWakelockTimeoutIsActive = false; + mCallback.onAnyMotionResult(status); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + }; + + private final Runnable mSensorRestart = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (mSensorRestartIsActive == true) { + mSensorRestartIsActive = false; + startOrientationMeasurementLocked(); + } + } + } + }; + + private final Runnable mMeasurementTimeout = new Runnable() { + @Override + public void run() { + int status = RESULT_UNKNOWN; + synchronized (mLock) { + if (mMeasurementTimeoutIsActive == true) { + mMeasurementTimeoutIsActive = false; + if (DEBUG) Slog.i(TAG, "mMeasurementTimeout. Failed to collect sufficient accel " + + "data within " + ACCELEROMETER_DATA_TIMEOUT_MILLIS + " ms. Stopping " + + "orientation measurement."); + status = stopOrientationMeasurementLocked(); + if (status != RESULT_UNKNOWN) { + mHandler.removeCallbacks(mWakelockTimeout); + mWakelockTimeoutIsActive = false; + mCallback.onAnyMotionResult(status); + } + } + } + } + }; + + private final Runnable mWakelockTimeout = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (mWakelockTimeoutIsActive == true) { + mWakelockTimeoutIsActive = false; + stop(); + } + } + } + }; + + /** + * A timestamped three dimensional vector and some vector operations. + */ + public static final class Vector3 { + public long timeMillisSinceBoot; + public float x; + public float y; + public float z; + + public Vector3(long timeMillisSinceBoot, float x, float y, float z) { + this.timeMillisSinceBoot = timeMillisSinceBoot; + this.x = x; + this.y = y; + this.z = z; + } + + public float norm() { + return (float) Math.sqrt(dotProduct(this)); + } + + public Vector3 normalized() { + float mag = norm(); + return new Vector3(timeMillisSinceBoot, x / mag, y / mag, z / mag); + } + + /** + * Returns the angle between this 3D vector and another given 3D vector. + * Assumes both have already been normalized. + * + * @param other The other Vector3 vector. + * @return angle between this vector and the other given one. + */ + public float angleBetween(Vector3 other) { + Vector3 crossVector = cross(other); + float degrees = Math.abs((float)Math.toDegrees( + Math.atan2(crossVector.norm(), dotProduct(other)))); + Slog.d(TAG, "angleBetween: this = " + this.toString() + + ", other = " + other.toString() + ", degrees = " + degrees); + return degrees; + } + + public Vector3 cross(Vector3 v) { + return new Vector3( + v.timeMillisSinceBoot, + y * v.z - z * v.y, + z * v.x - x * v.z, + x * v.y - y * v.x); + } + + @Override + public String toString() { + String msg = ""; + msg += "timeMillisSinceBoot=" + timeMillisSinceBoot; + msg += " | x=" + x; + msg += ", y=" + y; + msg += ", z=" + z; + return msg; + } + + public float dotProduct(Vector3 v) { + return x * v.x + y * v.y + z * v.z; + } + + public Vector3 times(float val) { + return new Vector3(timeMillisSinceBoot, x * val, y * val, z * val); + } + + public Vector3 plus(Vector3 v) { + return new Vector3(v.timeMillisSinceBoot, x + v.x, y + v.y, z + v.z); + } + + public Vector3 minus(Vector3 v) { + return new Vector3(v.timeMillisSinceBoot, x - v.x, y - v.y, z - v.z); + } + } + + /** + * Maintains running statistics on the signal revelant to AnyMotion detection, including: + * <ul> + * <li>running average. + * <li>running sum-of-squared-errors as the energy of the signal derivative. + * <ul> + */ + private static class RunningSignalStats { + Vector3 previousVector; + Vector3 currentVector; + Vector3 runningSum; + float energy; + int sampleCount; + + public RunningSignalStats() { + reset(); + } + + public void reset() { + previousVector = null; + currentVector = null; + runningSum = new Vector3(0, 0, 0, 0); + energy = 0; + sampleCount = 0; + } + + /** + * Apply a 3D vector v as the next element in the running SSE. + */ + public void accumulate(Vector3 v) { + if (v == null) { + if (DEBUG) Slog.i(TAG, "Cannot accumulate a null vector."); + return; + } + sampleCount++; + runningSum = runningSum.plus(v); + previousVector = currentVector; + currentVector = v; + if (previousVector != null) { + Vector3 dv = currentVector.minus(previousVector); + float incrementalEnergy = dv.x * dv.x + dv.y * dv.y + dv.z * dv.z; + energy += incrementalEnergy; + if (DEBUG) Slog.i(TAG, "Accumulated vector " + currentVector.toString() + + ", runningSum = " + runningSum.toString() + + ", incrementalEnergy = " + incrementalEnergy + + ", energy = " + energy); + } + } + + public Vector3 getRunningAverage() { + if (sampleCount > 0) { + return runningSum.times((float)(1.0f / sampleCount)); + } + return null; + } + + public float getEnergy() { + return energy; + } + + public int getSampleCount() { + return sampleCount; + } + + @Override + public String toString() { + String msg = ""; + String currentVectorString = (currentVector == null) ? + "null" : currentVector.toString(); + String previousVectorString = (previousVector == null) ? + "null" : previousVector.toString(); + msg += "previousVector = " + previousVectorString; + msg += ", currentVector = " + currentVectorString; + msg += ", sampleCount = " + sampleCount; + msg += ", energy = " + energy; + return msg; + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java new file mode 100644 index 000000000000..ac58f3d6a94d --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -0,0 +1,4638 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.Manifest; +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.app.AlarmManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.ContentObserver; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.hardware.TriggerEvent; +import android.hardware.TriggerEventListener; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationRequest; +import android.net.ConnectivityManager; +import android.net.INetworkPolicyManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.BatteryManager; +import android.os.BatteryStats; +import android.os.Binder; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.IDeviceIdleController; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerManager.ServiceType; +import android.os.PowerManagerInternal; +import android.os.Process; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ServiceManager; +import android.os.ShellCallback; +import android.os.ShellCommand; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.KeyValueListParser; +import android.util.MutableLong; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.TimeUtils; +import android.util.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IBatteryStats; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.DumpUtils; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.XmlUtils; +import com.android.server.am.BatteryStatsService; +import com.android.server.deviceidle.ConstraintController; +import com.android.server.deviceidle.DeviceIdleConstraintTracker; +import com.android.server.deviceidle.IDeviceIdleConstraint; +import com.android.server.deviceidle.TvConstraintController; +import com.android.server.net.NetworkPolicyManagerInternal; +import com.android.server.wm.ActivityTaskManagerInternal; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Keeps track of device idleness and drives low power mode based on that. + * + * Test: atest com.android.server.DeviceIdleControllerTest + * + * Current idling state machine (as of Android Q). This can be visualized using Graphviz: + <pre> + + digraph { + subgraph deep { + label="deep"; + + STATE_ACTIVE [label="STATE_ACTIVE\nScreen on OR Charging OR Alarm going off soon"] + STATE_INACTIVE [label="STATE_INACTIVE\nScreen off AND Not charging"] + STATE_QUICK_DOZE_DELAY [ + label="STATE_QUICK_DOZE_DELAY\n" + + "Screen off AND Not charging\n" + + "Location, motion detection, and significant motion monitoring turned off" + ] + STATE_IDLE_PENDING [ + label="STATE_IDLE_PENDING\nSignificant motion monitoring turned on" + ] + STATE_SENSING [label="STATE_SENSING\nMonitoring for ANY motion"] + STATE_LOCATING [ + label="STATE_LOCATING\nRequesting location, motion monitoring still on" + ] + STATE_IDLE [ + label="STATE_IDLE\nLocation and motion detection turned off\n" + + "Significant motion monitoring state unchanged" + ] + STATE_IDLE_MAINTENANCE [label="STATE_IDLE_MAINTENANCE\n"] + + STATE_ACTIVE -> STATE_INACTIVE [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze not enabled" + ] + STATE_ACTIVE -> STATE_QUICK_DOZE_DELAY [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze enabled" + ] + + STATE_INACTIVE -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_INACTIVE -> STATE_IDLE_PENDING [label="stepIdleStateLocked()"] + STATE_INACTIVE -> STATE_QUICK_DOZE_DELAY [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze enabled" + ] + + STATE_IDLE_PENDING -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_IDLE_PENDING -> STATE_SENSING [label="stepIdleStateLocked()"] + STATE_IDLE_PENDING -> STATE_QUICK_DOZE_DELAY [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze enabled" + ] + + STATE_SENSING -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_SENSING -> STATE_LOCATING [label="stepIdleStateLocked()"] + STATE_SENSING -> STATE_QUICK_DOZE_DELAY [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze enabled" + ] + STATE_SENSING -> STATE_IDLE [ + label="stepIdleStateLocked()\n" + + "No Location Manager OR (no Network provider AND no GPS provider)" + ] + + STATE_LOCATING -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_LOCATING -> STATE_QUICK_DOZE_DELAY [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze enabled" + ] + STATE_LOCATING -> STATE_IDLE [label="stepIdleStateLocked()"] + + STATE_QUICK_DOZE_DELAY -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_QUICK_DOZE_DELAY -> STATE_IDLE [label="stepIdleStateLocked()"] + + STATE_IDLE -> STATE_ACTIVE [label="handleMotionDetectedLocked(), becomeActiveLocked()"] + STATE_IDLE -> STATE_IDLE_MAINTENANCE [label="stepIdleStateLocked()"] + + STATE_IDLE_MAINTENANCE -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_IDLE_MAINTENANCE -> STATE_IDLE [ + label="stepIdleStateLocked(), exitMaintenanceEarlyIfNeededLocked()" + ] + } + + subgraph light { + label="light" + + LIGHT_STATE_ACTIVE [ + label="LIGHT_STATE_ACTIVE\nScreen on OR Charging OR Alarm going off soon" + ] + LIGHT_STATE_INACTIVE [label="LIGHT_STATE_INACTIVE\nScreen off AND Not charging"] + LIGHT_STATE_PRE_IDLE [ + label="LIGHT_STATE_PRE_IDLE\n" + + "Delay going into LIGHT_STATE_IDLE due to some running jobs or alarms" + ] + LIGHT_STATE_IDLE [label="LIGHT_STATE_IDLE\n"] + LIGHT_STATE_WAITING_FOR_NETWORK [ + label="LIGHT_STATE_WAITING_FOR_NETWORK\n" + + "Coming out of LIGHT_STATE_IDLE, waiting for network" + ] + LIGHT_STATE_IDLE_MAINTENANCE [label="LIGHT_STATE_IDLE_MAINTENANCE\n"] + LIGHT_STATE_OVERRIDE [ + label="LIGHT_STATE_OVERRIDE\nDevice in deep doze, light no longer changing states" + ] + + LIGHT_STATE_ACTIVE -> LIGHT_STATE_INACTIVE [ + label="becomeInactiveIfAppropriateLocked()" + ] + LIGHT_STATE_ACTIVE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"] + + LIGHT_STATE_INACTIVE -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"] + LIGHT_STATE_INACTIVE -> LIGHT_STATE_PRE_IDLE [label="active jobs"] + LIGHT_STATE_INACTIVE -> LIGHT_STATE_IDLE [label="no active jobs"] + LIGHT_STATE_INACTIVE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"] + + LIGHT_STATE_PRE_IDLE -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"] + LIGHT_STATE_PRE_IDLE -> LIGHT_STATE_IDLE [ + label="stepLightIdleStateLocked(), exitMaintenanceEarlyIfNeededLocked()" + ] + LIGHT_STATE_PRE_IDLE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"] + + LIGHT_STATE_IDLE -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"] + LIGHT_STATE_IDLE -> LIGHT_STATE_WAITING_FOR_NETWORK [label="no network"] + LIGHT_STATE_IDLE -> LIGHT_STATE_IDLE_MAINTENANCE + LIGHT_STATE_IDLE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"] + + LIGHT_STATE_WAITING_FOR_NETWORK -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"] + LIGHT_STATE_WAITING_FOR_NETWORK -> LIGHT_STATE_IDLE_MAINTENANCE + LIGHT_STATE_WAITING_FOR_NETWORK -> LIGHT_STATE_OVERRIDE [ + label="deep goes to STATE_IDLE" + ] + + LIGHT_STATE_IDLE_MAINTENANCE -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"] + LIGHT_STATE_IDLE_MAINTENANCE -> LIGHT_STATE_IDLE [ + label="stepLightIdleStateLocked(), exitMaintenanceEarlyIfNeededLocked()" + ] + LIGHT_STATE_IDLE_MAINTENANCE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"] + + LIGHT_STATE_OVERRIDE -> LIGHT_STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + } + } + </pre> + */ +public class DeviceIdleController extends SystemService + implements AnyMotionDetector.DeviceIdleCallback { + private static final String TAG = "DeviceIdleController"; + + private static final boolean DEBUG = false; + + private static final boolean COMPRESS_TIME = false; + + private static final int EVENT_BUFFER_SIZE = 100; + + private AlarmManager mAlarmManager; + private AlarmManagerInternal mLocalAlarmManager; + private IBatteryStats mBatteryStats; + private ActivityManagerInternal mLocalActivityManager; + private ActivityTaskManagerInternal mLocalActivityTaskManager; + private DeviceIdleInternal mLocalService; + private PowerManagerInternal mLocalPowerManager; + private PowerManager mPowerManager; + private INetworkPolicyManager mNetworkPolicyManager; + private SensorManager mSensorManager; + private final boolean mUseMotionSensor; + private Sensor mMotionSensor; + private LocationRequest mLocationRequest; + private Intent mIdleIntent; + private Intent mLightIdleIntent; + private AnyMotionDetector mAnyMotionDetector; + private final AppStateTracker mAppStateTracker; + private boolean mLightEnabled; + private boolean mDeepEnabled; + private boolean mQuickDozeActivated; + private boolean mQuickDozeActivatedWhileIdling; + private boolean mForceIdle; + private boolean mNetworkConnected; + private boolean mScreenOn; + private boolean mCharging; + private boolean mNotMoving; + private boolean mLocating; + private boolean mLocated; + private boolean mHasGps; + private boolean mHasNetworkLocation; + private Location mLastGenericLocation; + private Location mLastGpsLocation; + + /** Time in the elapsed realtime timebase when this listener last received a motion event. */ + private long mLastMotionEventElapsed; + + // Current locked state of the screen + private boolean mScreenLocked; + private int mNumBlockingConstraints = 0; + + /** + * Constraints are the "handbrakes" that stop the device from moving into a lower state until + * every one is released at the same time. + * + * @see #registerDeviceIdleConstraintInternal(IDeviceIdleConstraint, String, int) + */ + private final ArrayMap<IDeviceIdleConstraint, DeviceIdleConstraintTracker> + mConstraints = new ArrayMap<>(); + private ConstraintController mConstraintController; + + /** Device is currently active. */ + @VisibleForTesting + static final int STATE_ACTIVE = 0; + /** Device is inactive (screen off, no motion) and we are waiting to for idle. */ + @VisibleForTesting + static final int STATE_INACTIVE = 1; + /** Device is past the initial inactive period, and waiting for the next idle period. */ + @VisibleForTesting + static final int STATE_IDLE_PENDING = 2; + /** Device is currently sensing motion. */ + @VisibleForTesting + static final int STATE_SENSING = 3; + /** Device is currently finding location (and may still be sensing). */ + @VisibleForTesting + static final int STATE_LOCATING = 4; + /** Device is in the idle state, trying to stay asleep as much as possible. */ + @VisibleForTesting + static final int STATE_IDLE = 5; + /** Device is in the idle state, but temporarily out of idle to do regular maintenance. */ + @VisibleForTesting + static final int STATE_IDLE_MAINTENANCE = 6; + /** + * Device is inactive and should go straight into idle (foregoing motion and location + * monitoring), but allow some time for current work to complete first. + */ + @VisibleForTesting + static final int STATE_QUICK_DOZE_DELAY = 7; + + private static final int ACTIVE_REASON_UNKNOWN = 0; + private static final int ACTIVE_REASON_MOTION = 1; + private static final int ACTIVE_REASON_SCREEN = 2; + private static final int ACTIVE_REASON_CHARGING = 3; + private static final int ACTIVE_REASON_UNLOCKED = 4; + private static final int ACTIVE_REASON_FROM_BINDER_CALL = 5; + private static final int ACTIVE_REASON_FORCED = 6; + private static final int ACTIVE_REASON_ALARM = 7; + @VisibleForTesting + static final int SET_IDLE_FACTOR_RESULT_UNINIT = -1; + @VisibleForTesting + static final int SET_IDLE_FACTOR_RESULT_IGNORED = 0; + @VisibleForTesting + static final int SET_IDLE_FACTOR_RESULT_OK = 1; + @VisibleForTesting + static final int SET_IDLE_FACTOR_RESULT_NOT_SUPPORT = 2; + @VisibleForTesting + static final int SET_IDLE_FACTOR_RESULT_INVALID = 3; + @VisibleForTesting + static final long MIN_STATE_STEP_ALARM_CHANGE = 60 * 1000; + @VisibleForTesting + static final float MIN_PRE_IDLE_FACTOR_CHANGE = 0.05f; + + @VisibleForTesting + static String stateToString(int state) { + switch (state) { + case STATE_ACTIVE: return "ACTIVE"; + case STATE_INACTIVE: return "INACTIVE"; + case STATE_IDLE_PENDING: return "IDLE_PENDING"; + case STATE_SENSING: return "SENSING"; + case STATE_LOCATING: return "LOCATING"; + case STATE_IDLE: return "IDLE"; + case STATE_IDLE_MAINTENANCE: return "IDLE_MAINTENANCE"; + case STATE_QUICK_DOZE_DELAY: return "QUICK_DOZE_DELAY"; + default: return Integer.toString(state); + } + } + + /** Device is currently active. */ + @VisibleForTesting + static final int LIGHT_STATE_ACTIVE = 0; + /** Device is inactive (screen off) and we are waiting to for the first light idle. */ + @VisibleForTesting + static final int LIGHT_STATE_INACTIVE = 1; + /** Device is about to go idle for the first time, wait for current work to complete. */ + @VisibleForTesting + static final int LIGHT_STATE_PRE_IDLE = 3; + /** Device is in the light idle state, trying to stay asleep as much as possible. */ + @VisibleForTesting + static final int LIGHT_STATE_IDLE = 4; + /** Device is in the light idle state, we want to go in to idle maintenance but are + * waiting for network connectivity before doing so. */ + @VisibleForTesting + static final int LIGHT_STATE_WAITING_FOR_NETWORK = 5; + /** Device is in the light idle state, but temporarily out of idle to do regular maintenance. */ + @VisibleForTesting + static final int LIGHT_STATE_IDLE_MAINTENANCE = 6; + /** Device light idle state is overriden, now applying deep doze state. */ + @VisibleForTesting + static final int LIGHT_STATE_OVERRIDE = 7; + + @VisibleForTesting + static String lightStateToString(int state) { + switch (state) { + case LIGHT_STATE_ACTIVE: return "ACTIVE"; + case LIGHT_STATE_INACTIVE: return "INACTIVE"; + case LIGHT_STATE_PRE_IDLE: return "PRE_IDLE"; + case LIGHT_STATE_IDLE: return "IDLE"; + case LIGHT_STATE_WAITING_FOR_NETWORK: return "WAITING_FOR_NETWORK"; + case LIGHT_STATE_IDLE_MAINTENANCE: return "IDLE_MAINTENANCE"; + case LIGHT_STATE_OVERRIDE: return "OVERRIDE"; + default: return Integer.toString(state); + } + } + + private int mState; + private int mLightState; + + private long mInactiveTimeout; + private long mNextAlarmTime; + private long mNextIdlePendingDelay; + private long mNextIdleDelay; + private long mNextLightIdleDelay; + private long mNextLightAlarmTime; + private long mNextSensingTimeoutAlarmTime; + + /** How long a light idle maintenance window should last. */ + private long mCurLightIdleBudget; + + /** + * Start time of the current (light or full) maintenance window, in the elapsed timebase. Valid + * only if {@link #mState} == {@link #STATE_IDLE_MAINTENANCE} or + * {@link #mLightState} == {@link #LIGHT_STATE_IDLE_MAINTENANCE}. + */ + private long mMaintenanceStartTime; + private long mIdleStartTime; + + private int mActiveIdleOpCount; + private PowerManager.WakeLock mActiveIdleWakeLock; // held when there are operations in progress + private PowerManager.WakeLock mGoingIdleWakeLock; // held when we are going idle so hardware + // (especially NetworkPolicyManager) can shut + // down. + private boolean mJobsActive; + private boolean mAlarmsActive; + + /* Factor to apply to INACTIVE_TIMEOUT and IDLE_AFTER_INACTIVE_TIMEOUT in order to enter + * STATE_IDLE faster or slower. Don't apply this to SENSING_TIMEOUT or LOCATING_TIMEOUT because: + * - Both of them are shorter + * - Device sensor might take time be to become be stabilized + * Also don't apply the factor if the device is in motion because device motion provides a + * stronger signal than a prediction algorithm. + */ + private float mPreIdleFactor; + private float mLastPreIdleFactor; + private int mActiveReason; + + public final AtomicFile mConfigFile; + + /** + * Package names the system has white-listed to opt out of power save restrictions, + * except for device idle mode. + */ + private final ArrayMap<String, Integer> mPowerSaveWhitelistAppsExceptIdle = new ArrayMap<>(); + + /** + * Package names the user has white-listed using commandline option to opt out of + * power save restrictions, except for device idle mode. + */ + private final ArraySet<String> mPowerSaveWhitelistUserAppsExceptIdle = new ArraySet<>(); + + /** + * Package names the system has white-listed to opt out of power save restrictions for + * all modes. + */ + private final ArrayMap<String, Integer> mPowerSaveWhitelistApps = new ArrayMap<>(); + + /** + * Package names the user has white-listed to opt out of power save restrictions. + */ + private final ArrayMap<String, Integer> mPowerSaveWhitelistUserApps = new ArrayMap<>(); + + /** + * App IDs of built-in system apps that have been white-listed except for idle modes. + */ + private final SparseBooleanArray mPowerSaveWhitelistSystemAppIdsExceptIdle + = new SparseBooleanArray(); + + /** + * App IDs of built-in system apps that have been white-listed. + */ + private final SparseBooleanArray mPowerSaveWhitelistSystemAppIds = new SparseBooleanArray(); + + /** + * App IDs that have been white-listed to opt out of power save restrictions, except + * for device idle modes. + */ + private final SparseBooleanArray mPowerSaveWhitelistExceptIdleAppIds = new SparseBooleanArray(); + + /** + * Current app IDs that are in the complete power save white list, but shouldn't be + * excluded from idle modes. This array can be shared with others because it will not be + * modified once set. + */ + private int[] mPowerSaveWhitelistExceptIdleAppIdArray = new int[0]; + + /** + * App IDs that have been white-listed to opt out of power save restrictions. + */ + private final SparseBooleanArray mPowerSaveWhitelistAllAppIds = new SparseBooleanArray(); + + /** + * Current app IDs that are in the complete power save white list. This array can + * be shared with others because it will not be modified once set. + */ + private int[] mPowerSaveWhitelistAllAppIdArray = new int[0]; + + /** + * App IDs that have been white-listed by the user to opt out of power save restrictions. + */ + private final SparseBooleanArray mPowerSaveWhitelistUserAppIds = new SparseBooleanArray(); + + /** + * Current app IDs that are in the user power save white list. This array can + * be shared with others because it will not be modified once set. + */ + private int[] mPowerSaveWhitelistUserAppIdArray = new int[0]; + + /** + * List of end times for UIDs that are temporarily marked as being allowed to access + * the network and acquire wakelocks. Times are in milliseconds. + */ + private final SparseArray<Pair<MutableLong, String>> mTempWhitelistAppIdEndTimes + = new SparseArray<>(); + + private NetworkPolicyManagerInternal mNetworkPolicyManagerInternal; + + /** + * Current app IDs of temporarily whitelist apps for high-priority messages. + */ + private int[] mTempWhitelistAppIdArray = new int[0]; + + /** + * Apps in the system whitelist that have been taken out (probably because the user wanted to). + * They can be restored back by calling restoreAppToSystemWhitelist(String). + */ + private ArrayMap<String, Integer> mRemovedFromSystemWhitelistApps = new ArrayMap<>(); + + private final ArraySet<DeviceIdleInternal.StationaryListener> mStationaryListeners = + new ArraySet<>(); + + private static final int EVENT_NULL = 0; + private static final int EVENT_NORMAL = 1; + private static final int EVENT_LIGHT_IDLE = 2; + private static final int EVENT_LIGHT_MAINTENANCE = 3; + private static final int EVENT_DEEP_IDLE = 4; + private static final int EVENT_DEEP_MAINTENANCE = 5; + + private final int[] mEventCmds = new int[EVENT_BUFFER_SIZE]; + private final long[] mEventTimes = new long[EVENT_BUFFER_SIZE]; + private final String[] mEventReasons = new String[EVENT_BUFFER_SIZE]; + + private void addEvent(int cmd, String reason) { + if (mEventCmds[0] != cmd) { + System.arraycopy(mEventCmds, 0, mEventCmds, 1, EVENT_BUFFER_SIZE - 1); + System.arraycopy(mEventTimes, 0, mEventTimes, 1, EVENT_BUFFER_SIZE - 1); + System.arraycopy(mEventReasons, 0, mEventReasons, 1, EVENT_BUFFER_SIZE - 1); + mEventCmds[0] = cmd; + mEventTimes[0] = SystemClock.elapsedRealtime(); + mEventReasons[0] = reason; + } + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case ConnectivityManager.CONNECTIVITY_ACTION: { + updateConnectivityState(intent); + } break; + case Intent.ACTION_BATTERY_CHANGED: { + boolean present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true); + boolean plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; + synchronized (DeviceIdleController.this) { + updateChargingLocked(present && plugged); + } + } break; + case Intent.ACTION_PACKAGE_REMOVED: { + if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + Uri data = intent.getData(); + String ssp; + if (data != null && (ssp = data.getSchemeSpecificPart()) != null) { + removePowerSaveWhitelistAppInternal(ssp); + } + } + } break; + } + } + }; + + private final AlarmManager.OnAlarmListener mLightAlarmListener + = new AlarmManager.OnAlarmListener() { + @Override + public void onAlarm() { + synchronized (DeviceIdleController.this) { + stepLightIdleStateLocked("s:alarm"); + } + } + }; + + /** AlarmListener to start monitoring motion if there are registered stationary listeners. */ + private final AlarmManager.OnAlarmListener mMotionRegistrationAlarmListener = () -> { + synchronized (DeviceIdleController.this) { + if (mStationaryListeners.size() > 0) { + startMonitoringMotionLocked(); + } + } + }; + + private final AlarmManager.OnAlarmListener mMotionTimeoutAlarmListener = () -> { + synchronized (DeviceIdleController.this) { + if (!isStationaryLocked()) { + // If the device keeps registering motion, then the alarm should be + // rescheduled, so this shouldn't go off until the device is stationary. + // This case may happen in a race condition (alarm goes off right before + // motion is detected, but handleMotionDetectedLocked is called before + // we enter this block). + Slog.w(TAG, "motion timeout went off and device isn't stationary"); + return; + } + } + postStationaryStatusUpdated(); + }; + + private final AlarmManager.OnAlarmListener mSensingTimeoutAlarmListener + = new AlarmManager.OnAlarmListener() { + @Override + public void onAlarm() { + if (mState == STATE_SENSING) { + synchronized (DeviceIdleController.this) { + // Restart the device idle progression in case the device moved but the screen + // didn't turn on. + becomeInactiveIfAppropriateLocked(); + } + } + } + }; + + @VisibleForTesting + final AlarmManager.OnAlarmListener mDeepAlarmListener + = new AlarmManager.OnAlarmListener() { + @Override + public void onAlarm() { + synchronized (DeviceIdleController.this) { + stepIdleStateLocked("s:alarm"); + } + } + }; + + private final BroadcastReceiver mIdleStartedDoneReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + // When coming out of a deep idle, we will add in some delay before we allow + // the system to settle down and finish the maintenance window. This is + // to give a chance for any pending work to be scheduled. + if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction())) { + mHandler.sendEmptyMessageDelayed(MSG_FINISH_IDLE_OP, + mConstants.MIN_DEEP_MAINTENANCE_TIME); + } else { + mHandler.sendEmptyMessageDelayed(MSG_FINISH_IDLE_OP, + mConstants.MIN_LIGHT_MAINTENANCE_TIME); + } + } + }; + + private final BroadcastReceiver mInteractivityReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + synchronized (DeviceIdleController.this) { + updateInteractivityLocked(); + } + } + }; + + /** Post stationary status only to this listener. */ + private void postStationaryStatus(DeviceIdleInternal.StationaryListener listener) { + mHandler.obtainMessage(MSG_REPORT_STATIONARY_STATUS, listener).sendToTarget(); + } + + /** Post stationary status to all registered listeners. */ + private void postStationaryStatusUpdated() { + mHandler.sendEmptyMessage(MSG_REPORT_STATIONARY_STATUS); + } + + private boolean isStationaryLocked() { + final long now = mInjector.getElapsedRealtime(); + return mMotionListener.active + // Listening for motion for long enough and last motion was long enough ago. + && now - Math.max(mMotionListener.activatedTimeElapsed, mLastMotionEventElapsed) + >= mConstants.MOTION_INACTIVE_TIMEOUT; + } + + @VisibleForTesting + void registerStationaryListener(DeviceIdleInternal.StationaryListener listener) { + synchronized (this) { + if (!mStationaryListeners.add(listener)) { + // Listener already registered. + return; + } + postStationaryStatus(listener); + if (mMotionListener.active) { + if (!isStationaryLocked() && mStationaryListeners.size() == 1) { + // First listener to be registered and the device isn't stationary, so we + // need to register the alarm to report the device is stationary. + scheduleMotionTimeoutAlarmLocked(); + } + } else { + startMonitoringMotionLocked(); + scheduleMotionTimeoutAlarmLocked(); + } + } + } + + private void unregisterStationaryListener(DeviceIdleInternal.StationaryListener listener) { + synchronized (this) { + if (mStationaryListeners.remove(listener) && mStationaryListeners.size() == 0 + // Motion detection is started when transitioning from INACTIVE to IDLE_PENDING + // and so doesn't need to be on for ACTIVE or INACTIVE states. + // Motion detection isn't needed when idling due to Quick Doze. + && (mState == STATE_ACTIVE || mState == STATE_INACTIVE + || mQuickDozeActivated)) { + maybeStopMonitoringMotionLocked(); + } + } + } + + @VisibleForTesting + final class MotionListener extends TriggerEventListener + implements SensorEventListener { + + boolean active = false; + + /** + * Time in the elapsed realtime timebase when this listener was activated. Only valid if + * {@link #active} is true. + */ + long activatedTimeElapsed; + + public boolean isActive() { + return active; + } + + @Override + public void onTrigger(TriggerEvent event) { + synchronized (DeviceIdleController.this) { + // One_shot sensors (which call onTrigger) are unregistered when onTrigger is called + active = false; + motionLocked(); + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + synchronized (DeviceIdleController.this) { + // Since one_shot sensors are unregistered when onTrigger is called, unregister + // listeners here so that the MotionListener is in a consistent state when it calls + // out to motionLocked. + mSensorManager.unregisterListener(this, mMotionSensor); + active = false; + motionLocked(); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} + + public boolean registerLocked() { + boolean success; + if (mMotionSensor.getReportingMode() == Sensor.REPORTING_MODE_ONE_SHOT) { + success = mSensorManager.requestTriggerSensor(mMotionListener, mMotionSensor); + } else { + success = mSensorManager.registerListener( + mMotionListener, mMotionSensor, SensorManager.SENSOR_DELAY_NORMAL); + } + if (success) { + active = true; + activatedTimeElapsed = mInjector.getElapsedRealtime(); + } else { + Slog.e(TAG, "Unable to register for " + mMotionSensor); + } + return success; + } + + public void unregisterLocked() { + if (mMotionSensor.getReportingMode() == Sensor.REPORTING_MODE_ONE_SHOT) { + mSensorManager.cancelTriggerSensor(mMotionListener, mMotionSensor); + } else { + mSensorManager.unregisterListener(mMotionListener); + } + active = false; + } + } + @VisibleForTesting final MotionListener mMotionListener = new MotionListener(); + + private final LocationListener mGenericLocationListener = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + synchronized (DeviceIdleController.this) { + receivedGenericLocationLocked(location); + } + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onProviderDisabled(String provider) { + } + }; + + private final LocationListener mGpsLocationListener = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + synchronized (DeviceIdleController.this) { + receivedGpsLocationLocked(location); + } + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onProviderDisabled(String provider) { + } + }; + + /** + * All times are in milliseconds. These constants are kept synchronized with the system + * global Settings. Any access to this class or its fields should be done while + * holding the DeviceIdleController lock. + */ + public final class Constants extends ContentObserver { + // Key names stored in the settings value. + private static final String KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT + = "light_after_inactive_to"; + private static final String KEY_LIGHT_PRE_IDLE_TIMEOUT = "light_pre_idle_to"; + private static final String KEY_LIGHT_IDLE_TIMEOUT = "light_idle_to"; + private static final String KEY_LIGHT_IDLE_FACTOR = "light_idle_factor"; + private static final String KEY_LIGHT_MAX_IDLE_TIMEOUT = "light_max_idle_to"; + private static final String KEY_LIGHT_IDLE_MAINTENANCE_MIN_BUDGET + = "light_idle_maintenance_min_budget"; + private static final String KEY_LIGHT_IDLE_MAINTENANCE_MAX_BUDGET + = "light_idle_maintenance_max_budget"; + private static final String KEY_MIN_LIGHT_MAINTENANCE_TIME = "min_light_maintenance_time"; + private static final String KEY_MIN_DEEP_MAINTENANCE_TIME = "min_deep_maintenance_time"; + private static final String KEY_INACTIVE_TIMEOUT = "inactive_to"; + private static final String KEY_SENSING_TIMEOUT = "sensing_to"; + private static final String KEY_LOCATING_TIMEOUT = "locating_to"; + private static final String KEY_LOCATION_ACCURACY = "location_accuracy"; + private static final String KEY_MOTION_INACTIVE_TIMEOUT = "motion_inactive_to"; + private static final String KEY_IDLE_AFTER_INACTIVE_TIMEOUT = "idle_after_inactive_to"; + private static final String KEY_IDLE_PENDING_TIMEOUT = "idle_pending_to"; + private static final String KEY_MAX_IDLE_PENDING_TIMEOUT = "max_idle_pending_to"; + private static final String KEY_IDLE_PENDING_FACTOR = "idle_pending_factor"; + private static final String KEY_QUICK_DOZE_DELAY_TIMEOUT = "quick_doze_delay_to"; + private static final String KEY_IDLE_TIMEOUT = "idle_to"; + private static final String KEY_MAX_IDLE_TIMEOUT = "max_idle_to"; + private static final String KEY_IDLE_FACTOR = "idle_factor"; + private static final String KEY_MIN_TIME_TO_ALARM = "min_time_to_alarm"; + private static final String KEY_MAX_TEMP_APP_WHITELIST_DURATION = + "max_temp_app_whitelist_duration"; + private static final String KEY_MMS_TEMP_APP_WHITELIST_DURATION = + "mms_temp_app_whitelist_duration"; + private static final String KEY_SMS_TEMP_APP_WHITELIST_DURATION = + "sms_temp_app_whitelist_duration"; + private static final String KEY_NOTIFICATION_WHITELIST_DURATION = + "notification_whitelist_duration"; + /** + * Whether to wait for the user to unlock the device before causing screen-on to + * exit doze. Default = true + */ + private static final String KEY_WAIT_FOR_UNLOCK = "wait_for_unlock"; + private static final String KEY_PRE_IDLE_FACTOR_LONG = + "pre_idle_factor_long"; + private static final String KEY_PRE_IDLE_FACTOR_SHORT = + "pre_idle_factor_short"; + + /** + * This is the time, after becoming inactive, that we go in to the first + * light-weight idle mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT + */ + public long LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT; + + /** + * This is amount of time we will wait from the point where we decide we would + * like to go idle until we actually do, while waiting for jobs and other current + * activity to finish. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_PRE_IDLE_TIMEOUT + */ + public long LIGHT_PRE_IDLE_TIMEOUT; + + /** + * This is the initial time that we will run in idle maintenance mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_IDLE_TIMEOUT + */ + public long LIGHT_IDLE_TIMEOUT; + + /** + * Scaling factor to apply to the light idle mode time each time we complete a cycle. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_IDLE_FACTOR + */ + public float LIGHT_IDLE_FACTOR; + + /** + * This is the maximum time we will run in idle maintenance mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_MAX_IDLE_TIMEOUT + */ + public long LIGHT_MAX_IDLE_TIMEOUT; + + /** + * This is the minimum amount of time we want to make available for maintenance mode + * when lightly idling. That is, we will always have at least this amount of time + * available maintenance before timing out and cutting off maintenance mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_IDLE_MAINTENANCE_MIN_BUDGET + */ + public long LIGHT_IDLE_MAINTENANCE_MIN_BUDGET; + + /** + * This is the maximum amount of time we want to make available for maintenance mode + * when lightly idling. That is, if the system isn't using up its minimum maintenance + * budget and this time is being added to the budget reserve, this is the maximum + * reserve size we will allow to grow and thus the maximum amount of time we will + * allow for the maintenance window. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_IDLE_MAINTENANCE_MAX_BUDGET + */ + public long LIGHT_IDLE_MAINTENANCE_MAX_BUDGET; + + /** + * This is the minimum amount of time that we will stay in maintenance mode after + * a light doze. We have this minimum to allow various things to respond to switching + * in to maintenance mode and scheduling their work -- otherwise we may + * see there is nothing to do (no jobs pending) and go out of maintenance + * mode immediately. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MIN_LIGHT_MAINTENANCE_TIME + */ + public long MIN_LIGHT_MAINTENANCE_TIME; + + /** + * This is the minimum amount of time that we will stay in maintenance mode after + * a full doze. We have this minimum to allow various things to respond to switching + * in to maintenance mode and scheduling their work -- otherwise we may + * see there is nothing to do (no jobs pending) and go out of maintenance + * mode immediately. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MIN_DEEP_MAINTENANCE_TIME + */ + public long MIN_DEEP_MAINTENANCE_TIME; + + /** + * This is the time, after becoming inactive, at which we start looking at the + * motion sensor to determine if the device is being left alone. We don't do this + * immediately after going inactive just because we don't want to be continually running + * the motion sensor whenever the screen is off. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_INACTIVE_TIMEOUT + */ + public long INACTIVE_TIMEOUT; + + /** + * If we don't receive a callback from AnyMotion in this amount of time + + * {@link #LOCATING_TIMEOUT}, we will change from + * STATE_SENSING to STATE_INACTIVE, and any AnyMotion callbacks while not in STATE_SENSING + * will be ignored. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_SENSING_TIMEOUT + */ + public long SENSING_TIMEOUT; + + /** + * This is how long we will wait to try to get a good location fix before going in to + * idle mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LOCATING_TIMEOUT + */ + public long LOCATING_TIMEOUT; + + /** + * The desired maximum accuracy (in meters) we consider the location to be good enough to go + * on to idle. We will be trying to get an accuracy fix at least this good or until + * {@link #LOCATING_TIMEOUT} expires. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LOCATION_ACCURACY + */ + public float LOCATION_ACCURACY; + + /** + * This is the time, after seeing motion, that we wait after becoming inactive from + * that until we start looking for motion again. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MOTION_INACTIVE_TIMEOUT + */ + public long MOTION_INACTIVE_TIMEOUT; + + /** + * This is the time, after the inactive timeout elapses, that we will wait looking + * for motion until we truly consider the device to be idle. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_IDLE_AFTER_INACTIVE_TIMEOUT + */ + public long IDLE_AFTER_INACTIVE_TIMEOUT; + + /** + * This is the initial time, after being idle, that we will allow ourself to be back + * in the IDLE_MAINTENANCE state allowing the system to run normally until we return to + * idle. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_IDLE_PENDING_TIMEOUT + */ + public long IDLE_PENDING_TIMEOUT; + + /** + * Maximum pending idle timeout (time spent running) we will be allowed to use. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MAX_IDLE_PENDING_TIMEOUT + */ + public long MAX_IDLE_PENDING_TIMEOUT; + + /** + * Scaling factor to apply to current pending idle timeout each time we cycle through + * that state. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_IDLE_PENDING_FACTOR + */ + public float IDLE_PENDING_FACTOR; + + /** + * This is amount of time we will wait from the point where we go into + * STATE_QUICK_DOZE_DELAY until we actually go into STATE_IDLE, while waiting for jobs + * and other current activity to finish. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_QUICK_DOZE_DELAY_TIMEOUT + */ + public long QUICK_DOZE_DELAY_TIMEOUT; + + /** + * This is the initial time that we want to sit in the idle state before waking up + * again to return to pending idle and allowing normal work to run. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_IDLE_TIMEOUT + */ + public long IDLE_TIMEOUT; + + /** + * Maximum idle duration we will be allowed to use. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MAX_IDLE_TIMEOUT + */ + public long MAX_IDLE_TIMEOUT; + + /** + * Scaling factor to apply to current idle timeout each time we cycle through that state. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_IDLE_FACTOR + */ + public float IDLE_FACTOR; + + /** + * This is the minimum time we will allow until the next upcoming alarm for us to + * actually go in to idle mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MIN_TIME_TO_ALARM + */ + public long MIN_TIME_TO_ALARM; + + /** + * Max amount of time to temporarily whitelist an app when it receives a high priority + * tickle. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MAX_TEMP_APP_WHITELIST_DURATION + */ + public long MAX_TEMP_APP_WHITELIST_DURATION; + + /** + * Amount of time we would like to whitelist an app that is receiving an MMS. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MMS_TEMP_APP_WHITELIST_DURATION + */ + public long MMS_TEMP_APP_WHITELIST_DURATION; + + /** + * Amount of time we would like to whitelist an app that is receiving an SMS. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_SMS_TEMP_APP_WHITELIST_DURATION + */ + public long SMS_TEMP_APP_WHITELIST_DURATION; + + /** + * Amount of time we would like to whitelist an app that is handling a + * {@link android.app.PendingIntent} triggered by a {@link android.app.Notification}. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_NOTIFICATION_WHITELIST_DURATION + */ + public long NOTIFICATION_WHITELIST_DURATION; + + /** + * Pre idle time factor use to make idle delay longer + */ + public float PRE_IDLE_FACTOR_LONG; + + /** + * Pre idle time factor use to make idle delay shorter + */ + public float PRE_IDLE_FACTOR_SHORT; + + public boolean WAIT_FOR_UNLOCK; + + private final ContentResolver mResolver; + private final boolean mSmallBatteryDevice; + private final KeyValueListParser mParser = new KeyValueListParser(','); + + public Constants(Handler handler, ContentResolver resolver) { + super(handler); + mResolver = resolver; + mSmallBatteryDevice = ActivityManager.isSmallBatteryDevice(); + mResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.DEVICE_IDLE_CONSTANTS), + false, this); + updateConstants(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + updateConstants(); + } + + private void updateConstants() { + synchronized (DeviceIdleController.this) { + try { + mParser.setString(Settings.Global.getString(mResolver, + Settings.Global.DEVICE_IDLE_CONSTANTS)); + } catch (IllegalArgumentException e) { + // Failed to parse the settings string, log this and move on + // with defaults. + Slog.e(TAG, "Bad device idle settings", e); + } + + LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getDurationMillis( + KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT, + !COMPRESS_TIME ? 3 * 60 * 1000L : 15 * 1000L); + LIGHT_PRE_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_PRE_IDLE_TIMEOUT, + !COMPRESS_TIME ? 3 * 60 * 1000L : 30 * 1000L); + LIGHT_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_IDLE_TIMEOUT, + !COMPRESS_TIME ? 5 * 60 * 1000L : 15 * 1000L); + LIGHT_IDLE_FACTOR = mParser.getFloat(KEY_LIGHT_IDLE_FACTOR, + 2f); + LIGHT_MAX_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_MAX_IDLE_TIMEOUT, + !COMPRESS_TIME ? 15 * 60 * 1000L : 60 * 1000L); + LIGHT_IDLE_MAINTENANCE_MIN_BUDGET = mParser.getDurationMillis( + KEY_LIGHT_IDLE_MAINTENANCE_MIN_BUDGET, + !COMPRESS_TIME ? 1 * 60 * 1000L : 15 * 1000L); + LIGHT_IDLE_MAINTENANCE_MAX_BUDGET = mParser.getDurationMillis( + KEY_LIGHT_IDLE_MAINTENANCE_MAX_BUDGET, + !COMPRESS_TIME ? 5 * 60 * 1000L : 30 * 1000L); + MIN_LIGHT_MAINTENANCE_TIME = mParser.getDurationMillis( + KEY_MIN_LIGHT_MAINTENANCE_TIME, + !COMPRESS_TIME ? 5 * 1000L : 1 * 1000L); + MIN_DEEP_MAINTENANCE_TIME = mParser.getDurationMillis( + KEY_MIN_DEEP_MAINTENANCE_TIME, + !COMPRESS_TIME ? 30 * 1000L : 5 * 1000L); + long inactiveTimeoutDefault = (mSmallBatteryDevice ? 15 : 30) * 60 * 1000L; + INACTIVE_TIMEOUT = mParser.getDurationMillis(KEY_INACTIVE_TIMEOUT, + !COMPRESS_TIME ? inactiveTimeoutDefault : (inactiveTimeoutDefault / 10)); + SENSING_TIMEOUT = mParser.getDurationMillis(KEY_SENSING_TIMEOUT, + !COMPRESS_TIME ? 4 * 60 * 1000L : 60 * 1000L); + LOCATING_TIMEOUT = mParser.getDurationMillis(KEY_LOCATING_TIMEOUT, + !COMPRESS_TIME ? 30 * 1000L : 15 * 1000L); + LOCATION_ACCURACY = mParser.getFloat(KEY_LOCATION_ACCURACY, 20); + MOTION_INACTIVE_TIMEOUT = mParser.getDurationMillis(KEY_MOTION_INACTIVE_TIMEOUT, + !COMPRESS_TIME ? 10 * 60 * 1000L : 60 * 1000L); + long idleAfterInactiveTimeout = (mSmallBatteryDevice ? 15 : 30) * 60 * 1000L; + IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getDurationMillis( + KEY_IDLE_AFTER_INACTIVE_TIMEOUT, + !COMPRESS_TIME ? idleAfterInactiveTimeout + : (idleAfterInactiveTimeout / 10)); + IDLE_PENDING_TIMEOUT = mParser.getDurationMillis(KEY_IDLE_PENDING_TIMEOUT, + !COMPRESS_TIME ? 5 * 60 * 1000L : 30 * 1000L); + MAX_IDLE_PENDING_TIMEOUT = mParser.getDurationMillis(KEY_MAX_IDLE_PENDING_TIMEOUT, + !COMPRESS_TIME ? 10 * 60 * 1000L : 60 * 1000L); + IDLE_PENDING_FACTOR = mParser.getFloat(KEY_IDLE_PENDING_FACTOR, + 2f); + QUICK_DOZE_DELAY_TIMEOUT = mParser.getDurationMillis( + KEY_QUICK_DOZE_DELAY_TIMEOUT, !COMPRESS_TIME ? 60 * 1000L : 15 * 1000L); + IDLE_TIMEOUT = mParser.getDurationMillis(KEY_IDLE_TIMEOUT, + !COMPRESS_TIME ? 60 * 60 * 1000L : 6 * 60 * 1000L); + MAX_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_MAX_IDLE_TIMEOUT, + !COMPRESS_TIME ? 6 * 60 * 60 * 1000L : 30 * 60 * 1000L); + IDLE_FACTOR = mParser.getFloat(KEY_IDLE_FACTOR, + 2f); + MIN_TIME_TO_ALARM = mParser.getDurationMillis(KEY_MIN_TIME_TO_ALARM, + !COMPRESS_TIME ? 30 * 60 * 1000L : 6 * 60 * 1000L); + MAX_TEMP_APP_WHITELIST_DURATION = mParser.getDurationMillis( + KEY_MAX_TEMP_APP_WHITELIST_DURATION, 5 * 60 * 1000L); + MMS_TEMP_APP_WHITELIST_DURATION = mParser.getDurationMillis( + KEY_MMS_TEMP_APP_WHITELIST_DURATION, 60 * 1000L); + SMS_TEMP_APP_WHITELIST_DURATION = mParser.getDurationMillis( + KEY_SMS_TEMP_APP_WHITELIST_DURATION, 20 * 1000L); + NOTIFICATION_WHITELIST_DURATION = mParser.getDurationMillis( + KEY_NOTIFICATION_WHITELIST_DURATION, 30 * 1000L); + WAIT_FOR_UNLOCK = mParser.getBoolean(KEY_WAIT_FOR_UNLOCK, true); + PRE_IDLE_FACTOR_LONG = mParser.getFloat(KEY_PRE_IDLE_FACTOR_LONG, 1.67f); + PRE_IDLE_FACTOR_SHORT = mParser.getFloat(KEY_PRE_IDLE_FACTOR_SHORT, 0.33f); + } + } + + void dump(PrintWriter pw) { + pw.println(" Settings:"); + + pw.print(" "); pw.print(KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_PRE_IDLE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(LIGHT_PRE_IDLE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_IDLE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(LIGHT_IDLE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_IDLE_FACTOR); pw.print("="); + pw.print(LIGHT_IDLE_FACTOR); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_MAX_IDLE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(LIGHT_MAX_IDLE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_IDLE_MAINTENANCE_MIN_BUDGET); pw.print("="); + TimeUtils.formatDuration(LIGHT_IDLE_MAINTENANCE_MIN_BUDGET, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_IDLE_MAINTENANCE_MAX_BUDGET); pw.print("="); + TimeUtils.formatDuration(LIGHT_IDLE_MAINTENANCE_MAX_BUDGET, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MIN_LIGHT_MAINTENANCE_TIME); pw.print("="); + TimeUtils.formatDuration(MIN_LIGHT_MAINTENANCE_TIME, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MIN_DEEP_MAINTENANCE_TIME); pw.print("="); + TimeUtils.formatDuration(MIN_DEEP_MAINTENANCE_TIME, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_INACTIVE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(INACTIVE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_SENSING_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(SENSING_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LOCATING_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(LOCATING_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LOCATION_ACCURACY); pw.print("="); + pw.print(LOCATION_ACCURACY); pw.print("m"); + pw.println(); + + pw.print(" "); pw.print(KEY_MOTION_INACTIVE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(MOTION_INACTIVE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_IDLE_AFTER_INACTIVE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(IDLE_AFTER_INACTIVE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_IDLE_PENDING_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(IDLE_PENDING_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MAX_IDLE_PENDING_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(MAX_IDLE_PENDING_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_IDLE_PENDING_FACTOR); pw.print("="); + pw.println(IDLE_PENDING_FACTOR); + + pw.print(" "); pw.print(KEY_QUICK_DOZE_DELAY_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(QUICK_DOZE_DELAY_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_IDLE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(IDLE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MAX_IDLE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(MAX_IDLE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_IDLE_FACTOR); pw.print("="); + pw.println(IDLE_FACTOR); + + pw.print(" "); pw.print(KEY_MIN_TIME_TO_ALARM); pw.print("="); + TimeUtils.formatDuration(MIN_TIME_TO_ALARM, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MAX_TEMP_APP_WHITELIST_DURATION); pw.print("="); + TimeUtils.formatDuration(MAX_TEMP_APP_WHITELIST_DURATION, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MMS_TEMP_APP_WHITELIST_DURATION); pw.print("="); + TimeUtils.formatDuration(MMS_TEMP_APP_WHITELIST_DURATION, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_SMS_TEMP_APP_WHITELIST_DURATION); pw.print("="); + TimeUtils.formatDuration(SMS_TEMP_APP_WHITELIST_DURATION, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_NOTIFICATION_WHITELIST_DURATION); pw.print("="); + TimeUtils.formatDuration(NOTIFICATION_WHITELIST_DURATION, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_WAIT_FOR_UNLOCK); pw.print("="); + pw.println(WAIT_FOR_UNLOCK); + + pw.print(" "); pw.print(KEY_PRE_IDLE_FACTOR_LONG); pw.print("="); + pw.println(PRE_IDLE_FACTOR_LONG); + + pw.print(" "); pw.print(KEY_PRE_IDLE_FACTOR_SHORT); pw.print("="); + pw.println(PRE_IDLE_FACTOR_SHORT); + } + } + + private Constants mConstants; + + @Override + public void onAnyMotionResult(int result) { + if (DEBUG) Slog.d(TAG, "onAnyMotionResult(" + result + ")"); + if (result != AnyMotionDetector.RESULT_UNKNOWN) { + synchronized (this) { + cancelSensingTimeoutAlarmLocked(); + } + } + if ((result == AnyMotionDetector.RESULT_MOVED) || + (result == AnyMotionDetector.RESULT_UNKNOWN)) { + synchronized (this) { + handleMotionDetectedLocked(mConstants.INACTIVE_TIMEOUT, "non_stationary"); + } + } else if (result == AnyMotionDetector.RESULT_STATIONARY) { + if (mState == STATE_SENSING) { + // If we are currently sensing, it is time to move to locating. + synchronized (this) { + mNotMoving = true; + stepIdleStateLocked("s:stationary"); + } + } else if (mState == STATE_LOCATING) { + // If we are currently locating, note that we are not moving and step + // if we have located the position. + synchronized (this) { + mNotMoving = true; + if (mLocated) { + stepIdleStateLocked("s:stationary"); + } + } + } + } + } + + private static final int MSG_WRITE_CONFIG = 1; + private static final int MSG_REPORT_IDLE_ON = 2; + private static final int MSG_REPORT_IDLE_ON_LIGHT = 3; + private static final int MSG_REPORT_IDLE_OFF = 4; + private static final int MSG_REPORT_ACTIVE = 5; + private static final int MSG_TEMP_APP_WHITELIST_TIMEOUT = 6; + @VisibleForTesting + static final int MSG_REPORT_STATIONARY_STATUS = 7; + private static final int MSG_FINISH_IDLE_OP = 8; + private static final int MSG_REPORT_TEMP_APP_WHITELIST_CHANGED = 9; + private static final int MSG_SEND_CONSTRAINT_MONITORING = 10; + @VisibleForTesting + static final int MSG_UPDATE_PRE_IDLE_TIMEOUT_FACTOR = 11; + @VisibleForTesting + static final int MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR = 12; + + final class MyHandler extends Handler { + MyHandler(Looper looper) { + super(looper); + } + + @Override public void handleMessage(Message msg) { + if (DEBUG) Slog.d(TAG, "handleMessage(" + msg.what + ")"); + switch (msg.what) { + case MSG_WRITE_CONFIG: { + // Does not hold a wakelock. Just let this happen whenever. + handleWriteConfigFile(); + } break; + case MSG_REPORT_IDLE_ON: + case MSG_REPORT_IDLE_ON_LIGHT: { + // mGoingIdleWakeLock is held at this point + EventLogTags.writeDeviceIdleOnStart(); + final boolean deepChanged; + final boolean lightChanged; + if (msg.what == MSG_REPORT_IDLE_ON) { + deepChanged = mLocalPowerManager.setDeviceIdleMode(true); + lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false); + } else { + deepChanged = mLocalPowerManager.setDeviceIdleMode(false); + lightChanged = mLocalPowerManager.setLightDeviceIdleMode(true); + } + try { + mNetworkPolicyManager.setDeviceIdleMode(true); + mBatteryStats.noteDeviceIdleMode(msg.what == MSG_REPORT_IDLE_ON + ? BatteryStats.DEVICE_IDLE_MODE_DEEP + : BatteryStats.DEVICE_IDLE_MODE_LIGHT, null, Process.myUid()); + } catch (RemoteException e) { + } + if (deepChanged) { + getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL); + } + if (lightChanged) { + getContext().sendBroadcastAsUser(mLightIdleIntent, UserHandle.ALL); + } + EventLogTags.writeDeviceIdleOnComplete(); + mGoingIdleWakeLock.release(); + } break; + case MSG_REPORT_IDLE_OFF: { + // mActiveIdleWakeLock is held at this point + EventLogTags.writeDeviceIdleOffStart("unknown"); + final boolean deepChanged = mLocalPowerManager.setDeviceIdleMode(false); + final boolean lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false); + try { + mNetworkPolicyManager.setDeviceIdleMode(false); + mBatteryStats.noteDeviceIdleMode(BatteryStats.DEVICE_IDLE_MODE_OFF, + null, Process.myUid()); + } catch (RemoteException e) { + } + if (deepChanged) { + incActiveIdleOps(); + getContext().sendOrderedBroadcastAsUser(mIdleIntent, UserHandle.ALL, + null, mIdleStartedDoneReceiver, null, 0, null, null); + } + if (lightChanged) { + incActiveIdleOps(); + getContext().sendOrderedBroadcastAsUser(mLightIdleIntent, UserHandle.ALL, + null, mIdleStartedDoneReceiver, null, 0, null, null); + } + // Always start with one active op for the message being sent here. + // Now we are done! + decActiveIdleOps(); + EventLogTags.writeDeviceIdleOffComplete(); + } break; + case MSG_REPORT_ACTIVE: { + // The device is awake at this point, so no wakelock necessary. + String activeReason = (String)msg.obj; + int activeUid = msg.arg1; + EventLogTags.writeDeviceIdleOffStart( + activeReason != null ? activeReason : "unknown"); + final boolean deepChanged = mLocalPowerManager.setDeviceIdleMode(false); + final boolean lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false); + try { + mNetworkPolicyManager.setDeviceIdleMode(false); + mBatteryStats.noteDeviceIdleMode(BatteryStats.DEVICE_IDLE_MODE_OFF, + activeReason, activeUid); + } catch (RemoteException e) { + } + if (deepChanged) { + getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL); + } + if (lightChanged) { + getContext().sendBroadcastAsUser(mLightIdleIntent, UserHandle.ALL); + } + EventLogTags.writeDeviceIdleOffComplete(); + } break; + case MSG_TEMP_APP_WHITELIST_TIMEOUT: { + // TODO: What is keeping the device awake at this point? Does it need to be? + int appId = msg.arg1; + checkTempAppWhitelistTimeout(appId); + } break; + case MSG_FINISH_IDLE_OP: { + // mActiveIdleWakeLock is held at this point + decActiveIdleOps(); + } break; + case MSG_REPORT_TEMP_APP_WHITELIST_CHANGED: { + final int appId = msg.arg1; + final boolean added = (msg.arg2 == 1); + mNetworkPolicyManagerInternal.onTempPowerSaveWhitelistChange(appId, added); + } break; + case MSG_SEND_CONSTRAINT_MONITORING: { + final IDeviceIdleConstraint constraint = (IDeviceIdleConstraint) msg.obj; + final boolean monitoring = (msg.arg1 == 1); + if (monitoring) { + constraint.startMonitoring(); + } else { + constraint.stopMonitoring(); + } + } break; + case MSG_UPDATE_PRE_IDLE_TIMEOUT_FACTOR: { + updatePreIdleFactor(); + } break; + case MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR: { + updatePreIdleFactor(); + maybeDoImmediateMaintenance(); + } break; + case MSG_REPORT_STATIONARY_STATUS: { + final DeviceIdleInternal.StationaryListener newListener = + (DeviceIdleInternal.StationaryListener) msg.obj; + final DeviceIdleInternal.StationaryListener[] listeners; + final boolean isStationary; + synchronized (DeviceIdleController.this) { + isStationary = isStationaryLocked(); + if (newListener == null) { + // Only notify all listeners if we aren't directing to one listener. + listeners = mStationaryListeners.toArray( + new DeviceIdleInternal.StationaryListener[ + mStationaryListeners.size()]); + } else { + listeners = null; + } + } + if (listeners != null) { + for (DeviceIdleInternal.StationaryListener listener : listeners) { + listener.onDeviceStationaryChanged(isStationary); + } + } + if (newListener != null) { + newListener.onDeviceStationaryChanged(isStationary); + } + } + break; + } + } + } + + final MyHandler mHandler; + + BinderService mBinderService; + + private final class BinderService extends IDeviceIdleController.Stub { + @Override public void addPowerSaveWhitelistApp(String name) { + if (DEBUG) { + Slog.i(TAG, "addPowerSaveWhitelistApp(name = " + name + ")"); + } + addPowerSaveWhitelistApps(Collections.singletonList(name)); + } + + @Override + public int addPowerSaveWhitelistApps(List<String> packageNames) { + if (DEBUG) { + Slog.i(TAG, + "addPowerSaveWhitelistApps(name = " + packageNames + ")"); + } + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + return addPowerSaveWhitelistAppsInternal(packageNames); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public void removePowerSaveWhitelistApp(String name) { + if (DEBUG) { + Slog.i(TAG, "removePowerSaveWhitelistApp(name = " + name + ")"); + } + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + removePowerSaveWhitelistAppInternal(name); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public void removeSystemPowerWhitelistApp(String name) { + if (DEBUG) { + Slog.d(TAG, "removeAppFromSystemWhitelist(name = " + name + ")"); + } + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + removeSystemPowerWhitelistAppInternal(name); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public void restoreSystemPowerWhitelistApp(String name) { + if (DEBUG) { + Slog.d(TAG, "restoreAppToSystemWhitelist(name = " + name + ")"); + } + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + restoreSystemPowerWhitelistAppInternal(name); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + public String[] getRemovedSystemPowerWhitelistApps() { + return getRemovedSystemPowerWhitelistAppsInternal(); + } + + @Override public String[] getSystemPowerWhitelistExceptIdle() { + return getSystemPowerWhitelistExceptIdleInternal(); + } + + @Override public String[] getSystemPowerWhitelist() { + return getSystemPowerWhitelistInternal(); + } + + @Override public String[] getUserPowerWhitelist() { + return getUserPowerWhitelistInternal(); + } + + @Override public String[] getFullPowerWhitelistExceptIdle() { + return getFullPowerWhitelistExceptIdleInternal(); + } + + @Override public String[] getFullPowerWhitelist() { + return getFullPowerWhitelistInternal(); + } + + @Override public int[] getAppIdWhitelistExceptIdle() { + return getAppIdWhitelistExceptIdleInternal(); + } + + @Override public int[] getAppIdWhitelist() { + return getAppIdWhitelistInternal(); + } + + @Override public int[] getAppIdUserWhitelist() { + return getAppIdUserWhitelistInternal(); + } + + @Override public int[] getAppIdTempWhitelist() { + return getAppIdTempWhitelistInternal(); + } + + @Override public boolean isPowerSaveWhitelistExceptIdleApp(String name) { + return isPowerSaveWhitelistExceptIdleAppInternal(name); + } + + @Override public boolean isPowerSaveWhitelistApp(String name) { + return isPowerSaveWhitelistAppInternal(name); + } + + @Override + public long whitelistAppTemporarily(String packageName, int userId, String reason) + throws RemoteException { + // At least 10 seconds. + long duration = Math.max(10_000L, mConstants.MAX_TEMP_APP_WHITELIST_DURATION / 2); + addPowerSaveTempWhitelistAppChecked(packageName, duration, userId, reason); + return duration; + } + + @Override + public void addPowerSaveTempWhitelistApp(String packageName, long duration, + int userId, String reason) throws RemoteException { + addPowerSaveTempWhitelistAppChecked(packageName, duration, userId, reason); + } + + @Override public long addPowerSaveTempWhitelistAppForMms(String packageName, + int userId, String reason) throws RemoteException { + long duration = mConstants.MMS_TEMP_APP_WHITELIST_DURATION; + addPowerSaveTempWhitelistAppChecked(packageName, duration, userId, reason); + return duration; + } + + @Override public long addPowerSaveTempWhitelistAppForSms(String packageName, + int userId, String reason) throws RemoteException { + long duration = mConstants.SMS_TEMP_APP_WHITELIST_DURATION; + addPowerSaveTempWhitelistAppChecked(packageName, duration, userId, reason); + return duration; + } + + @Override public void exitIdle(String reason) { + getContext().enforceCallingOrSelfPermission(Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + exitIdleInternal(reason); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public int setPreIdleTimeoutMode(int mode) { + getContext().enforceCallingOrSelfPermission(Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + return DeviceIdleController.this.setPreIdleTimeoutMode(mode); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public void resetPreIdleTimeoutMode() { + getContext().enforceCallingOrSelfPermission(Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + DeviceIdleController.this.resetPreIdleTimeoutMode(); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + DeviceIdleController.this.dump(fd, pw, args); + } + + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, + FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { + (new Shell()).exec(this, in, out, err, args, callback, resultReceiver); + } + } + + private class LocalService implements DeviceIdleInternal { + @Override + public void onConstraintStateChanged(IDeviceIdleConstraint constraint, boolean active) { + synchronized (DeviceIdleController.this) { + onConstraintStateChangedLocked(constraint, active); + } + } + + @Override + public void registerDeviceIdleConstraint(IDeviceIdleConstraint constraint, String name, + @IDeviceIdleConstraint.MinimumState int minState) { + registerDeviceIdleConstraintInternal(constraint, name, minState); + } + + @Override + public void unregisterDeviceIdleConstraint(IDeviceIdleConstraint constraint) { + unregisterDeviceIdleConstraintInternal(constraint); + } + + @Override + public void exitIdle(String reason) { + exitIdleInternal(reason); + } + + // duration in milliseconds + @Override + public void addPowerSaveTempWhitelistApp(int callingUid, String packageName, + long duration, int userId, boolean sync, String reason) { + addPowerSaveTempWhitelistAppInternal(callingUid, packageName, duration, + userId, sync, reason); + } + + // duration in milliseconds + @Override + public void addPowerSaveTempWhitelistAppDirect(int uid, long duration, boolean sync, + String reason) { + addPowerSaveTempWhitelistAppDirectInternal(0, uid, duration, sync, reason); + } + + // duration in milliseconds + @Override + public long getNotificationWhitelistDuration() { + return mConstants.NOTIFICATION_WHITELIST_DURATION; + } + + @Override + public void setJobsActive(boolean active) { + DeviceIdleController.this.setJobsActive(active); + } + + // Up-call from alarm manager. + @Override + public void setAlarmsActive(boolean active) { + DeviceIdleController.this.setAlarmsActive(active); + } + + /** Is the app on any of the power save whitelists, whether system or user? */ + @Override + public boolean isAppOnWhitelist(int appid) { + return DeviceIdleController.this.isAppOnWhitelistInternal(appid); + } + + /** + * Returns the array of app ids whitelisted by user. Take care not to + * modify this, as it is a reference to the original copy. But the reference + * can change when the list changes, so it needs to be re-acquired when + * {@link PowerManager#ACTION_POWER_SAVE_WHITELIST_CHANGED} is sent. + */ + @Override + public int[] getPowerSaveWhitelistUserAppIds() { + return DeviceIdleController.this.getPowerSaveWhitelistUserAppIds(); + } + + @Override + public int[] getPowerSaveTempWhitelistAppIds() { + return DeviceIdleController.this.getAppIdTempWhitelistInternal(); + } + + @Override + public void registerStationaryListener(StationaryListener listener) { + DeviceIdleController.this.registerStationaryListener(listener); + } + + @Override + public void unregisterStationaryListener(StationaryListener listener) { + DeviceIdleController.this.unregisterStationaryListener(listener); + } + } + + static class Injector { + private final Context mContext; + private ConnectivityManager mConnectivityManager; + private Constants mConstants; + private LocationManager mLocationManager; + + Injector(Context ctx) { + mContext = ctx; + } + + AlarmManager getAlarmManager() { + return mContext.getSystemService(AlarmManager.class); + } + + AnyMotionDetector getAnyMotionDetector(Handler handler, SensorManager sm, + AnyMotionDetector.DeviceIdleCallback callback, float angleThreshold) { + return new AnyMotionDetector(getPowerManager(), handler, sm, callback, angleThreshold); + } + + AppStateTracker getAppStateTracker(Context ctx, Looper looper) { + return new AppStateTracker(ctx, looper); + } + + ConnectivityManager getConnectivityManager() { + if (mConnectivityManager == null) { + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); + } + return mConnectivityManager; + } + + Constants getConstants(DeviceIdleController controller, Handler handler, + ContentResolver resolver) { + if (mConstants == null) { + mConstants = controller.new Constants(handler, resolver); + } + return mConstants; + } + + + /** Returns the current elapsed realtime in milliseconds. */ + long getElapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + + LocationManager getLocationManager() { + if (mLocationManager == null) { + mLocationManager = mContext.getSystemService(LocationManager.class); + } + return mLocationManager; + } + + MyHandler getHandler(DeviceIdleController controller) { + return controller.new MyHandler(BackgroundThread.getHandler().getLooper()); + } + + Sensor getMotionSensor() { + final SensorManager sensorManager = getSensorManager(); + Sensor motionSensor = null; + int sigMotionSensorId = mContext.getResources().getInteger( + com.android.internal.R.integer.config_autoPowerModeAnyMotionSensor); + if (sigMotionSensorId > 0) { + motionSensor = sensorManager.getDefaultSensor(sigMotionSensorId, true); + } + if (motionSensor == null && mContext.getResources().getBoolean( + com.android.internal.R.bool.config_autoPowerModePreferWristTilt)) { + motionSensor = sensorManager.getDefaultSensor( + Sensor.TYPE_WRIST_TILT_GESTURE, true); + } + if (motionSensor == null) { + // As a last ditch, fall back to SMD. + motionSensor = sensorManager.getDefaultSensor( + Sensor.TYPE_SIGNIFICANT_MOTION, true); + } + return motionSensor; + } + + PowerManager getPowerManager() { + return mContext.getSystemService(PowerManager.class); + } + + SensorManager getSensorManager() { + return mContext.getSystemService(SensorManager.class); + } + + ConstraintController getConstraintController(Handler handler, + DeviceIdleInternal localService) { + if (mContext.getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY)) { + return new TvConstraintController(mContext, handler); + } + return null; + } + + boolean useMotionSensor() { + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_autoPowerModeUseMotionSensor); + } + } + + private final Injector mInjector; + + private ActivityTaskManagerInternal.ScreenObserver mScreenObserver = + new ActivityTaskManagerInternal.ScreenObserver() { + @Override + public void onAwakeStateChanged(boolean isAwake) { } + + @Override + public void onKeyguardStateChanged(boolean isShowing) { + synchronized (DeviceIdleController.this) { + DeviceIdleController.this.keyguardShowingLocked(isShowing); + } + } + }; + + @VisibleForTesting DeviceIdleController(Context context, Injector injector) { + super(context); + mInjector = injector; + mConfigFile = new AtomicFile(new File(getSystemDir(), "deviceidle.xml")); + mHandler = mInjector.getHandler(this); + mAppStateTracker = mInjector.getAppStateTracker(context, FgThread.get().getLooper()); + LocalServices.addService(AppStateTracker.class, mAppStateTracker); + mUseMotionSensor = mInjector.useMotionSensor(); + } + + public DeviceIdleController(Context context) { + this(context, new Injector(context)); + } + + boolean isAppOnWhitelistInternal(int appid) { + synchronized (this) { + return Arrays.binarySearch(mPowerSaveWhitelistAllAppIdArray, appid) >= 0; + } + } + + int[] getPowerSaveWhitelistUserAppIds() { + synchronized (this) { + return mPowerSaveWhitelistUserAppIdArray; + } + } + + private static File getSystemDir() { + return new File(Environment.getDataDirectory(), "system"); + } + + @Override + public void onStart() { + final PackageManager pm = getContext().getPackageManager(); + + synchronized (this) { + mLightEnabled = mDeepEnabled = getContext().getResources().getBoolean( + com.android.internal.R.bool.config_enableAutoPowerModes); + SystemConfig sysConfig = SystemConfig.getInstance(); + ArraySet<String> allowPowerExceptIdle = sysConfig.getAllowInPowerSaveExceptIdle(); + for (int i=0; i<allowPowerExceptIdle.size(); i++) { + String pkg = allowPowerExceptIdle.valueAt(i); + try { + ApplicationInfo ai = pm.getApplicationInfo(pkg, + PackageManager.MATCH_SYSTEM_ONLY); + int appid = UserHandle.getAppId(ai.uid); + mPowerSaveWhitelistAppsExceptIdle.put(ai.packageName, appid); + mPowerSaveWhitelistSystemAppIdsExceptIdle.put(appid, true); + } catch (PackageManager.NameNotFoundException e) { + } + } + ArraySet<String> allowPower = sysConfig.getAllowInPowerSave(); + for (int i=0; i<allowPower.size(); i++) { + String pkg = allowPower.valueAt(i); + try { + ApplicationInfo ai = pm.getApplicationInfo(pkg, + PackageManager.MATCH_SYSTEM_ONLY); + int appid = UserHandle.getAppId(ai.uid); + // These apps are on both the whitelist-except-idle as well + // as the full whitelist, so they apply in all cases. + mPowerSaveWhitelistAppsExceptIdle.put(ai.packageName, appid); + mPowerSaveWhitelistSystemAppIdsExceptIdle.put(appid, true); + mPowerSaveWhitelistApps.put(ai.packageName, appid); + mPowerSaveWhitelistSystemAppIds.put(appid, true); + } catch (PackageManager.NameNotFoundException e) { + } + } + + mConstants = mInjector.getConstants(this, mHandler, getContext().getContentResolver()); + + readConfigFileLocked(); + updateWhitelistAppIdsLocked(); + + mNetworkConnected = true; + mScreenOn = true; + mScreenLocked = false; + // Start out assuming we are charging. If we aren't, we will at least get + // a battery update the next time the level drops. + mCharging = true; + mActiveReason = ACTIVE_REASON_UNKNOWN; + mState = STATE_ACTIVE; + mLightState = LIGHT_STATE_ACTIVE; + mInactiveTimeout = mConstants.INACTIVE_TIMEOUT; + mPreIdleFactor = 1.0f; + mLastPreIdleFactor = 1.0f; + } + + mBinderService = new BinderService(); + publishBinderService(Context.DEVICE_IDLE_CONTROLLER, mBinderService); + mLocalService = new LocalService(); + publishLocalService(DeviceIdleInternal.class, mLocalService); + } + + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_SYSTEM_SERVICES_READY) { + synchronized (this) { + mAlarmManager = mInjector.getAlarmManager(); + mLocalAlarmManager = getLocalService(AlarmManagerInternal.class); + mBatteryStats = BatteryStatsService.getService(); + mLocalActivityManager = getLocalService(ActivityManagerInternal.class); + mLocalActivityTaskManager = getLocalService(ActivityTaskManagerInternal.class); + mLocalPowerManager = getLocalService(PowerManagerInternal.class); + mPowerManager = mInjector.getPowerManager(); + mActiveIdleWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "deviceidle_maint"); + mActiveIdleWakeLock.setReferenceCounted(false); + mGoingIdleWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "deviceidle_going_idle"); + mGoingIdleWakeLock.setReferenceCounted(true); + mNetworkPolicyManager = INetworkPolicyManager.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_POLICY_SERVICE)); + mNetworkPolicyManagerInternal = getLocalService(NetworkPolicyManagerInternal.class); + mSensorManager = mInjector.getSensorManager(); + + if (mUseMotionSensor) { + mMotionSensor = mInjector.getMotionSensor(); + } + + if (getContext().getResources().getBoolean( + com.android.internal.R.bool.config_autoPowerModePrefetchLocation)) { + mLocationRequest = LocationRequest.create() + .setQuality(LocationRequest.ACCURACY_FINE) + .setInterval(0) + .setFastestInterval(0) + .setNumUpdates(1); + } + + mConstraintController = mInjector.getConstraintController( + mHandler, getLocalService(LocalService.class)); + if (mConstraintController != null) { + mConstraintController.start(); + } + + float angleThreshold = getContext().getResources().getInteger( + com.android.internal.R.integer.config_autoPowerModeThresholdAngle) / 100f; + mAnyMotionDetector = mInjector.getAnyMotionDetector(mHandler, mSensorManager, this, + angleThreshold); + + mAppStateTracker.onSystemServicesReady(); + + mIdleIntent = new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + mIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY + | Intent.FLAG_RECEIVER_FOREGROUND); + mLightIdleIntent = new Intent(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED); + mLightIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY + | Intent.FLAG_RECEIVER_FOREGROUND); + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_BATTERY_CHANGED); + getContext().registerReceiver(mReceiver, filter); + + filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + getContext().registerReceiver(mReceiver, filter); + + filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + getContext().registerReceiver(mReceiver, filter); + + filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_SCREEN_ON); + getContext().registerReceiver(mInteractivityReceiver, filter); + + mLocalActivityManager.setDeviceIdleWhitelist( + mPowerSaveWhitelistAllAppIdArray, mPowerSaveWhitelistExceptIdleAppIdArray); + mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray); + + mLocalPowerManager.registerLowPowerModeObserver(ServiceType.QUICK_DOZE, + state -> { + synchronized (DeviceIdleController.this) { + updateQuickDozeFlagLocked(state.batterySaverEnabled); + } + }); + updateQuickDozeFlagLocked( + mLocalPowerManager.getLowPowerState( + ServiceType.QUICK_DOZE).batterySaverEnabled); + + mLocalActivityTaskManager.registerScreenObserver(mScreenObserver); + + passWhiteListsToForceAppStandbyTrackerLocked(); + updateInteractivityLocked(); + } + updateConnectivityState(null); + } + } + + @VisibleForTesting + boolean hasMotionSensor() { + return mUseMotionSensor && mMotionSensor != null; + } + + private void registerDeviceIdleConstraintInternal(IDeviceIdleConstraint constraint, + final String name, final int type) { + final int minState; + switch (type) { + case IDeviceIdleConstraint.ACTIVE: + minState = STATE_ACTIVE; + break; + case IDeviceIdleConstraint.SENSING_OR_ABOVE: + minState = STATE_SENSING; + break; + default: + Slog.wtf(TAG, "Registering device-idle constraint with invalid type: " + type); + return; + } + synchronized (this) { + if (mConstraints.containsKey(constraint)) { + Slog.e(TAG, "Re-registering device-idle constraint: " + constraint + "."); + return; + } + DeviceIdleConstraintTracker tracker = new DeviceIdleConstraintTracker(name, minState); + mConstraints.put(constraint, tracker); + updateActiveConstraintsLocked(); + } + } + + private void unregisterDeviceIdleConstraintInternal(IDeviceIdleConstraint constraint) { + synchronized (this) { + // Artificially force the constraint to inactive to unblock anything waiting for it. + onConstraintStateChangedLocked(constraint, /* active= */ false); + + // Let the constraint know that we are not listening to it any more. + setConstraintMonitoringLocked(constraint, /* monitoring= */ false); + mConstraints.remove(constraint); + } + } + + @GuardedBy("this") + private void onConstraintStateChangedLocked(IDeviceIdleConstraint constraint, boolean active) { + DeviceIdleConstraintTracker tracker = mConstraints.get(constraint); + if (tracker == null) { + Slog.e(TAG, "device-idle constraint " + constraint + " has not been registered."); + return; + } + if (active != tracker.active && tracker.monitoring) { + tracker.active = active; + mNumBlockingConstraints += (tracker.active ? +1 : -1); + if (mNumBlockingConstraints == 0) { + if (mState == STATE_ACTIVE) { + becomeInactiveIfAppropriateLocked(); + } else if (mNextAlarmTime == 0 || mNextAlarmTime < SystemClock.elapsedRealtime()) { + stepIdleStateLocked("s:" + tracker.name); + } + } + } + } + + @GuardedBy("this") + private void setConstraintMonitoringLocked(IDeviceIdleConstraint constraint, boolean monitor) { + DeviceIdleConstraintTracker tracker = mConstraints.get(constraint); + if (tracker.monitoring != monitor) { + tracker.monitoring = monitor; + updateActiveConstraintsLocked(); + // We send the callback on a separate thread instead of just relying on oneway as + // the client could be in the system server with us and cause re-entry problems. + mHandler.obtainMessage(MSG_SEND_CONSTRAINT_MONITORING, + /* monitoring= */ monitor ? 1 : 0, + /* <not used>= */ -1, + /* constraint= */ constraint).sendToTarget(); + } + } + + @GuardedBy("this") + private void updateActiveConstraintsLocked() { + mNumBlockingConstraints = 0; + for (int i = 0; i < mConstraints.size(); i++) { + final IDeviceIdleConstraint constraint = mConstraints.keyAt(i); + final DeviceIdleConstraintTracker tracker = mConstraints.valueAt(i); + final boolean monitoring = (tracker.minState == mState); + if (monitoring != tracker.monitoring) { + setConstraintMonitoringLocked(constraint, monitoring); + tracker.active = monitoring; + } + if (tracker.monitoring && tracker.active) { + mNumBlockingConstraints++; + } + } + } + + private int addPowerSaveWhitelistAppsInternal(List<String> pkgNames) { + int numAdded = 0; + int numErrors = 0; + synchronized (this) { + for (int i = pkgNames.size() - 1; i >= 0; --i) { + final String name = pkgNames.get(i); + if (name == null) { + numErrors++; + continue; + } + try { + ApplicationInfo ai = getContext().getPackageManager().getApplicationInfo(name, + PackageManager.MATCH_ANY_USER); + if (mPowerSaveWhitelistUserApps.put(name, UserHandle.getAppId(ai.uid)) + == null) { + numAdded++; + } + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Tried to add unknown package to power save whitelist: " + name); + numErrors++; + } + } + if (numAdded > 0) { + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); + } + } + return pkgNames.size() - numErrors; + } + + public boolean removePowerSaveWhitelistAppInternal(String name) { + synchronized (this) { + if (mPowerSaveWhitelistUserApps.remove(name) != null) { + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); + return true; + } + } + return false; + } + + public boolean getPowerSaveWhitelistAppInternal(String name) { + synchronized (this) { + return mPowerSaveWhitelistUserApps.containsKey(name); + } + } + + void resetSystemPowerWhitelistInternal() { + synchronized (this) { + mPowerSaveWhitelistApps.putAll(mRemovedFromSystemWhitelistApps); + mRemovedFromSystemWhitelistApps.clear(); + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); + } + } + + public boolean restoreSystemPowerWhitelistAppInternal(String name) { + synchronized (this) { + if (!mRemovedFromSystemWhitelistApps.containsKey(name)) { + return false; + } + mPowerSaveWhitelistApps.put(name, mRemovedFromSystemWhitelistApps.remove(name)); + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); + return true; + } + } + + public boolean removeSystemPowerWhitelistAppInternal(String name) { + synchronized (this) { + if (!mPowerSaveWhitelistApps.containsKey(name)) { + return false; + } + mRemovedFromSystemWhitelistApps.put(name, mPowerSaveWhitelistApps.remove(name)); + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); + return true; + } + } + + public boolean addPowerSaveWhitelistExceptIdleInternal(String name) { + synchronized (this) { + try { + final ApplicationInfo ai = getContext().getPackageManager().getApplicationInfo(name, + PackageManager.MATCH_ANY_USER); + if (mPowerSaveWhitelistAppsExceptIdle.put(name, UserHandle.getAppId(ai.uid)) + == null) { + mPowerSaveWhitelistUserAppsExceptIdle.add(name); + reportPowerSaveWhitelistChangedLocked(); + mPowerSaveWhitelistExceptIdleAppIdArray = buildAppIdArray( + mPowerSaveWhitelistAppsExceptIdle, mPowerSaveWhitelistUserApps, + mPowerSaveWhitelistExceptIdleAppIds); + + passWhiteListsToForceAppStandbyTrackerLocked(); + } + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + } + + public void resetPowerSaveWhitelistExceptIdleInternal() { + synchronized (this) { + if (mPowerSaveWhitelistAppsExceptIdle.removeAll( + mPowerSaveWhitelistUserAppsExceptIdle)) { + reportPowerSaveWhitelistChangedLocked(); + mPowerSaveWhitelistExceptIdleAppIdArray = buildAppIdArray( + mPowerSaveWhitelistAppsExceptIdle, mPowerSaveWhitelistUserApps, + mPowerSaveWhitelistExceptIdleAppIds); + mPowerSaveWhitelistUserAppsExceptIdle.clear(); + + passWhiteListsToForceAppStandbyTrackerLocked(); + } + } + } + + public boolean getPowerSaveWhitelistExceptIdleInternal(String name) { + synchronized (this) { + return mPowerSaveWhitelistAppsExceptIdle.containsKey(name); + } + } + + public String[] getSystemPowerWhitelistExceptIdleInternal() { + synchronized (this) { + int size = mPowerSaveWhitelistAppsExceptIdle.size(); + String[] apps = new String[size]; + for (int i = 0; i < size; i++) { + apps[i] = mPowerSaveWhitelistAppsExceptIdle.keyAt(i); + } + return apps; + } + } + + public String[] getSystemPowerWhitelistInternal() { + synchronized (this) { + int size = mPowerSaveWhitelistApps.size(); + String[] apps = new String[size]; + for (int i = 0; i < size; i++) { + apps[i] = mPowerSaveWhitelistApps.keyAt(i); + } + return apps; + } + } + + public String[] getRemovedSystemPowerWhitelistAppsInternal() { + synchronized (this) { + int size = mRemovedFromSystemWhitelistApps.size(); + final String[] apps = new String[size]; + for (int i = 0; i < size; i++) { + apps[i] = mRemovedFromSystemWhitelistApps.keyAt(i); + } + return apps; + } + } + + public String[] getUserPowerWhitelistInternal() { + synchronized (this) { + int size = mPowerSaveWhitelistUserApps.size(); + String[] apps = new String[size]; + for (int i = 0; i < mPowerSaveWhitelistUserApps.size(); i++) { + apps[i] = mPowerSaveWhitelistUserApps.keyAt(i); + } + return apps; + } + } + + public String[] getFullPowerWhitelistExceptIdleInternal() { + synchronized (this) { + int size = mPowerSaveWhitelistAppsExceptIdle.size() + mPowerSaveWhitelistUserApps.size(); + String[] apps = new String[size]; + int cur = 0; + for (int i = 0; i < mPowerSaveWhitelistAppsExceptIdle.size(); i++) { + apps[cur] = mPowerSaveWhitelistAppsExceptIdle.keyAt(i); + cur++; + } + for (int i = 0; i < mPowerSaveWhitelistUserApps.size(); i++) { + apps[cur] = mPowerSaveWhitelistUserApps.keyAt(i); + cur++; + } + return apps; + } + } + + public String[] getFullPowerWhitelistInternal() { + synchronized (this) { + int size = mPowerSaveWhitelistApps.size() + mPowerSaveWhitelistUserApps.size(); + String[] apps = new String[size]; + int cur = 0; + for (int i = 0; i < mPowerSaveWhitelistApps.size(); i++) { + apps[cur] = mPowerSaveWhitelistApps.keyAt(i); + cur++; + } + for (int i = 0; i < mPowerSaveWhitelistUserApps.size(); i++) { + apps[cur] = mPowerSaveWhitelistUserApps.keyAt(i); + cur++; + } + return apps; + } + } + + public boolean isPowerSaveWhitelistExceptIdleAppInternal(String packageName) { + synchronized (this) { + return mPowerSaveWhitelistAppsExceptIdle.containsKey(packageName) + || mPowerSaveWhitelistUserApps.containsKey(packageName); + } + } + + public boolean isPowerSaveWhitelistAppInternal(String packageName) { + synchronized (this) { + return mPowerSaveWhitelistApps.containsKey(packageName) + || mPowerSaveWhitelistUserApps.containsKey(packageName); + } + } + + public int[] getAppIdWhitelistExceptIdleInternal() { + synchronized (this) { + return mPowerSaveWhitelistExceptIdleAppIdArray; + } + } + + public int[] getAppIdWhitelistInternal() { + synchronized (this) { + return mPowerSaveWhitelistAllAppIdArray; + } + } + + public int[] getAppIdUserWhitelistInternal() { + synchronized (this) { + return mPowerSaveWhitelistUserAppIdArray; + } + } + + public int[] getAppIdTempWhitelistInternal() { + synchronized (this) { + return mTempWhitelistAppIdArray; + } + } + + void addPowerSaveTempWhitelistAppChecked(String packageName, long duration, + int userId, String reason) throws RemoteException { + getContext().enforceCallingPermission( + Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, + "No permission to change device idle whitelist"); + final int callingUid = Binder.getCallingUid(); + userId = ActivityManager.getService().handleIncomingUser( + Binder.getCallingPid(), + callingUid, + userId, + /*allowAll=*/ false, + /*requireFull=*/ false, + "addPowerSaveTempWhitelistApp", null); + final long token = Binder.clearCallingIdentity(); + try { + addPowerSaveTempWhitelistAppInternal(callingUid, + packageName, duration, userId, true, reason); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + void removePowerSaveTempWhitelistAppChecked(String packageName, int userId) + throws RemoteException { + getContext().enforceCallingPermission( + Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, + "No permission to change device idle whitelist"); + final int callingUid = Binder.getCallingUid(); + userId = ActivityManager.getService().handleIncomingUser( + Binder.getCallingPid(), + callingUid, + userId, + /*allowAll=*/ false, + /*requireFull=*/ false, + "removePowerSaveTempWhitelistApp", null); + final long token = Binder.clearCallingIdentity(); + try { + removePowerSaveTempWhitelistAppInternal(packageName, userId); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Adds an app to the temporary whitelist and resets the endTime for granting the + * app an exemption to access network and acquire wakelocks. + */ + void addPowerSaveTempWhitelistAppInternal(int callingUid, String packageName, + long duration, int userId, boolean sync, String reason) { + try { + int uid = getContext().getPackageManager().getPackageUidAsUser(packageName, userId); + addPowerSaveTempWhitelistAppDirectInternal(callingUid, uid, duration, sync, reason); + } catch (NameNotFoundException e) { + } + } + + /** + * Adds an app to the temporary whitelist and resets the endTime for granting the + * app an exemption to access network and acquire wakelocks. + */ + void addPowerSaveTempWhitelistAppDirectInternal(int callingUid, int uid, + long duration, boolean sync, String reason) { + final long timeNow = SystemClock.elapsedRealtime(); + boolean informWhitelistChanged = false; + int appId = UserHandle.getAppId(uid); + synchronized (this) { + int callingAppId = UserHandle.getAppId(callingUid); + if (callingAppId >= Process.FIRST_APPLICATION_UID) { + if (!mPowerSaveWhitelistSystemAppIds.get(callingAppId)) { + throw new SecurityException("Calling app " + UserHandle.formatUid(callingUid) + + " is not on whitelist"); + } + } + duration = Math.min(duration, mConstants.MAX_TEMP_APP_WHITELIST_DURATION); + Pair<MutableLong, String> entry = mTempWhitelistAppIdEndTimes.get(appId); + final boolean newEntry = entry == null; + // Set the new end time + if (newEntry) { + entry = new Pair<>(new MutableLong(0), reason); + mTempWhitelistAppIdEndTimes.put(appId, entry); + } + entry.first.value = timeNow + duration; + if (DEBUG) { + Slog.d(TAG, "Adding AppId " + appId + " to temp whitelist. New entry: " + newEntry); + } + if (newEntry) { + // No pending timeout for the app id, post a delayed message + try { + mBatteryStats.noteEvent(BatteryStats.HistoryItem.EVENT_TEMP_WHITELIST_START, + reason, uid); + } catch (RemoteException e) { + } + postTempActiveTimeoutMessage(appId, duration); + updateTempWhitelistAppIdsLocked(appId, true); + if (sync) { + informWhitelistChanged = true; + } else { + mHandler.obtainMessage(MSG_REPORT_TEMP_APP_WHITELIST_CHANGED, appId, 1) + .sendToTarget(); + } + reportTempWhitelistChangedLocked(); + } + } + if (informWhitelistChanged) { + mNetworkPolicyManagerInternal.onTempPowerSaveWhitelistChange(appId, true); + } + } + + /** + * Removes an app from the temporary whitelist and notifies the observers. + */ + private void removePowerSaveTempWhitelistAppInternal(String packageName, int userId) { + try { + final int uid = getContext().getPackageManager().getPackageUidAsUser( + packageName, userId); + final int appId = UserHandle.getAppId(uid); + removePowerSaveTempWhitelistAppDirectInternal(appId); + } catch (NameNotFoundException e) { + } + } + + private void removePowerSaveTempWhitelistAppDirectInternal(int appId) { + synchronized (this) { + final int idx = mTempWhitelistAppIdEndTimes.indexOfKey(appId); + if (idx < 0) { + // Nothing else to do + return; + } + final String reason = mTempWhitelistAppIdEndTimes.valueAt(idx).second; + mTempWhitelistAppIdEndTimes.removeAt(idx); + onAppRemovedFromTempWhitelistLocked(appId, reason); + } + } + + private void postTempActiveTimeoutMessage(int appId, long delay) { + if (DEBUG) { + Slog.d(TAG, "postTempActiveTimeoutMessage: appId=" + appId + ", delay=" + delay); + } + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_TEMP_APP_WHITELIST_TIMEOUT, appId, 0), delay); + } + + void checkTempAppWhitelistTimeout(int appId) { + final long timeNow = SystemClock.elapsedRealtime(); + if (DEBUG) { + Slog.d(TAG, "checkTempAppWhitelistTimeout: appId=" + appId + ", timeNow=" + timeNow); + } + synchronized (this) { + Pair<MutableLong, String> entry = mTempWhitelistAppIdEndTimes.get(appId); + if (entry == null) { + // Nothing to do + return; + } + if (timeNow >= entry.first.value) { + mTempWhitelistAppIdEndTimes.delete(appId); + onAppRemovedFromTempWhitelistLocked(appId, entry.second); + } else { + // Need more time + if (DEBUG) { + Slog.d(TAG, "Time to remove AppId " + appId + ": " + entry.first.value); + } + postTempActiveTimeoutMessage(appId, entry.first.value - timeNow); + } + } + } + + @GuardedBy("this") + private void onAppRemovedFromTempWhitelistLocked(int appId, String reason) { + if (DEBUG) { + Slog.d(TAG, "Removing appId " + appId + " from temp whitelist"); + } + updateTempWhitelistAppIdsLocked(appId, false); + mHandler.obtainMessage(MSG_REPORT_TEMP_APP_WHITELIST_CHANGED, appId, 0) + .sendToTarget(); + reportTempWhitelistChangedLocked(); + try { + mBatteryStats.noteEvent(BatteryStats.HistoryItem.EVENT_TEMP_WHITELIST_FINISH, + reason, appId); + } catch (RemoteException e) { + } + } + + public void exitIdleInternal(String reason) { + synchronized (this) { + mActiveReason = ACTIVE_REASON_FROM_BINDER_CALL; + becomeActiveLocked(reason, Binder.getCallingUid()); + } + } + + @VisibleForTesting + boolean isNetworkConnected() { + synchronized (this) { + return mNetworkConnected; + } + } + + void updateConnectivityState(Intent connIntent) { + ConnectivityManager cm; + synchronized (this) { + cm = mInjector.getConnectivityManager(); + } + if (cm == null) { + return; + } + // Note: can't call out to ConnectivityService with our lock held. + NetworkInfo ni = cm.getActiveNetworkInfo(); + synchronized (this) { + boolean conn; + if (ni == null) { + conn = false; + } else { + if (connIntent == null) { + conn = ni.isConnected(); + } else { + final int networkType = + connIntent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, + ConnectivityManager.TYPE_NONE); + if (ni.getType() != networkType) { + return; + } + conn = !connIntent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, + false); + } + } + if (conn != mNetworkConnected) { + mNetworkConnected = conn; + if (conn && mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) { + stepLightIdleStateLocked("network"); + } + } + } + } + + @VisibleForTesting + boolean isScreenOn() { + synchronized (this) { + return mScreenOn; + } + } + + void updateInteractivityLocked() { + // The interactivity state from the power manager tells us whether the display is + // in a state that we need to keep things running so they will update at a normal + // frequency. + boolean screenOn = mPowerManager.isInteractive(); + if (DEBUG) Slog.d(TAG, "updateInteractivityLocked: screenOn=" + screenOn); + if (!screenOn && mScreenOn) { + mScreenOn = false; + if (!mForceIdle) { + becomeInactiveIfAppropriateLocked(); + } + } else if (screenOn) { + mScreenOn = true; + if (!mForceIdle && (!mScreenLocked || !mConstants.WAIT_FOR_UNLOCK)) { + mActiveReason = ACTIVE_REASON_SCREEN; + becomeActiveLocked("screen", Process.myUid()); + } + } + } + + @VisibleForTesting + boolean isCharging() { + synchronized (this) { + return mCharging; + } + } + + void updateChargingLocked(boolean charging) { + if (DEBUG) Slog.i(TAG, "updateChargingLocked: charging=" + charging); + if (!charging && mCharging) { + mCharging = false; + if (!mForceIdle) { + becomeInactiveIfAppropriateLocked(); + } + } else if (charging) { + mCharging = charging; + if (!mForceIdle) { + mActiveReason = ACTIVE_REASON_CHARGING; + becomeActiveLocked("charging", Process.myUid()); + } + } + } + + @VisibleForTesting + boolean isQuickDozeEnabled() { + synchronized (this) { + return mQuickDozeActivated; + } + } + + /** Updates the quick doze flag and enters deep doze if appropriate. */ + @VisibleForTesting + void updateQuickDozeFlagLocked(boolean enabled) { + if (DEBUG) Slog.i(TAG, "updateQuickDozeFlagLocked: enabled=" + enabled); + mQuickDozeActivated = enabled; + mQuickDozeActivatedWhileIdling = + mQuickDozeActivated && (mState == STATE_IDLE || mState == STATE_IDLE_MAINTENANCE); + if (enabled) { + // If Quick Doze is enabled, see if we should go straight into it. + becomeInactiveIfAppropriateLocked(); + } + // Going from Deep Doze to Light Idle (if quick doze becomes disabled) is tricky and + // probably not worth the overhead, so leave in deep doze if that's the case until the + // next natural time to come out of it. + } + + + /** Returns true if the screen is locked. */ + @VisibleForTesting + boolean isKeyguardShowing() { + synchronized (this) { + return mScreenLocked; + } + } + + @VisibleForTesting + void keyguardShowingLocked(boolean showing) { + if (DEBUG) Slog.i(TAG, "keyguardShowing=" + showing); + if (mScreenLocked != showing) { + mScreenLocked = showing; + if (mScreenOn && !mForceIdle && !mScreenLocked) { + mActiveReason = ACTIVE_REASON_UNLOCKED; + becomeActiveLocked("unlocked", Process.myUid()); + } + } + } + + @VisibleForTesting + void scheduleReportActiveLocked(String activeReason, int activeUid) { + Message msg = mHandler.obtainMessage(MSG_REPORT_ACTIVE, activeUid, 0, activeReason); + mHandler.sendMessage(msg); + } + + void becomeActiveLocked(String activeReason, int activeUid) { + becomeActiveLocked(activeReason, activeUid, mConstants.INACTIVE_TIMEOUT, true); + } + + private void becomeActiveLocked(String activeReason, int activeUid, + long newInactiveTimeout, boolean changeLightIdle) { + if (DEBUG) { + Slog.i(TAG, "becomeActiveLocked, reason=" + activeReason + + ", changeLightIdle=" + changeLightIdle); + } + if (mState != STATE_ACTIVE || mLightState != STATE_ACTIVE) { + EventLogTags.writeDeviceIdle(STATE_ACTIVE, activeReason); + mState = STATE_ACTIVE; + mInactiveTimeout = newInactiveTimeout; + resetIdleManagementLocked(); + // Don't reset maintenance window start time if we're in a light idle maintenance window + // because its used in the light idle budget calculation. + if (mLightState != LIGHT_STATE_IDLE_MAINTENANCE) { + mMaintenanceStartTime = 0; + } + + if (changeLightIdle) { + EventLogTags.writeDeviceIdleLight(LIGHT_STATE_ACTIVE, activeReason); + mLightState = LIGHT_STATE_ACTIVE; + resetLightIdleManagementLocked(); + // Only report active if light is also ACTIVE. + scheduleReportActiveLocked(activeReason, activeUid); + addEvent(EVENT_NORMAL, activeReason); + } + } + } + + /** Must only be used in tests. */ + @VisibleForTesting + void setDeepEnabledForTest(boolean enabled) { + synchronized (this) { + mDeepEnabled = enabled; + } + } + + /** Must only be used in tests. */ + @VisibleForTesting + void setLightEnabledForTest(boolean enabled) { + synchronized (this) { + mLightEnabled = enabled; + } + } + + /** Sanity check to make sure DeviceIdleController and AlarmManager are on the same page. */ + private void verifyAlarmStateLocked() { + if (mState == STATE_ACTIVE && mNextAlarmTime != 0) { + Slog.wtf(TAG, "mState=ACTIVE but mNextAlarmTime=" + mNextAlarmTime); + } + if (mState != STATE_IDLE && mLocalAlarmManager.isIdling()) { + Slog.wtf(TAG, "mState=" + stateToString(mState) + " but AlarmManager is idling"); + } + if (mState == STATE_IDLE && !mLocalAlarmManager.isIdling()) { + Slog.wtf(TAG, "mState=IDLE but AlarmManager is not idling"); + } + if (mLightState == LIGHT_STATE_ACTIVE && mNextLightAlarmTime != 0) { + Slog.wtf(TAG, "mLightState=ACTIVE but mNextLightAlarmTime is " + + TimeUtils.formatDuration(mNextLightAlarmTime - SystemClock.elapsedRealtime()) + + " from now"); + } + } + + void becomeInactiveIfAppropriateLocked() { + verifyAlarmStateLocked(); + + final boolean isScreenBlockingInactive = + mScreenOn && (!mConstants.WAIT_FOR_UNLOCK || !mScreenLocked); + if (DEBUG) { + Slog.d(TAG, "becomeInactiveIfAppropriateLocked():" + + " isScreenBlockingInactive=" + isScreenBlockingInactive + + " (mScreenOn=" + mScreenOn + + ", WAIT_FOR_UNLOCK=" + mConstants.WAIT_FOR_UNLOCK + + ", mScreenLocked=" + mScreenLocked + ")" + + " mCharging=" + mCharging + + " mForceIdle=" + mForceIdle + ); + } + if (!mForceIdle && (mCharging || isScreenBlockingInactive)) { + return; + } + // Become inactive and determine if we will ultimately go idle. + if (mDeepEnabled) { + if (mQuickDozeActivated) { + if (mState == STATE_QUICK_DOZE_DELAY || mState == STATE_IDLE + || mState == STATE_IDLE_MAINTENANCE) { + // Already "idling". Don't want to restart the process. + // mLightState can't be LIGHT_STATE_ACTIVE if mState is any of these 3 + // values, so returning here is safe. + return; + } + if (DEBUG) { + Slog.d(TAG, "Moved from " + + stateToString(mState) + " to STATE_QUICK_DOZE_DELAY"); + } + mState = STATE_QUICK_DOZE_DELAY; + // Make sure any motion sensing or locating is stopped. + resetIdleManagementLocked(); + if (isUpcomingAlarmClock()) { + // If there's an upcoming AlarmClock alarm, we won't go into idle, so + // setting a wakeup alarm before the upcoming alarm is futile. Set the quick + // doze alarm to after the upcoming AlarmClock alarm. + scheduleAlarmLocked( + mAlarmManager.getNextWakeFromIdleTime() - mInjector.getElapsedRealtime() + + mConstants.QUICK_DOZE_DELAY_TIMEOUT, false); + } else { + // Wait a small amount of time in case something (eg: background service from + // recently closed app) needs to finish running. + scheduleAlarmLocked(mConstants.QUICK_DOZE_DELAY_TIMEOUT, false); + } + EventLogTags.writeDeviceIdle(mState, "no activity"); + } else if (mState == STATE_ACTIVE) { + mState = STATE_INACTIVE; + if (DEBUG) Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE"); + resetIdleManagementLocked(); + long delay = mInactiveTimeout; + if (shouldUseIdleTimeoutFactorLocked()) { + delay = (long) (mPreIdleFactor * delay); + } + if (isUpcomingAlarmClock()) { + // If there's an upcoming AlarmClock alarm, we won't go into idle, so + // setting a wakeup alarm before the upcoming alarm is futile. Set the idle + // alarm to after the upcoming AlarmClock alarm. + scheduleAlarmLocked( + mAlarmManager.getNextWakeFromIdleTime() - mInjector.getElapsedRealtime() + + delay, false); + } else { + scheduleAlarmLocked(delay, false); + } + EventLogTags.writeDeviceIdle(mState, "no activity"); + } + } + if (mLightState == LIGHT_STATE_ACTIVE && mLightEnabled) { + mLightState = LIGHT_STATE_INACTIVE; + if (DEBUG) Slog.d(TAG, "Moved from LIGHT_STATE_ACTIVE to LIGHT_STATE_INACTIVE"); + resetLightIdleManagementLocked(); + scheduleLightAlarmLocked(mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT); + EventLogTags.writeDeviceIdleLight(mLightState, "no activity"); + } + } + + private void resetIdleManagementLocked() { + mNextIdlePendingDelay = 0; + mNextIdleDelay = 0; + mIdleStartTime = 0; + mQuickDozeActivatedWhileIdling = false; + cancelAlarmLocked(); + cancelSensingTimeoutAlarmLocked(); + cancelLocatingLocked(); + maybeStopMonitoringMotionLocked(); + mAnyMotionDetector.stop(); + updateActiveConstraintsLocked(); + } + + private void resetLightIdleManagementLocked() { + mNextLightIdleDelay = 0; + mCurLightIdleBudget = 0; + cancelLightAlarmLocked(); + } + + void exitForceIdleLocked() { + if (mForceIdle) { + mForceIdle = false; + if (mScreenOn || mCharging) { + mActiveReason = ACTIVE_REASON_FORCED; + becomeActiveLocked("exit-force", Process.myUid()); + } + } + } + + /** + * Must only be used in tests. + * + * This sets the state value directly and thus doesn't trigger any behavioral changes. + */ + @VisibleForTesting + void setLightStateForTest(int lightState) { + synchronized (this) { + mLightState = lightState; + } + } + + @VisibleForTesting + int getLightState() { + return mLightState; + } + + void stepLightIdleStateLocked(String reason) { + if (mLightState == LIGHT_STATE_OVERRIDE) { + // If we are already in deep device idle mode, then + // there is nothing left to do for light mode. + return; + } + + if (DEBUG) Slog.d(TAG, "stepLightIdleStateLocked: mLightState=" + mLightState); + EventLogTags.writeDeviceIdleLightStep(); + + switch (mLightState) { + case LIGHT_STATE_INACTIVE: + mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET; + // Reset the upcoming idle delays. + mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT; + mMaintenanceStartTime = 0; + if (!isOpsInactiveLocked()) { + // We have some active ops going on... give them a chance to finish + // before going in to our first idle. + mLightState = LIGHT_STATE_PRE_IDLE; + EventLogTags.writeDeviceIdleLight(mLightState, reason); + scheduleLightAlarmLocked(mConstants.LIGHT_PRE_IDLE_TIMEOUT); + break; + } + // Nothing active, fall through to immediately idle. + case LIGHT_STATE_PRE_IDLE: + case LIGHT_STATE_IDLE_MAINTENANCE: + if (mMaintenanceStartTime != 0) { + long duration = SystemClock.elapsedRealtime() - mMaintenanceStartTime; + if (duration < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) { + // We didn't use up all of our minimum budget; add this to the reserve. + mCurLightIdleBudget += + (mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET - duration); + } else { + // We used more than our minimum budget; this comes out of the reserve. + mCurLightIdleBudget -= + (duration - mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET); + } + } + mMaintenanceStartTime = 0; + scheduleLightAlarmLocked(mNextLightIdleDelay); + mNextLightIdleDelay = Math.min(mConstants.LIGHT_MAX_IDLE_TIMEOUT, + (long)(mNextLightIdleDelay * mConstants.LIGHT_IDLE_FACTOR)); + if (mNextLightIdleDelay < mConstants.LIGHT_IDLE_TIMEOUT) { + mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT; + } + if (DEBUG) Slog.d(TAG, "Moved to LIGHT_STATE_IDLE."); + mLightState = LIGHT_STATE_IDLE; + EventLogTags.writeDeviceIdleLight(mLightState, reason); + addEvent(EVENT_LIGHT_IDLE, null); + mGoingIdleWakeLock.acquire(); + mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON_LIGHT); + break; + case LIGHT_STATE_IDLE: + case LIGHT_STATE_WAITING_FOR_NETWORK: + if (mNetworkConnected || mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) { + // We have been idling long enough, now it is time to do some work. + mActiveIdleOpCount = 1; + mActiveIdleWakeLock.acquire(); + mMaintenanceStartTime = SystemClock.elapsedRealtime(); + if (mCurLightIdleBudget < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) { + mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET; + } else if (mCurLightIdleBudget > mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET) { + mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET; + } + scheduleLightAlarmLocked(mCurLightIdleBudget); + if (DEBUG) Slog.d(TAG, + "Moved from LIGHT_STATE_IDLE to LIGHT_STATE_IDLE_MAINTENANCE."); + mLightState = LIGHT_STATE_IDLE_MAINTENANCE; + EventLogTags.writeDeviceIdleLight(mLightState, reason); + addEvent(EVENT_LIGHT_MAINTENANCE, null); + mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF); + } else { + // We'd like to do maintenance, but currently don't have network + // connectivity... let's try to wait until the network comes back. + // We'll only wait for another full idle period, however, and then give up. + scheduleLightAlarmLocked(mNextLightIdleDelay); + if (DEBUG) Slog.d(TAG, "Moved to LIGHT_WAITING_FOR_NETWORK."); + mLightState = LIGHT_STATE_WAITING_FOR_NETWORK; + EventLogTags.writeDeviceIdleLight(mLightState, reason); + } + break; + } + } + + @VisibleForTesting + int getState() { + return mState; + } + + /** + * Returns true if there's an upcoming AlarmClock alarm that is soon enough to prevent the + * device from going into idle. + */ + private boolean isUpcomingAlarmClock() { + return mInjector.getElapsedRealtime() + mConstants.MIN_TIME_TO_ALARM + >= mAlarmManager.getNextWakeFromIdleTime(); + } + + @VisibleForTesting + void stepIdleStateLocked(String reason) { + if (DEBUG) Slog.d(TAG, "stepIdleStateLocked: mState=" + mState); + EventLogTags.writeDeviceIdleStep(); + + if (isUpcomingAlarmClock()) { + // Whoops, there is an upcoming alarm. We don't actually want to go idle. + if (mState != STATE_ACTIVE) { + mActiveReason = ACTIVE_REASON_ALARM; + becomeActiveLocked("alarm", Process.myUid()); + becomeInactiveIfAppropriateLocked(); + } + return; + } + + if (mNumBlockingConstraints != 0 && !mForceIdle) { + // We have some constraints from other parts of the system server preventing + // us from moving to the next state. + if (DEBUG) { + Slog.i(TAG, "Cannot step idle state. Blocked by: " + mConstraints.values().stream() + .filter(x -> x.active) + .map(x -> x.name) + .collect(Collectors.joining(","))); + } + return; + } + + switch (mState) { + case STATE_INACTIVE: + // We have now been inactive long enough, it is time to start looking + // for motion and sleep some more while doing so. + startMonitoringMotionLocked(); + long delay = mConstants.IDLE_AFTER_INACTIVE_TIMEOUT; + if (shouldUseIdleTimeoutFactorLocked()) { + delay = (long) (mPreIdleFactor * delay); + } + scheduleAlarmLocked(delay, false); + moveToStateLocked(STATE_IDLE_PENDING, reason); + break; + case STATE_IDLE_PENDING: + moveToStateLocked(STATE_SENSING, reason); + cancelLocatingLocked(); + mLocated = false; + mLastGenericLocation = null; + mLastGpsLocation = null; + updateActiveConstraintsLocked(); + + // Wait for open constraints and an accelerometer reading before moving on. + if (mUseMotionSensor && mAnyMotionDetector.hasSensor()) { + scheduleSensingTimeoutAlarmLocked(mConstants.SENSING_TIMEOUT); + mNotMoving = false; + mAnyMotionDetector.checkForAnyMotion(); + break; + } else if (mNumBlockingConstraints != 0) { + cancelAlarmLocked(); + break; + } + + mNotMoving = true; + // Otherwise, fall through and check this off the list of requirements. + case STATE_SENSING: + cancelSensingTimeoutAlarmLocked(); + moveToStateLocked(STATE_LOCATING, reason); + scheduleAlarmLocked(mConstants.LOCATING_TIMEOUT, false); + LocationManager locationManager = mInjector.getLocationManager(); + if (locationManager != null + && locationManager.getProvider(LocationManager.NETWORK_PROVIDER) != null) { + locationManager.requestLocationUpdates(mLocationRequest, + mGenericLocationListener, mHandler.getLooper()); + mLocating = true; + } else { + mHasNetworkLocation = false; + } + if (locationManager != null + && locationManager.getProvider(LocationManager.GPS_PROVIDER) != null) { + mHasGps = true; + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 5, + mGpsLocationListener, mHandler.getLooper()); + mLocating = true; + } else { + mHasGps = false; + } + // If we have a location provider, we're all set, the listeners will move state + // forward. + if (mLocating) { + break; + } + + // Otherwise, we have to move from locating into idle maintenance. + case STATE_LOCATING: + cancelAlarmLocked(); + cancelLocatingLocked(); + mAnyMotionDetector.stop(); + + // Intentional fallthrough -- time to go into IDLE state. + case STATE_QUICK_DOZE_DELAY: + // Reset the upcoming idle delays. + mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT; + mNextIdleDelay = mConstants.IDLE_TIMEOUT; + + // Everything is in place to go into IDLE state. + case STATE_IDLE_MAINTENANCE: + scheduleAlarmLocked(mNextIdleDelay, true); + if (DEBUG) Slog.d(TAG, "Moved to STATE_IDLE. Next alarm in " + mNextIdleDelay + + " ms."); + mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR); + if (DEBUG) Slog.d(TAG, "Setting mNextIdleDelay = " + mNextIdleDelay); + mIdleStartTime = SystemClock.elapsedRealtime(); + mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT); + if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) { + mNextIdleDelay = mConstants.IDLE_TIMEOUT; + } + moveToStateLocked(STATE_IDLE, reason); + if (mLightState != LIGHT_STATE_OVERRIDE) { + mLightState = LIGHT_STATE_OVERRIDE; + cancelLightAlarmLocked(); + } + addEvent(EVENT_DEEP_IDLE, null); + mGoingIdleWakeLock.acquire(); + mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON); + break; + case STATE_IDLE: + // We have been idling long enough, now it is time to do some work. + mActiveIdleOpCount = 1; + mActiveIdleWakeLock.acquire(); + scheduleAlarmLocked(mNextIdlePendingDelay, false); + if (DEBUG) Slog.d(TAG, "Moved from STATE_IDLE to STATE_IDLE_MAINTENANCE. " + + "Next alarm in " + mNextIdlePendingDelay + " ms."); + mMaintenanceStartTime = SystemClock.elapsedRealtime(); + mNextIdlePendingDelay = Math.min(mConstants.MAX_IDLE_PENDING_TIMEOUT, + (long)(mNextIdlePendingDelay * mConstants.IDLE_PENDING_FACTOR)); + if (mNextIdlePendingDelay < mConstants.IDLE_PENDING_TIMEOUT) { + mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT; + } + moveToStateLocked(STATE_IDLE_MAINTENANCE, reason); + addEvent(EVENT_DEEP_MAINTENANCE, null); + mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF); + break; + } + } + + private void moveToStateLocked(int state, String reason) { + final int oldState = mState; + mState = state; + if (DEBUG) { + Slog.d(TAG, String.format("Moved from STATE_%s to STATE_%s.", + stateToString(oldState), stateToString(mState))); + } + EventLogTags.writeDeviceIdle(mState, reason); + updateActiveConstraintsLocked(); + } + + void incActiveIdleOps() { + synchronized (this) { + mActiveIdleOpCount++; + } + } + + void decActiveIdleOps() { + synchronized (this) { + mActiveIdleOpCount--; + if (mActiveIdleOpCount <= 0) { + exitMaintenanceEarlyIfNeededLocked(); + mActiveIdleWakeLock.release(); + } + } + } + + /** Must only be used in tests. */ + @VisibleForTesting + void setActiveIdleOpsForTest(int count) { + synchronized (this) { + mActiveIdleOpCount = count; + } + } + + void setJobsActive(boolean active) { + synchronized (this) { + mJobsActive = active; + if (!active) { + exitMaintenanceEarlyIfNeededLocked(); + } + } + } + + void setAlarmsActive(boolean active) { + synchronized (this) { + mAlarmsActive = active; + if (!active) { + exitMaintenanceEarlyIfNeededLocked(); + } + } + } + + @VisibleForTesting + int setPreIdleTimeoutMode(int mode) { + return setPreIdleTimeoutFactor(getPreIdleTimeoutByMode(mode)); + } + + @VisibleForTesting + float getPreIdleTimeoutByMode(int mode) { + switch (mode) { + case PowerManager.PRE_IDLE_TIMEOUT_MODE_LONG: { + return mConstants.PRE_IDLE_FACTOR_LONG; + } + case PowerManager.PRE_IDLE_TIMEOUT_MODE_SHORT: { + return mConstants.PRE_IDLE_FACTOR_SHORT; + } + case PowerManager.PRE_IDLE_TIMEOUT_MODE_NORMAL: { + return 1.0f; + } + default: { + Slog.w(TAG, "Invalid time out factor mode: " + mode); + return 1.0f; + } + } + } + + @VisibleForTesting + float getPreIdleTimeoutFactor() { + return mPreIdleFactor; + } + + @VisibleForTesting + int setPreIdleTimeoutFactor(float ratio) { + if (!mDeepEnabled) { + if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: Deep Idle disable"); + return SET_IDLE_FACTOR_RESULT_NOT_SUPPORT; + } else if (ratio <= MIN_PRE_IDLE_FACTOR_CHANGE) { + if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: Invalid input"); + return SET_IDLE_FACTOR_RESULT_INVALID; + } else if (Math.abs(ratio - mPreIdleFactor) < MIN_PRE_IDLE_FACTOR_CHANGE) { + if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: New factor same as previous factor"); + return SET_IDLE_FACTOR_RESULT_IGNORED; + } + synchronized (this) { + mLastPreIdleFactor = mPreIdleFactor; + mPreIdleFactor = ratio; + } + if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: " + ratio); + postUpdatePreIdleFactor(); + return SET_IDLE_FACTOR_RESULT_OK; + } + + @VisibleForTesting + void resetPreIdleTimeoutMode() { + synchronized (this) { + mLastPreIdleFactor = mPreIdleFactor; + mPreIdleFactor = 1.0f; + } + if (DEBUG) Slog.d(TAG, "resetPreIdleTimeoutMode to 1.0"); + postResetPreIdleTimeoutFactor(); + } + + private void postUpdatePreIdleFactor() { + mHandler.sendEmptyMessage(MSG_UPDATE_PRE_IDLE_TIMEOUT_FACTOR); + } + + private void postResetPreIdleTimeoutFactor() { + mHandler.sendEmptyMessage(MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR); + } + + private void updatePreIdleFactor() { + synchronized (this) { + if (!shouldUseIdleTimeoutFactorLocked()) { + return; + } + if (mState == STATE_INACTIVE || mState == STATE_IDLE_PENDING) { + if (mNextAlarmTime == 0) { + return; + } + long delay = mNextAlarmTime - SystemClock.elapsedRealtime(); + if (delay < MIN_STATE_STEP_ALARM_CHANGE) { + return; + } + long newDelay = (long) (delay / mLastPreIdleFactor * mPreIdleFactor); + if (Math.abs(delay - newDelay) < MIN_STATE_STEP_ALARM_CHANGE) { + return; + } + scheduleAlarmLocked(newDelay, false); + } + } + } + + private void maybeDoImmediateMaintenance() { + synchronized (this) { + if (mState == STATE_IDLE) { + long duration = SystemClock.elapsedRealtime() - mIdleStartTime; + /* Let's trgger a immediate maintenance, + * if it has been idle for a long time */ + if (duration > mConstants.IDLE_TIMEOUT) { + scheduleAlarmLocked(0, false); + } + } + } + } + + private boolean shouldUseIdleTimeoutFactorLocked() { + // exclude ACTIVE_REASON_MOTION, for exclude device in pocket case + if (mActiveReason == ACTIVE_REASON_MOTION) { + return false; + } + return true; + } + + /** Must only be used in tests. */ + @VisibleForTesting + void setIdleStartTimeForTest(long idleStartTime) { + synchronized (this) { + mIdleStartTime = idleStartTime; + maybeDoImmediateMaintenance(); + } + } + + @VisibleForTesting + long getNextAlarmTime() { + return mNextAlarmTime; + } + + boolean isOpsInactiveLocked() { + return mActiveIdleOpCount <= 0 && !mJobsActive && !mAlarmsActive; + } + + void exitMaintenanceEarlyIfNeededLocked() { + if (mState == STATE_IDLE_MAINTENANCE || mLightState == LIGHT_STATE_IDLE_MAINTENANCE + || mLightState == LIGHT_STATE_PRE_IDLE) { + if (isOpsInactiveLocked()) { + final long now = SystemClock.elapsedRealtime(); + if (DEBUG) { + StringBuilder sb = new StringBuilder(); + sb.append("Exit: start="); + TimeUtils.formatDuration(mMaintenanceStartTime, sb); + sb.append(" now="); + TimeUtils.formatDuration(now, sb); + Slog.d(TAG, sb.toString()); + } + if (mState == STATE_IDLE_MAINTENANCE) { + stepIdleStateLocked("s:early"); + } else if (mLightState == LIGHT_STATE_PRE_IDLE) { + stepLightIdleStateLocked("s:predone"); + } else { + stepLightIdleStateLocked("s:early"); + } + } + } + } + + void motionLocked() { + if (DEBUG) Slog.d(TAG, "motionLocked()"); + mLastMotionEventElapsed = mInjector.getElapsedRealtime(); + handleMotionDetectedLocked(mConstants.MOTION_INACTIVE_TIMEOUT, "motion"); + } + + void handleMotionDetectedLocked(long timeout, String type) { + if (mStationaryListeners.size() > 0) { + postStationaryStatusUpdated(); + scheduleMotionTimeoutAlarmLocked(); + // We need to re-register the motion listener, but we don't want the sensors to be + // constantly active or to churn the CPU by registering too early, register after some + // delay. + scheduleMotionRegistrationAlarmLocked(); + } + if (mQuickDozeActivated && !mQuickDozeActivatedWhileIdling) { + // Don't exit idle due to motion if quick doze is enabled. + // However, if the device started idling due to the normal progression (going through + // all the states) and then had quick doze activated, come out briefly on motion so the + // user can get slightly fresher content. + return; + } + maybeStopMonitoringMotionLocked(); + // The device is not yet active, so we want to go back to the pending idle + // state to wait again for no motion. Note that we only monitor for motion + // after moving out of the inactive state, so no need to worry about that. + final boolean becomeInactive = mState != STATE_ACTIVE + || mLightState == LIGHT_STATE_OVERRIDE; + // We only want to change the IDLE state if it's OVERRIDE. + becomeActiveLocked(type, Process.myUid(), timeout, mLightState == LIGHT_STATE_OVERRIDE); + if (becomeInactive) { + becomeInactiveIfAppropriateLocked(); + } + } + + void receivedGenericLocationLocked(Location location) { + if (mState != STATE_LOCATING) { + cancelLocatingLocked(); + return; + } + if (DEBUG) Slog.d(TAG, "Generic location: " + location); + mLastGenericLocation = new Location(location); + if (location.getAccuracy() > mConstants.LOCATION_ACCURACY && mHasGps) { + return; + } + mLocated = true; + if (mNotMoving) { + stepIdleStateLocked("s:location"); + } + } + + void receivedGpsLocationLocked(Location location) { + if (mState != STATE_LOCATING) { + cancelLocatingLocked(); + return; + } + if (DEBUG) Slog.d(TAG, "GPS location: " + location); + mLastGpsLocation = new Location(location); + if (location.getAccuracy() > mConstants.LOCATION_ACCURACY) { + return; + } + mLocated = true; + if (mNotMoving) { + stepIdleStateLocked("s:gps"); + } + } + + void startMonitoringMotionLocked() { + if (DEBUG) Slog.d(TAG, "startMonitoringMotionLocked()"); + if (mMotionSensor != null && !mMotionListener.active) { + mMotionListener.registerLocked(); + } + } + + /** + * Stops motion monitoring. Will not stop monitoring if there are registered stationary + * listeners. + */ + private void maybeStopMonitoringMotionLocked() { + if (DEBUG) Slog.d(TAG, "maybeStopMonitoringMotionLocked()"); + if (mMotionSensor != null && mStationaryListeners.size() == 0) { + if (mMotionListener.active) { + mMotionListener.unregisterLocked(); + cancelMotionTimeoutAlarmLocked(); + } + cancelMotionRegistrationAlarmLocked(); + } + } + + void cancelAlarmLocked() { + if (mNextAlarmTime != 0) { + mNextAlarmTime = 0; + mAlarmManager.cancel(mDeepAlarmListener); + } + } + + void cancelLightAlarmLocked() { + if (mNextLightAlarmTime != 0) { + mNextLightAlarmTime = 0; + mAlarmManager.cancel(mLightAlarmListener); + } + } + + void cancelLocatingLocked() { + if (mLocating) { + LocationManager locationManager = mInjector.getLocationManager(); + locationManager.removeUpdates(mGenericLocationListener); + locationManager.removeUpdates(mGpsLocationListener); + mLocating = false; + } + } + + private void cancelMotionTimeoutAlarmLocked() { + mAlarmManager.cancel(mMotionTimeoutAlarmListener); + } + + private void cancelMotionRegistrationAlarmLocked() { + mAlarmManager.cancel(mMotionRegistrationAlarmListener); + } + + void cancelSensingTimeoutAlarmLocked() { + if (mNextSensingTimeoutAlarmTime != 0) { + mNextSensingTimeoutAlarmTime = 0; + mAlarmManager.cancel(mSensingTimeoutAlarmListener); + } + } + + void scheduleAlarmLocked(long delay, boolean idleUntil) { + if (DEBUG) Slog.d(TAG, "scheduleAlarmLocked(" + delay + ", " + idleUntil + ")"); + + if (mUseMotionSensor && mMotionSensor == null + && mState != STATE_QUICK_DOZE_DELAY + && mState != STATE_IDLE + && mState != STATE_IDLE_MAINTENANCE) { + // If there is no motion sensor on this device, but we need one, then we won't schedule + // alarms, because we can't determine if the device is not moving. This effectively + // turns off normal execution of device idling, although it is still possible to + // manually poke it by pretending like the alarm is going off. + // STATE_QUICK_DOZE_DELAY skips the motion sensing so if the state is past the motion + // sensing stage (ie, is QUICK_DOZE_DELAY, IDLE, or IDLE_MAINTENANCE), then idling + // can continue until the user interacts with the device. + return; + } + mNextAlarmTime = SystemClock.elapsedRealtime() + delay; + if (idleUntil) { + mAlarmManager.setIdleUntil(AlarmManager.ELAPSED_REALTIME_WAKEUP, + mNextAlarmTime, "DeviceIdleController.deep", mDeepAlarmListener, mHandler); + } else { + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + mNextAlarmTime, "DeviceIdleController.deep", mDeepAlarmListener, mHandler); + } + } + + void scheduleLightAlarmLocked(long delay) { + if (DEBUG) Slog.d(TAG, "scheduleLightAlarmLocked(" + delay + ")"); + mNextLightAlarmTime = SystemClock.elapsedRealtime() + delay; + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + mNextLightAlarmTime, "DeviceIdleController.light", mLightAlarmListener, mHandler); + } + + private void scheduleMotionRegistrationAlarmLocked() { + if (DEBUG) Slog.d(TAG, "scheduleMotionRegistrationAlarmLocked"); + long nextMotionRegistrationAlarmTime = + mInjector.getElapsedRealtime() + mConstants.MOTION_INACTIVE_TIMEOUT / 2; + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextMotionRegistrationAlarmTime, + "DeviceIdleController.motion_registration", mMotionRegistrationAlarmListener, + mHandler); + } + + private void scheduleMotionTimeoutAlarmLocked() { + if (DEBUG) Slog.d(TAG, "scheduleMotionAlarmLocked"); + long nextMotionTimeoutAlarmTime = + mInjector.getElapsedRealtime() + mConstants.MOTION_INACTIVE_TIMEOUT; + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextMotionTimeoutAlarmTime, + "DeviceIdleController.motion", mMotionTimeoutAlarmListener, mHandler); + } + + void scheduleSensingTimeoutAlarmLocked(long delay) { + if (DEBUG) Slog.d(TAG, "scheduleSensingAlarmLocked(" + delay + ")"); + mNextSensingTimeoutAlarmTime = SystemClock.elapsedRealtime() + delay; + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, mNextSensingTimeoutAlarmTime, + "DeviceIdleController.sensing", mSensingTimeoutAlarmListener, mHandler); + } + + private static int[] buildAppIdArray(ArrayMap<String, Integer> systemApps, + ArrayMap<String, Integer> userApps, SparseBooleanArray outAppIds) { + outAppIds.clear(); + if (systemApps != null) { + for (int i = 0; i < systemApps.size(); i++) { + outAppIds.put(systemApps.valueAt(i), true); + } + } + if (userApps != null) { + for (int i = 0; i < userApps.size(); i++) { + outAppIds.put(userApps.valueAt(i), true); + } + } + int size = outAppIds.size(); + int[] appids = new int[size]; + for (int i = 0; i < size; i++) { + appids[i] = outAppIds.keyAt(i); + } + return appids; + } + + private void updateWhitelistAppIdsLocked() { + mPowerSaveWhitelistExceptIdleAppIdArray = buildAppIdArray(mPowerSaveWhitelistAppsExceptIdle, + mPowerSaveWhitelistUserApps, mPowerSaveWhitelistExceptIdleAppIds); + mPowerSaveWhitelistAllAppIdArray = buildAppIdArray(mPowerSaveWhitelistApps, + mPowerSaveWhitelistUserApps, mPowerSaveWhitelistAllAppIds); + mPowerSaveWhitelistUserAppIdArray = buildAppIdArray(null, + mPowerSaveWhitelistUserApps, mPowerSaveWhitelistUserAppIds); + if (mLocalActivityManager != null) { + mLocalActivityManager.setDeviceIdleWhitelist( + mPowerSaveWhitelistAllAppIdArray, mPowerSaveWhitelistExceptIdleAppIdArray); + } + if (mLocalPowerManager != null) { + if (DEBUG) { + Slog.d(TAG, "Setting wakelock whitelist to " + + Arrays.toString(mPowerSaveWhitelistAllAppIdArray)); + } + mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray); + } + passWhiteListsToForceAppStandbyTrackerLocked(); + } + + private void updateTempWhitelistAppIdsLocked(int appId, boolean adding) { + final int size = mTempWhitelistAppIdEndTimes.size(); + if (mTempWhitelistAppIdArray.length != size) { + mTempWhitelistAppIdArray = new int[size]; + } + for (int i = 0; i < size; i++) { + mTempWhitelistAppIdArray[i] = mTempWhitelistAppIdEndTimes.keyAt(i); + } + if (mLocalActivityManager != null) { + if (DEBUG) { + Slog.d(TAG, "Setting activity manager temp whitelist to " + + Arrays.toString(mTempWhitelistAppIdArray)); + } + mLocalActivityManager.updateDeviceIdleTempWhitelist(mTempWhitelistAppIdArray, appId, + adding); + } + if (mLocalPowerManager != null) { + if (DEBUG) { + Slog.d(TAG, "Setting wakelock temp whitelist to " + + Arrays.toString(mTempWhitelistAppIdArray)); + } + mLocalPowerManager.setDeviceIdleTempWhitelist(mTempWhitelistAppIdArray); + } + passWhiteListsToForceAppStandbyTrackerLocked(); + } + + private void reportPowerSaveWhitelistChangedLocked() { + Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM); + } + + private void reportTempWhitelistChangedLocked() { + Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM); + } + + private void passWhiteListsToForceAppStandbyTrackerLocked() { + mAppStateTracker.setPowerSaveWhitelistAppIds( + mPowerSaveWhitelistExceptIdleAppIdArray, + mPowerSaveWhitelistUserAppIdArray, + mTempWhitelistAppIdArray); + } + + void readConfigFileLocked() { + if (DEBUG) Slog.d(TAG, "Reading config from " + mConfigFile.getBaseFile()); + mPowerSaveWhitelistUserApps.clear(); + FileInputStream stream; + try { + stream = mConfigFile.openRead(); + } catch (FileNotFoundException e) { + return; + } + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(stream, StandardCharsets.UTF_8.name()); + readConfigFileLocked(parser); + } catch (XmlPullParserException e) { + } finally { + try { + stream.close(); + } catch (IOException e) { + } + } + } + + private void readConfigFileLocked(XmlPullParser parser) { + final PackageManager pm = getContext().getPackageManager(); + + try { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + ; + } + + if (type != XmlPullParser.START_TAG) { + throw new IllegalStateException("no start tag found"); + } + + int outerDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + switch (tagName) { + case "wl": + String name = parser.getAttributeValue(null, "n"); + if (name != null) { + try { + ApplicationInfo ai = pm.getApplicationInfo(name, + PackageManager.MATCH_ANY_USER); + mPowerSaveWhitelistUserApps.put(ai.packageName, + UserHandle.getAppId(ai.uid)); + } catch (PackageManager.NameNotFoundException e) { + } + } + break; + case "un-wl": + final String packageName = parser.getAttributeValue(null, "n"); + if (mPowerSaveWhitelistApps.containsKey(packageName)) { + mRemovedFromSystemWhitelistApps.put(packageName, + mPowerSaveWhitelistApps.remove(packageName)); + } + break; + default: + Slog.w(TAG, "Unknown element under <config>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + break; + } + } + + } catch (IllegalStateException e) { + Slog.w(TAG, "Failed parsing config " + e); + } catch (NullPointerException e) { + Slog.w(TAG, "Failed parsing config " + e); + } catch (NumberFormatException e) { + Slog.w(TAG, "Failed parsing config " + e); + } catch (XmlPullParserException e) { + Slog.w(TAG, "Failed parsing config " + e); + } catch (IOException e) { + Slog.w(TAG, "Failed parsing config " + e); + } catch (IndexOutOfBoundsException e) { + Slog.w(TAG, "Failed parsing config " + e); + } + } + + void writeConfigFileLocked() { + mHandler.removeMessages(MSG_WRITE_CONFIG); + mHandler.sendEmptyMessageDelayed(MSG_WRITE_CONFIG, 5000); + } + + void handleWriteConfigFile() { + final ByteArrayOutputStream memStream = new ByteArrayOutputStream(); + + try { + synchronized (this) { + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(memStream, StandardCharsets.UTF_8.name()); + writeConfigFileLocked(out); + } + } catch (IOException e) { + } + + synchronized (mConfigFile) { + FileOutputStream stream = null; + try { + stream = mConfigFile.startWrite(); + memStream.writeTo(stream); + mConfigFile.finishWrite(stream); + } catch (IOException e) { + Slog.w(TAG, "Error writing config file", e); + mConfigFile.failWrite(stream); + } + } + } + + void writeConfigFileLocked(XmlSerializer out) throws IOException { + out.startDocument(null, true); + out.startTag(null, "config"); + for (int i=0; i<mPowerSaveWhitelistUserApps.size(); i++) { + String name = mPowerSaveWhitelistUserApps.keyAt(i); + out.startTag(null, "wl"); + out.attribute(null, "n", name); + out.endTag(null, "wl"); + } + for (int i = 0; i < mRemovedFromSystemWhitelistApps.size(); i++) { + out.startTag(null, "un-wl"); + out.attribute(null, "n", mRemovedFromSystemWhitelistApps.keyAt(i)); + out.endTag(null, "un-wl"); + } + out.endTag(null, "config"); + out.endDocument(); + } + + static void dumpHelp(PrintWriter pw) { + pw.println("Device idle controller (deviceidle) commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(" step [light|deep]"); + pw.println(" Immediately step to next state, without waiting for alarm."); + pw.println(" force-idle [light|deep]"); + pw.println(" Force directly into idle mode, regardless of other device state."); + pw.println(" force-inactive"); + pw.println(" Force to be inactive, ready to freely step idle states."); + pw.println(" unforce"); + pw.println(" Resume normal functioning after force-idle or force-inactive."); + pw.println(" get [light|deep|force|screen|charging|network]"); + pw.println(" Retrieve the current given state."); + pw.println(" disable [light|deep|all]"); + pw.println(" Completely disable device idle mode."); + pw.println(" enable [light|deep|all]"); + pw.println(" Re-enable device idle mode after it had previously been disabled."); + pw.println(" enabled [light|deep|all]"); + pw.println(" Print 1 if device idle mode is currently enabled, else 0."); + pw.println(" whitelist"); + pw.println(" Print currently whitelisted apps."); + pw.println(" whitelist [package ...]"); + pw.println(" Add (prefix with +) or remove (prefix with -) packages."); + pw.println(" sys-whitelist [package ...|reset]"); + pw.println(" Prefix the package with '-' to remove it from the system whitelist or '+'" + + " to put it back in the system whitelist."); + pw.println(" Note that only packages that were" + + " earlier removed from the system whitelist can be added back."); + pw.println(" reset will reset the whitelist to the original state"); + pw.println(" Prints the system whitelist if no arguments are specified"); + pw.println(" except-idle-whitelist [package ...|reset]"); + pw.println(" Prefix the package with '+' to add it to whitelist or " + + "'=' to check if it is already whitelisted"); + pw.println(" [reset] will reset the whitelist to it's original state"); + pw.println(" Note that unlike <whitelist> cmd, " + + "changes made using this won't be persisted across boots"); + pw.println(" tempwhitelist"); + pw.println(" Print packages that are temporarily whitelisted."); + pw.println(" tempwhitelist [-u USER] [-d DURATION] [-r] [package]"); + pw.println(" Temporarily place package in whitelist for DURATION milliseconds."); + pw.println(" If no DURATION is specified, 10 seconds is used"); + pw.println(" If [-r] option is used, then the package is removed from temp whitelist " + + "and any [-d] is ignored"); + pw.println(" motion"); + pw.println(" Simulate a motion event to bring the device out of deep doze"); + pw.println(" pre-idle-factor [0|1|2]"); + pw.println(" Set a new factor to idle time before step to idle" + + "(inactive_to and idle_after_inactive_to)"); + pw.println(" reset-pre-idle-factor"); + pw.println(" Reset factor to idle time to default"); + } + + class Shell extends ShellCommand { + int userId = UserHandle.USER_SYSTEM; + + @Override + public int onCommand(String cmd) { + return onShellCommand(this, cmd); + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + dumpHelp(pw); + } + } + + int onShellCommand(Shell shell, String cmd) { + PrintWriter pw = shell.getOutPrintWriter(); + if ("step".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + String arg = shell.getNextArg(); + try { + if (arg == null || "deep".equals(arg)) { + stepIdleStateLocked("s:shell"); + pw.print("Stepped to deep: "); + pw.println(stateToString(mState)); + } else if ("light".equals(arg)) { + stepLightIdleStateLocked("s:shell"); + pw.print("Stepped to light: "); pw.println(lightStateToString(mLightState)); + } else { + pw.println("Unknown idle mode: " + arg); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("force-idle".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + String arg = shell.getNextArg(); + try { + if (arg == null || "deep".equals(arg)) { + if (!mDeepEnabled) { + pw.println("Unable to go deep idle; not enabled"); + return -1; + } + mForceIdle = true; + becomeInactiveIfAppropriateLocked(); + int curState = mState; + while (curState != STATE_IDLE) { + stepIdleStateLocked("s:shell"); + if (curState == mState) { + pw.print("Unable to go deep idle; stopped at "); + pw.println(stateToString(mState)); + exitForceIdleLocked(); + return -1; + } + curState = mState; + } + pw.println("Now forced in to deep idle mode"); + } else if ("light".equals(arg)) { + mForceIdle = true; + becomeInactiveIfAppropriateLocked(); + int curLightState = mLightState; + while (curLightState != LIGHT_STATE_IDLE) { + stepLightIdleStateLocked("s:shell"); + if (curLightState == mLightState) { + pw.print("Unable to go light idle; stopped at "); + pw.println(lightStateToString(mLightState)); + exitForceIdleLocked(); + return -1; + } + curLightState = mLightState; + } + pw.println("Now forced in to light idle mode"); + } else { + pw.println("Unknown idle mode: " + arg); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("force-inactive".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + mForceIdle = true; + becomeInactiveIfAppropriateLocked(); + pw.print("Light state: "); + pw.print(lightStateToString(mLightState)); + pw.print(", deep state: "); + pw.println(stateToString(mState)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("unforce".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + exitForceIdleLocked(); + pw.print("Light state: "); + pw.print(lightStateToString(mLightState)); + pw.print(", deep state: "); + pw.println(stateToString(mState)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("get".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + String arg = shell.getNextArg(); + if (arg != null) { + long token = Binder.clearCallingIdentity(); + try { + switch (arg) { + case "light": pw.println(lightStateToString(mLightState)); break; + case "deep": pw.println(stateToString(mState)); break; + case "force": pw.println(mForceIdle); break; + case "quick": pw.println(mQuickDozeActivated); break; + case "screen": pw.println(mScreenOn); break; + case "charging": pw.println(mCharging); break; + case "network": pw.println(mNetworkConnected); break; + default: pw.println("Unknown get option: " + arg); break; + } + } finally { + Binder.restoreCallingIdentity(token); + } + } else { + pw.println("Argument required"); + } + } + } else if ("disable".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + String arg = shell.getNextArg(); + try { + boolean becomeActive = false; + boolean valid = false; + if (arg == null || "deep".equals(arg) || "all".equals(arg)) { + valid = true; + if (mDeepEnabled) { + mDeepEnabled = false; + becomeActive = true; + pw.println("Deep idle mode disabled"); + } + } + if (arg == null || "light".equals(arg) || "all".equals(arg)) { + valid = true; + if (mLightEnabled) { + mLightEnabled = false; + becomeActive = true; + pw.println("Light idle mode disabled"); + } + } + if (becomeActive) { + mActiveReason = ACTIVE_REASON_FORCED; + becomeActiveLocked((arg == null ? "all" : arg) + "-disabled", + Process.myUid()); + } + if (!valid) { + pw.println("Unknown idle mode: " + arg); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("enable".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + String arg = shell.getNextArg(); + try { + boolean becomeInactive = false; + boolean valid = false; + if (arg == null || "deep".equals(arg) || "all".equals(arg)) { + valid = true; + if (!mDeepEnabled) { + mDeepEnabled = true; + becomeInactive = true; + pw.println("Deep idle mode enabled"); + } + } + if (arg == null || "light".equals(arg) || "all".equals(arg)) { + valid = true; + if (!mLightEnabled) { + mLightEnabled = true; + becomeInactive = true; + pw.println("Light idle mode enable"); + } + } + if (becomeInactive) { + becomeInactiveIfAppropriateLocked(); + } + if (!valid) { + pw.println("Unknown idle mode: " + arg); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("enabled".equals(cmd)) { + synchronized (this) { + String arg = shell.getNextArg(); + if (arg == null || "all".equals(arg)) { + pw.println(mDeepEnabled && mLightEnabled ? "1" : 0); + } else if ("deep".equals(arg)) { + pw.println(mDeepEnabled ? "1" : 0); + } else if ("light".equals(arg)) { + pw.println(mLightEnabled ? "1" : 0); + } else { + pw.println("Unknown idle mode: " + arg); + } + } + } else if ("whitelist".equals(cmd)) { + String arg = shell.getNextArg(); + if (arg != null) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.DEVICE_POWER, null); + long token = Binder.clearCallingIdentity(); + try { + do { + if (arg.length() < 1 || (arg.charAt(0) != '-' + && arg.charAt(0) != '+' && arg.charAt(0) != '=')) { + pw.println("Package must be prefixed with +, -, or =: " + arg); + return -1; + } + char op = arg.charAt(0); + String pkg = arg.substring(1); + if (op == '+') { + if (addPowerSaveWhitelistAppsInternal(Collections.singletonList(pkg)) + == 1) { + pw.println("Added: " + pkg); + } else { + pw.println("Unknown package: " + pkg); + } + } else if (op == '-') { + if (removePowerSaveWhitelistAppInternal(pkg)) { + pw.println("Removed: " + pkg); + } + } else { + pw.println(getPowerSaveWhitelistAppInternal(pkg)); + } + } while ((arg=shell.getNextArg()) != null); + } finally { + Binder.restoreCallingIdentity(token); + } + } else { + synchronized (this) { + for (int j=0; j<mPowerSaveWhitelistAppsExceptIdle.size(); j++) { + pw.print("system-excidle,"); + pw.print(mPowerSaveWhitelistAppsExceptIdle.keyAt(j)); + pw.print(","); + pw.println(mPowerSaveWhitelistAppsExceptIdle.valueAt(j)); + } + for (int j=0; j<mPowerSaveWhitelistApps.size(); j++) { + pw.print("system,"); + pw.print(mPowerSaveWhitelistApps.keyAt(j)); + pw.print(","); + pw.println(mPowerSaveWhitelistApps.valueAt(j)); + } + for (int j=0; j<mPowerSaveWhitelistUserApps.size(); j++) { + pw.print("user,"); + pw.print(mPowerSaveWhitelistUserApps.keyAt(j)); + pw.print(","); + pw.println(mPowerSaveWhitelistUserApps.valueAt(j)); + } + } + } + } else if ("tempwhitelist".equals(cmd)) { + long duration = 10000; + boolean removePkg = false; + String opt; + while ((opt=shell.getNextOption()) != null) { + if ("-u".equals(opt)) { + opt = shell.getNextArg(); + if (opt == null) { + pw.println("-u requires a user number"); + return -1; + } + shell.userId = Integer.parseInt(opt); + } else if ("-d".equals(opt)) { + opt = shell.getNextArg(); + if (opt == null) { + pw.println("-d requires a duration"); + return -1; + } + duration = Long.parseLong(opt); + } else if ("-r".equals(opt)) { + removePkg = true; + } + } + String arg = shell.getNextArg(); + if (arg != null) { + try { + if (removePkg) { + removePowerSaveTempWhitelistAppChecked(arg, shell.userId); + } else { + addPowerSaveTempWhitelistAppChecked(arg, duration, shell.userId, "shell"); + } + } catch (Exception e) { + pw.println("Failed: " + e); + return -1; + } + } else if (removePkg) { + pw.println("[-r] requires a package name"); + return -1; + } else { + dumpTempWhitelistSchedule(pw, false); + } + } else if ("except-idle-whitelist".equals(cmd)) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.DEVICE_POWER, null); + final long token = Binder.clearCallingIdentity(); + try { + String arg = shell.getNextArg(); + if (arg == null) { + pw.println("No arguments given"); + return -1; + } else if ("reset".equals(arg)) { + resetPowerSaveWhitelistExceptIdleInternal(); + } else { + do { + if (arg.length() < 1 || (arg.charAt(0) != '-' + && arg.charAt(0) != '+' && arg.charAt(0) != '=')) { + pw.println("Package must be prefixed with +, -, or =: " + arg); + return -1; + } + char op = arg.charAt(0); + String pkg = arg.substring(1); + if (op == '+') { + if (addPowerSaveWhitelistExceptIdleInternal(pkg)) { + pw.println("Added: " + pkg); + } else { + pw.println("Unknown package: " + pkg); + } + } else if (op == '=') { + pw.println(getPowerSaveWhitelistExceptIdleInternal(pkg)); + } else { + pw.println("Unknown argument: " + arg); + return -1; + } + } while ((arg = shell.getNextArg()) != null); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } else if ("sys-whitelist".equals(cmd)) { + String arg = shell.getNextArg(); + if (arg != null) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.DEVICE_POWER, null); + final long token = Binder.clearCallingIdentity(); + try { + if ("reset".equals(arg)) { + resetSystemPowerWhitelistInternal(); + } else { + do { + if (arg.length() < 1 + || (arg.charAt(0) != '-' && arg.charAt(0) != '+')) { + pw.println("Package must be prefixed with + or - " + arg); + return -1; + } + final char op = arg.charAt(0); + final String pkg = arg.substring(1); + switch (op) { + case '+': + if (restoreSystemPowerWhitelistAppInternal(pkg)) { + pw.println("Restored " + pkg); + } + break; + case '-': + if (removeSystemPowerWhitelistAppInternal(pkg)) { + pw.println("Removed " + pkg); + } + break; + } + } while ((arg = shell.getNextArg()) != null); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } else { + synchronized (this) { + for (int j = 0; j < mPowerSaveWhitelistApps.size(); j++) { + pw.print(mPowerSaveWhitelistApps.keyAt(j)); + pw.print(","); + pw.println(mPowerSaveWhitelistApps.valueAt(j)); + } + } + } + } else if ("motion".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + motionLocked(); + pw.print("Light state: "); + pw.print(lightStateToString(mLightState)); + pw.print(", deep state: "); + pw.println(stateToString(mState)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("pre-idle-factor".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + int ret = SET_IDLE_FACTOR_RESULT_UNINIT; + try { + String arg = shell.getNextArg(); + boolean valid = false; + int mode = 0; + if (arg != null) { + mode = Integer.parseInt(arg); + ret = setPreIdleTimeoutMode(mode); + if (ret == SET_IDLE_FACTOR_RESULT_OK) { + pw.println("pre-idle-factor: " + mode); + valid = true; + } else if (ret == SET_IDLE_FACTOR_RESULT_NOT_SUPPORT) { + valid = true; + pw.println("Deep idle not supported"); + } else if (ret == SET_IDLE_FACTOR_RESULT_IGNORED) { + valid = true; + pw.println("Idle timeout factor not changed"); + } + } + if (!valid) { + pw.println("Unknown idle timeout factor: " + arg + + ",(error code: " + ret + ")"); + } + } catch (NumberFormatException e) { + pw.println("Unknown idle timeout factor" + + ",(error code: " + ret + ")"); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("reset-pre-idle-factor".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + resetPreIdleTimeoutMode(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else { + return shell.handleDefaultCommands(cmd); + } + return 0; + } + + void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return; + + if (args != null) { + int userId = UserHandle.USER_SYSTEM; + for (int i=0; i<args.length; i++) { + String arg = args[i]; + if ("-h".equals(arg)) { + dumpHelp(pw); + return; + } else if ("-u".equals(arg)) { + i++; + if (i < args.length) { + arg = args[i]; + userId = Integer.parseInt(arg); + } + } else if ("-a".equals(arg)) { + // Ignore, we always dump all. + } else if (arg.length() > 0 && arg.charAt(0) == '-'){ + pw.println("Unknown option: " + arg); + return; + } else { + Shell shell = new Shell(); + shell.userId = userId; + String[] newArgs = new String[args.length-i]; + System.arraycopy(args, i, newArgs, 0, args.length-i); + shell.exec(mBinderService, null, fd, null, newArgs, null, + new ResultReceiver(null)); + return; + } + } + } + + synchronized (this) { + mConstants.dump(pw); + + if (mEventCmds[0] != EVENT_NULL) { + pw.println(" Idling history:"); + long now = SystemClock.elapsedRealtime(); + for (int i=EVENT_BUFFER_SIZE-1; i>=0; i--) { + int cmd = mEventCmds[i]; + if (cmd == EVENT_NULL) { + continue; + } + String label; + switch (mEventCmds[i]) { + case EVENT_NORMAL: label = " normal"; break; + case EVENT_LIGHT_IDLE: label = " light-idle"; break; + case EVENT_LIGHT_MAINTENANCE: label = "light-maint"; break; + case EVENT_DEEP_IDLE: label = " deep-idle"; break; + case EVENT_DEEP_MAINTENANCE: label = " deep-maint"; break; + default: label = " ??"; break; + } + pw.print(" "); + pw.print(label); + pw.print(": "); + TimeUtils.formatDuration(mEventTimes[i], now, pw); + if (mEventReasons[i] != null) { + pw.print(" ("); + pw.print(mEventReasons[i]); + pw.print(")"); + } + pw.println(); + + } + } + + int size = mPowerSaveWhitelistAppsExceptIdle.size(); + if (size > 0) { + pw.println(" Whitelist (except idle) system apps:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.println(mPowerSaveWhitelistAppsExceptIdle.keyAt(i)); + } + } + size = mPowerSaveWhitelistApps.size(); + if (size > 0) { + pw.println(" Whitelist system apps:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.println(mPowerSaveWhitelistApps.keyAt(i)); + } + } + size = mRemovedFromSystemWhitelistApps.size(); + if (size > 0) { + pw.println(" Removed from whitelist system apps:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.println(mRemovedFromSystemWhitelistApps.keyAt(i)); + } + } + size = mPowerSaveWhitelistUserApps.size(); + if (size > 0) { + pw.println(" Whitelist user apps:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.println(mPowerSaveWhitelistUserApps.keyAt(i)); + } + } + size = mPowerSaveWhitelistExceptIdleAppIds.size(); + if (size > 0) { + pw.println(" Whitelist (except idle) all app ids:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.print(mPowerSaveWhitelistExceptIdleAppIds.keyAt(i)); + pw.println(); + } + } + size = mPowerSaveWhitelistUserAppIds.size(); + if (size > 0) { + pw.println(" Whitelist user app ids:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.print(mPowerSaveWhitelistUserAppIds.keyAt(i)); + pw.println(); + } + } + size = mPowerSaveWhitelistAllAppIds.size(); + if (size > 0) { + pw.println(" Whitelist all app ids:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.print(mPowerSaveWhitelistAllAppIds.keyAt(i)); + pw.println(); + } + } + dumpTempWhitelistSchedule(pw, true); + + size = mTempWhitelistAppIdArray != null ? mTempWhitelistAppIdArray.length : 0; + if (size > 0) { + pw.println(" Temp whitelist app ids:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.print(mTempWhitelistAppIdArray[i]); + pw.println(); + } + } + + pw.print(" mLightEnabled="); pw.print(mLightEnabled); + pw.print(" mDeepEnabled="); pw.println(mDeepEnabled); + pw.print(" mForceIdle="); pw.println(mForceIdle); + pw.print(" mUseMotionSensor="); pw.print(mUseMotionSensor); + if (mUseMotionSensor) { + pw.print(" mMotionSensor="); pw.println(mMotionSensor); + } else { + pw.println(); + } + pw.print(" mScreenOn="); pw.println(mScreenOn); + pw.print(" mScreenLocked="); pw.println(mScreenLocked); + pw.print(" mNetworkConnected="); pw.println(mNetworkConnected); + pw.print(" mCharging="); pw.println(mCharging); + if (mConstraints.size() != 0) { + pw.println(" mConstraints={"); + for (int i = 0; i < mConstraints.size(); i++) { + final DeviceIdleConstraintTracker tracker = mConstraints.valueAt(i); + pw.print(" \""); pw.print(tracker.name); pw.print("\"="); + if (tracker.minState == mState) { + pw.println(tracker.active); + } else { + pw.print("ignored <mMinState="); pw.print(stateToString(tracker.minState)); + pw.println(">"); + } + } + pw.println(" }"); + } + if (mUseMotionSensor || mStationaryListeners.size() > 0) { + pw.print(" mMotionActive="); pw.println(mMotionListener.active); + pw.print(" mNotMoving="); pw.println(mNotMoving); + pw.print(" mMotionListener.activatedTimeElapsed="); + pw.println(mMotionListener.activatedTimeElapsed); + pw.print(" mLastMotionEventElapsed="); pw.println(mLastMotionEventElapsed); + pw.print(" "); pw.print(mStationaryListeners.size()); + pw.println(" stationary listeners registered"); + } + pw.print(" mLocating="); pw.print(mLocating); pw.print(" mHasGps="); + pw.print(mHasGps); pw.print(" mHasNetwork="); + pw.print(mHasNetworkLocation); pw.print(" mLocated="); pw.println(mLocated); + if (mLastGenericLocation != null) { + pw.print(" mLastGenericLocation="); pw.println(mLastGenericLocation); + } + if (mLastGpsLocation != null) { + pw.print(" mLastGpsLocation="); pw.println(mLastGpsLocation); + } + pw.print(" mState="); pw.print(stateToString(mState)); + pw.print(" mLightState="); + pw.println(lightStateToString(mLightState)); + pw.print(" mInactiveTimeout="); TimeUtils.formatDuration(mInactiveTimeout, pw); + pw.println(); + if (mActiveIdleOpCount != 0) { + pw.print(" mActiveIdleOpCount="); pw.println(mActiveIdleOpCount); + } + if (mNextAlarmTime != 0) { + pw.print(" mNextAlarmTime="); + TimeUtils.formatDuration(mNextAlarmTime, SystemClock.elapsedRealtime(), pw); + pw.println(); + } + if (mNextIdlePendingDelay != 0) { + pw.print(" mNextIdlePendingDelay="); + TimeUtils.formatDuration(mNextIdlePendingDelay, pw); + pw.println(); + } + if (mNextIdleDelay != 0) { + pw.print(" mNextIdleDelay="); + TimeUtils.formatDuration(mNextIdleDelay, pw); + pw.println(); + } + if (mNextLightIdleDelay != 0) { + pw.print(" mNextIdleDelay="); + TimeUtils.formatDuration(mNextLightIdleDelay, pw); + pw.println(); + } + if (mNextLightAlarmTime != 0) { + pw.print(" mNextLightAlarmTime="); + TimeUtils.formatDuration(mNextLightAlarmTime, SystemClock.elapsedRealtime(), pw); + pw.println(); + } + if (mCurLightIdleBudget != 0) { + pw.print(" mCurLightIdleBudget="); + TimeUtils.formatDuration(mCurLightIdleBudget, pw); + pw.println(); + } + if (mMaintenanceStartTime != 0) { + pw.print(" mMaintenanceStartTime="); + TimeUtils.formatDuration(mMaintenanceStartTime, SystemClock.elapsedRealtime(), pw); + pw.println(); + } + if (mJobsActive) { + pw.print(" mJobsActive="); pw.println(mJobsActive); + } + if (mAlarmsActive) { + pw.print(" mAlarmsActive="); pw.println(mAlarmsActive); + } + if (Math.abs(mPreIdleFactor - 1.0f) > MIN_PRE_IDLE_FACTOR_CHANGE) { + pw.print(" mPreIdleFactor="); pw.println(mPreIdleFactor); + } + } + } + + void dumpTempWhitelistSchedule(PrintWriter pw, boolean printTitle) { + final int size = mTempWhitelistAppIdEndTimes.size(); + if (size > 0) { + String prefix = ""; + if (printTitle) { + pw.println(" Temp whitelist schedule:"); + prefix = " "; + } + final long timeNow = SystemClock.elapsedRealtime(); + for (int i = 0; i < size; i++) { + pw.print(prefix); + pw.print("UID="); + pw.print(mTempWhitelistAppIdEndTimes.keyAt(i)); + pw.print(": "); + Pair<MutableLong, String> entry = mTempWhitelistAppIdEndTimes.valueAt(i); + TimeUtils.formatDuration(entry.first.value, timeNow, pw); + pw.print(" - "); + pw.println(entry.second); + } + } + } + } diff --git a/apex/jobscheduler/service/java/com/android/server/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/TEST_MAPPING new file mode 100644 index 000000000000..d99830dc47c9 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/TEST_MAPPING @@ -0,0 +1,23 @@ +{ + "presubmit": [ + { + "name": "FrameworksMockingServicesTests", + "file_patterns": [ + "DeviceIdleController\\.java" + ], + "options": [ + {"include-filter": "com.android.server.DeviceIdleControllerTest"}, + {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.FlakyTest"} + ] + } + ], + "postsubmit": [ + { + "name": "FrameworksMockingServicesTests", + "options": [ + {"include-filter": "com.android.server"} + ] + } + ] +}
\ No newline at end of file diff --git a/apex/jobscheduler/service/java/com/android/server/deviceidle/BluetoothConstraint.java b/apex/jobscheduler/service/java/com/android/server/deviceidle/BluetoothConstraint.java new file mode 100644 index 000000000000..cf181a99f3db --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/deviceidle/BluetoothConstraint.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.deviceidle; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Message; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.DeviceIdleInternal; + +// TODO: Should we part of the apex, or the platform?? + +/** + * Track whether there are any active Bluetooth devices connected. + */ +public class BluetoothConstraint implements IDeviceIdleConstraint { + private static final String TAG = BluetoothConstraint.class.getSimpleName(); + private static final long INACTIVITY_TIMEOUT_MS = 20 * 60 * 1000L; + + private final Context mContext; + private final Handler mHandler; + private final DeviceIdleInternal mLocalService; + private final BluetoothManager mBluetoothManager; + + private volatile boolean mConnected = true; + private volatile boolean mMonitoring = false; + + public BluetoothConstraint( + Context context, Handler handler, DeviceIdleInternal localService) { + mContext = context; + mHandler = handler; + mLocalService = localService; + mBluetoothManager = mContext.getSystemService(BluetoothManager.class); + } + + @Override + public synchronized void startMonitoring() { + // Start by assuming we have a connected bluetooth device. + mConnected = true; + mMonitoring = true; + + // Register a receiver to get updates on bluetooth devices disconnecting or the + // adapter state changing. + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + mContext.registerReceiver(mReceiver, filter); + + // Some devices will try to stay connected indefinitely. Set a timeout to ignore them. + mHandler.sendMessageDelayed( + Message.obtain(mHandler, mTimeoutCallback), INACTIVITY_TIMEOUT_MS); + + // Now we have the receiver registered, make a direct check for connected devices. + updateAndReportActiveLocked(); + } + + @Override + public synchronized void stopMonitoring() { + mContext.unregisterReceiver(mReceiver); + mHandler.removeCallbacks(mTimeoutCallback); + mMonitoring = false; + } + + private synchronized void cancelMonitoringDueToTimeout() { + if (mMonitoring) { + mMonitoring = false; + mLocalService.onConstraintStateChanged(this, /* active= */ false); + } + } + + /** + * Check the latest data from BluetoothManager and let DeviceIdleController know whether we + * have connected devices (for example TV remotes / gamepads) and thus want to stay awake. + */ + @GuardedBy("this") + private void updateAndReportActiveLocked() { + final boolean connected = isBluetoothConnected(mBluetoothManager); + if (connected != mConnected) { + mConnected = connected; + // If we lost all of our connections, we are on track to going into idle state. + mLocalService.onConstraintStateChanged(this, /* active= */ mConnected); + } + } + + /** + * True if the bluetooth adapter exists, is enabled, and has at least one GATT device connected. + */ + @VisibleForTesting + static boolean isBluetoothConnected(BluetoothManager bluetoothManager) { + BluetoothAdapter adapter = bluetoothManager.getAdapter(); + if (adapter != null && adapter.isEnabled()) { + return bluetoothManager.getConnectedDevices(BluetoothProfile.GATT).size() > 0; + } + return false; + } + + /** + * Registered in {@link #startMonitoring()}, unregistered in {@link #stopMonitoring()}. + */ + @VisibleForTesting + final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(intent.getAction())) { + mLocalService.exitIdle("bluetooth"); + } else { + updateAndReportActiveLocked(); + } + } + }; + + private final Runnable mTimeoutCallback = () -> cancelMonitoringDueToTimeout(); +} diff --git a/apex/jobscheduler/service/java/com/android/server/deviceidle/DeviceIdleConstraintTracker.java b/apex/jobscheduler/service/java/com/android/server/deviceidle/DeviceIdleConstraintTracker.java new file mode 100644 index 000000000000..4d5760ef9c86 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/deviceidle/DeviceIdleConstraintTracker.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.deviceidle; + +/** + * Current state of an {@link IDeviceIdleConstraint}. + * + * If the current doze state is between leastActive and mostActive, then startMonitoring() will + * be the most recent call. Otherwise, stopMonitoring() is the most recent call. + */ +public class DeviceIdleConstraintTracker { + + /** + * Appears in "dumpsys deviceidle". + */ + public final String name; + + /** + * Whenever a constraint is active, it will keep the device at or above + * minState (provided the rule is currently in effect). + * + */ + public final int minState; + + /** + * Whether this constraint currently prevents going below {@link #minState}. + * + * When the state is set to exactly minState, active is automatically + * overwritten with {@code true}. + */ + public boolean active = false; + + /** + * Internal tracking for whether the {@link IDeviceIdleConstraint} on the other + * side has been told it needs to send updates. + */ + public boolean monitoring = false; + + public DeviceIdleConstraintTracker(final String name, int minState) { + this.name = name; + this.minState = minState; + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/deviceidle/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/deviceidle/TEST_MAPPING new file mode 100644 index 000000000000..b76c582cf287 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/deviceidle/TEST_MAPPING @@ -0,0 +1,20 @@ +{ + "presubmit": [ + { + "name": "FrameworksMockingServicesTests", + "options": [ + {"include-filter": "com.android.server.DeviceIdleControllerTest"}, + {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.FlakyTest"} + ] + } + ], + "postsubmit": [ + { + "name": "FrameworksMockingServicesTests", + "options": [ + {"include-filter": "com.android.server"} + ] + } + ] +}
\ No newline at end of file diff --git a/apex/jobscheduler/service/java/com/android/server/deviceidle/TvConstraintController.java b/apex/jobscheduler/service/java/com/android/server/deviceidle/TvConstraintController.java new file mode 100644 index 000000000000..7f0a2717ed4a --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/deviceidle/TvConstraintController.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.deviceidle; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Handler; + +import com.android.server.DeviceIdleInternal; +import com.android.server.LocalServices; + +/** + * Device idle constraints for television devices. + * + * <p>Televisions are devices with {@code FEATURE_LEANBACK_ONLY}. Other devices might support + * some kind of leanback mode but they should not follow the same rules for idle state. + */ +public class TvConstraintController implements ConstraintController { + private final Context mContext; + private final Handler mHandler; + private final DeviceIdleInternal mDeviceIdleService; + + @Nullable + private final BluetoothConstraint mBluetoothConstraint; + + public TvConstraintController(Context context, Handler handler) { + mContext = context; + mHandler = handler; + mDeviceIdleService = LocalServices.getService(DeviceIdleInternal.class); + + final PackageManager pm = context.getPackageManager(); + mBluetoothConstraint = pm.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) + ? new BluetoothConstraint(mContext, mHandler, mDeviceIdleService) + : null; + } + + @Override + public void start() { + if (mBluetoothConstraint != null) { + mDeviceIdleService.registerDeviceIdleConstraint( + mBluetoothConstraint, "bluetooth", IDeviceIdleConstraint.SENSING_OR_ABOVE); + } + } + + @Override + public void stop() { + if (mBluetoothConstraint != null) { + mDeviceIdleService.unregisterDeviceIdleConstraint(mBluetoothConstraint); + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/GrantedUriPermissions.java b/apex/jobscheduler/service/java/com/android/server/job/GrantedUriPermissions.java new file mode 100644 index 000000000000..b7e8cf6e3fc8 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/GrantedUriPermissions.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job; + +import android.app.UriGrantsManager; +import android.content.ClipData; +import android.content.ContentProvider; +import android.content.Intent; +import android.net.Uri; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; +import com.android.server.LocalServices; +import com.android.server.uri.UriGrantsManagerInternal; + +import java.io.PrintWriter; +import java.util.ArrayList; + +public final class GrantedUriPermissions { + private final int mGrantFlags; + private final int mSourceUserId; + private final String mTag; + private final IBinder mPermissionOwner; + private final ArrayList<Uri> mUris = new ArrayList<>(); + + private GrantedUriPermissions(int grantFlags, int uid, String tag) + throws RemoteException { + mGrantFlags = grantFlags; + mSourceUserId = UserHandle.getUserId(uid); + mTag = tag; + mPermissionOwner = LocalServices + .getService(UriGrantsManagerInternal.class).newUriPermissionOwner("job: " + tag); + } + + public void revoke() { + for (int i = mUris.size()-1; i >= 0; i--) { + LocalServices.getService(UriGrantsManagerInternal.class).revokeUriPermissionFromOwner( + mPermissionOwner, mUris.get(i), mGrantFlags, mSourceUserId); + } + mUris.clear(); + } + + public static boolean checkGrantFlags(int grantFlags) { + return (grantFlags & (Intent.FLAG_GRANT_WRITE_URI_PERMISSION + |Intent.FLAG_GRANT_READ_URI_PERMISSION)) != 0; + } + + public static GrantedUriPermissions createFromIntent(Intent intent, + int sourceUid, String targetPackage, int targetUserId, String tag) { + int grantFlags = intent.getFlags(); + if (!checkGrantFlags(grantFlags)) { + return null; + } + + GrantedUriPermissions perms = null; + + Uri data = intent.getData(); + if (data != null) { + perms = grantUri(data, sourceUid, targetPackage, targetUserId, grantFlags, tag, + perms); + } + + ClipData clip = intent.getClipData(); + if (clip != null) { + perms = grantClip(clip, sourceUid, targetPackage, targetUserId, grantFlags, tag, + perms); + } + + return perms; + } + + public static GrantedUriPermissions createFromClip(ClipData clip, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag) { + if (!checkGrantFlags(grantFlags)) { + return null; + } + GrantedUriPermissions perms = null; + if (clip != null) { + perms = grantClip(clip, sourceUid, targetPackage, targetUserId, grantFlags, + tag, perms); + } + return perms; + } + + private static GrantedUriPermissions grantClip(ClipData clip, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag, + GrantedUriPermissions curPerms) { + final int N = clip.getItemCount(); + for (int i = 0; i < N; i++) { + curPerms = grantItem(clip.getItemAt(i), sourceUid, targetPackage, targetUserId, + grantFlags, tag, curPerms); + } + return curPerms; + } + + private static GrantedUriPermissions grantUri(Uri uri, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag, + GrantedUriPermissions curPerms) { + try { + int sourceUserId = ContentProvider.getUserIdFromUri(uri, + UserHandle.getUserId(sourceUid)); + uri = ContentProvider.getUriWithoutUserId(uri); + if (curPerms == null) { + curPerms = new GrantedUriPermissions(grantFlags, sourceUid, tag); + } + UriGrantsManager.getService().grantUriPermissionFromOwner(curPerms.mPermissionOwner, + sourceUid, targetPackage, uri, grantFlags, sourceUserId, targetUserId); + curPerms.mUris.add(uri); + } catch (RemoteException e) { + Slog.e("JobScheduler", "AM dead"); + } + return curPerms; + } + + private static GrantedUriPermissions grantItem(ClipData.Item item, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag, + GrantedUriPermissions curPerms) { + if (item.getUri() != null) { + curPerms = grantUri(item.getUri(), sourceUid, targetPackage, targetUserId, + grantFlags, tag, curPerms); + } + Intent intent = item.getIntent(); + if (intent != null && intent.getData() != null) { + curPerms = grantUri(intent.getData(), sourceUid, targetPackage, targetUserId, + grantFlags, tag, curPerms); + } + return curPerms; + } + + // Dumpsys infrastructure + public void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.print("mGrantFlags=0x"); pw.print(Integer.toHexString(mGrantFlags)); + pw.print(" mSourceUserId="); pw.println(mSourceUserId); + pw.print(prefix); pw.print("mTag="); pw.println(mTag); + pw.print(prefix); pw.print("mPermissionOwner="); pw.println(mPermissionOwner); + for (int i = 0; i < mUris.size(); i++) { + pw.print(prefix); pw.print("#"); pw.print(i); pw.print(": "); + pw.println(mUris.get(i)); + } + } + + public void dump(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write(GrantedUriPermissionsDumpProto.FLAGS, mGrantFlags); + proto.write(GrantedUriPermissionsDumpProto.SOURCE_USER_ID, mSourceUserId); + proto.write(GrantedUriPermissionsDumpProto.TAG, mTag); + proto.write(GrantedUriPermissionsDumpProto.PERMISSION_OWNER, mPermissionOwner.toString()); + for (int i = 0; i < mUris.size(); i++) { + Uri u = mUris.get(i); + if (u != null) { + proto.write(GrantedUriPermissionsDumpProto.URIS, u.toString()); + } + } + + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java b/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java new file mode 100644 index 000000000000..34ba753b3daa --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job; + +import com.android.server.job.controllers.JobStatus; + +/** + * Used for communication between {@link com.android.server.job.JobServiceContext} and the + * {@link com.android.server.job.JobSchedulerService}. + */ +public interface JobCompletedListener { + /** + * Callback for when a job is completed. + * @param needsReschedule Whether the implementing class should reschedule this job. + */ + void onJobCompletedLocked(JobStatus jobStatus, boolean needsReschedule); +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java new file mode 100644 index 000000000000..435384dd2319 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -0,0 +1,726 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.job; + +import android.app.ActivityManager; +import android.app.job.JobInfo; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.PowerManager; +import android.os.RemoteException; +import android.util.Slog; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.procstats.ProcessStats; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.StatLogger; +import com.android.server.job.JobSchedulerService.Constants; +import com.android.server.job.JobSchedulerService.MaxJobCountsPerMemoryTrimLevel; +import com.android.server.job.controllers.JobStatus; +import com.android.server.job.controllers.StateController; + +import java.util.Iterator; +import java.util.List; + +/** + * This class decides, given the various configuration and the system status, how many more jobs + * can start. + */ +class JobConcurrencyManager { + private static final String TAG = JobSchedulerService.TAG; + private static final boolean DEBUG = JobSchedulerService.DEBUG; + + private final Object mLock; + private final JobSchedulerService mService; + private final JobSchedulerService.Constants mConstants; + private final Context mContext; + private final Handler mHandler; + + private PowerManager mPowerManager; + + private boolean mCurrentInteractiveState; + private boolean mEffectiveInteractiveState; + + private long mLastScreenOnRealtime; + private long mLastScreenOffRealtime; + + private static final int MAX_JOB_CONTEXTS_COUNT = JobSchedulerService.MAX_JOB_CONTEXTS_COUNT; + + /** + * This array essentially stores the state of mActiveServices array. + * The ith index stores the job present on the ith JobServiceContext. + * We manipulate this array until we arrive at what jobs should be running on + * what JobServiceContext. + */ + JobStatus[] mRecycledAssignContextIdToJobMap = new JobStatus[MAX_JOB_CONTEXTS_COUNT]; + + boolean[] mRecycledSlotChanged = new boolean[MAX_JOB_CONTEXTS_COUNT]; + + int[] mRecycledPreferredUidForContext = new int[MAX_JOB_CONTEXTS_COUNT]; + + /** Max job counts according to the current system state. */ + private JobSchedulerService.MaxJobCounts mMaxJobCounts; + + private final JobCountTracker mJobCountTracker = new JobCountTracker(); + + /** Current memory trim level. */ + private int mLastMemoryTrimLevel; + + /** Used to throttle heavy API calls. */ + private long mNextSystemStateRefreshTime; + private static final int SYSTEM_STATE_REFRESH_MIN_INTERVAL = 1000; + + private final StatLogger mStatLogger = new StatLogger(new String[]{ + "assignJobsToContexts", + "refreshSystemState", + }); + + interface Stats { + int ASSIGN_JOBS_TO_CONTEXTS = 0; + int REFRESH_SYSTEM_STATE = 1; + + int COUNT = REFRESH_SYSTEM_STATE + 1; + } + + JobConcurrencyManager(JobSchedulerService service) { + mService = service; + mLock = mService.mLock; + mConstants = service.mConstants; + mContext = service.getContext(); + + mHandler = BackgroundThread.getHandler(); + } + + public void onSystemReady() { + mPowerManager = mContext.getSystemService(PowerManager.class); + + final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + mContext.registerReceiver(mReceiver, filter); + + onInteractiveStateChanged(mPowerManager.isInteractive()); + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case Intent.ACTION_SCREEN_ON: + onInteractiveStateChanged(true); + break; + case Intent.ACTION_SCREEN_OFF: + onInteractiveStateChanged(false); + break; + } + } + }; + + /** + * Called when the screen turns on / off. + */ + private void onInteractiveStateChanged(boolean interactive) { + synchronized (mLock) { + if (mCurrentInteractiveState == interactive) { + return; + } + mCurrentInteractiveState = interactive; + if (DEBUG) { + Slog.d(TAG, "Interactive: " + interactive); + } + + final long nowRealtime = JobSchedulerService.sElapsedRealtimeClock.millis(); + if (interactive) { + mLastScreenOnRealtime = nowRealtime; + mEffectiveInteractiveState = true; + + mHandler.removeCallbacks(mRampUpForScreenOff); + } else { + mLastScreenOffRealtime = nowRealtime; + + // Set mEffectiveInteractiveState to false after the delay, when we may increase + // the concurrency. + // We don't need a wakeup alarm here. When there's a pending job, there should + // also be jobs running too, meaning the device should be awake. + + // Note: we can't directly do postDelayed(this::rampUpForScreenOn), because + // we need the exact same instance for removeCallbacks(). + mHandler.postDelayed(mRampUpForScreenOff, + mConstants.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.getValue()); + } + } + } + + private final Runnable mRampUpForScreenOff = this::rampUpForScreenOff; + + /** + * Called in {@link Constants#SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS} after + * the screen turns off, in order to increase concurrency. + */ + private void rampUpForScreenOff() { + synchronized (mLock) { + // Make sure the screen has really been off for the configured duration. + // (There could be a race.) + if (!mEffectiveInteractiveState) { + return; + } + if (mLastScreenOnRealtime > mLastScreenOffRealtime) { + return; + } + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + if ((mLastScreenOffRealtime + + mConstants.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.getValue()) + > now) { + return; + } + + mEffectiveInteractiveState = false; + + if (DEBUG) { + Slog.d(TAG, "Ramping up concurrency"); + } + + mService.maybeRunPendingJobsLocked(); + } + } + + private boolean isFgJob(JobStatus job) { + return job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP; + } + + @GuardedBy("mLock") + private void refreshSystemStateLocked() { + final long nowUptime = JobSchedulerService.sUptimeMillisClock.millis(); + + // Only refresh the information every so often. + if (nowUptime < mNextSystemStateRefreshTime) { + return; + } + + final long start = mStatLogger.getTime(); + mNextSystemStateRefreshTime = nowUptime + SYSTEM_STATE_REFRESH_MIN_INTERVAL; + + mLastMemoryTrimLevel = ProcessStats.ADJ_MEM_FACTOR_NORMAL; + try { + mLastMemoryTrimLevel = ActivityManager.getService().getMemoryTrimLevel(); + } catch (RemoteException e) { + } + + mStatLogger.logDurationStat(Stats.REFRESH_SYSTEM_STATE, start); + } + + @GuardedBy("mLock") + private void updateMaxCountsLocked() { + refreshSystemStateLocked(); + + final MaxJobCountsPerMemoryTrimLevel jobCounts = mEffectiveInteractiveState + ? mConstants.MAX_JOB_COUNTS_SCREEN_ON + : mConstants.MAX_JOB_COUNTS_SCREEN_OFF; + + + switch (mLastMemoryTrimLevel) { + case ProcessStats.ADJ_MEM_FACTOR_MODERATE: + mMaxJobCounts = jobCounts.moderate; + break; + case ProcessStats.ADJ_MEM_FACTOR_LOW: + mMaxJobCounts = jobCounts.low; + break; + case ProcessStats.ADJ_MEM_FACTOR_CRITICAL: + mMaxJobCounts = jobCounts.critical; + break; + default: + mMaxJobCounts = jobCounts.normal; + break; + } + } + + /** + * Takes jobs from pending queue and runs them on available contexts. + * If no contexts are available, preempts lower priority jobs to + * run higher priority ones. + * Lock on mJobs before calling this function. + */ + @GuardedBy("mLock") + void assignJobsToContextsLocked() { + final long start = mStatLogger.getTime(); + + assignJobsToContextsInternalLocked(); + + mStatLogger.logDurationStat(Stats.ASSIGN_JOBS_TO_CONTEXTS, start); + } + + @GuardedBy("mLock") + private void assignJobsToContextsInternalLocked() { + if (DEBUG) { + Slog.d(TAG, printPendingQueueLocked()); + } + + final JobPackageTracker tracker = mService.mJobPackageTracker; + final List<JobStatus> pendingJobs = mService.mPendingJobs; + final List<JobServiceContext> activeServices = mService.mActiveServices; + final List<StateController> controllers = mService.mControllers; + + updateMaxCountsLocked(); + + // To avoid GC churn, we recycle the arrays. + JobStatus[] contextIdToJobMap = mRecycledAssignContextIdToJobMap; + boolean[] slotChanged = mRecycledSlotChanged; + int[] preferredUidForContext = mRecycledPreferredUidForContext; + + + // Initialize the work variables and also count running jobs. + mJobCountTracker.reset( + mMaxJobCounts.getMaxTotal(), + mMaxJobCounts.getMaxBg(), + mMaxJobCounts.getMinBg()); + + for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) { + final JobServiceContext js = mService.mActiveServices.get(i); + final JobStatus status = js.getRunningJobLocked(); + + if ((contextIdToJobMap[i] = status) != null) { + mJobCountTracker.incrementRunningJobCount(isFgJob(status)); + } + + slotChanged[i] = false; + preferredUidForContext[i] = js.getPreferredUid(); + } + if (DEBUG) { + Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial")); + } + + // Next, update the job priorities, and also count the pending FG / BG jobs. + for (int i = 0; i < pendingJobs.size(); i++) { + final JobStatus pending = pendingJobs.get(i); + + // If job is already running, go to next job. + int jobRunningContext = findJobContextIdFromMap(pending, contextIdToJobMap); + if (jobRunningContext != -1) { + continue; + } + + final int priority = mService.evaluateJobPriorityLocked(pending); + pending.lastEvaluatedPriority = priority; + + mJobCountTracker.incrementPendingJobCount(isFgJob(pending)); + } + + mJobCountTracker.onCountDone(); + + for (int i = 0; i < pendingJobs.size(); i++) { + final JobStatus nextPending = pendingJobs.get(i); + + // Unfortunately we need to repeat this relatively expensive check. + int jobRunningContext = findJobContextIdFromMap(nextPending, contextIdToJobMap); + if (jobRunningContext != -1) { + continue; + } + + final boolean isPendingFg = isFgJob(nextPending); + + // Find an available slot for nextPending. The context should be available OR + // it should have lowest priority among all running jobs + // (sharing the same Uid as nextPending) + int minPriorityForPreemption = Integer.MAX_VALUE; + int selectedContextId = -1; + boolean startingJob = false; + for (int j=0; j<MAX_JOB_CONTEXTS_COUNT; j++) { + JobStatus job = contextIdToJobMap[j]; + int preferredUid = preferredUidForContext[j]; + if (job == null) { + final boolean preferredUidOkay = (preferredUid == nextPending.getUid()) + || (preferredUid == JobServiceContext.NO_PREFERRED_UID); + + if (preferredUidOkay && mJobCountTracker.canJobStart(isPendingFg)) { + // This slot is free, and we haven't yet hit the limit on + // concurrent jobs... we can just throw the job in to here. + selectedContextId = j; + startingJob = true; + break; + } + // No job on this context, but nextPending can't run here because + // the context has a preferred Uid or we have reached the limit on + // concurrent jobs. + continue; + } + if (job.getUid() != nextPending.getUid()) { + continue; + } + + final int jobPriority = mService.evaluateJobPriorityLocked(job); + if (jobPriority >= nextPending.lastEvaluatedPriority) { + continue; + } + + if (minPriorityForPreemption > jobPriority) { + // Step down the preemption threshold - wind up replacing + // the lowest-priority running job + minPriorityForPreemption = jobPriority; + selectedContextId = j; + // In this case, we're just going to preempt a low priority job, we're not + // actually starting a job, so don't set startingJob. + } + } + if (selectedContextId != -1) { + contextIdToJobMap[selectedContextId] = nextPending; + slotChanged[selectedContextId] = true; + } + if (startingJob) { + // Increase the counters when we're going to start a job. + mJobCountTracker.onStartingNewJob(isPendingFg); + } + } + if (DEBUG) { + Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs final")); + } + + mJobCountTracker.logStatus(); + + tracker.noteConcurrency(mJobCountTracker.getTotalRunningJobCountToNote(), + mJobCountTracker.getFgRunningJobCountToNote()); + + for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) { + boolean preservePreferredUid = false; + if (slotChanged[i]) { + JobStatus js = activeServices.get(i).getRunningJobLocked(); + if (js != null) { + if (DEBUG) { + Slog.d(TAG, "preempting job: " + + activeServices.get(i).getRunningJobLocked()); + } + // preferredUid will be set to uid of currently running job. + activeServices.get(i).preemptExecutingJobLocked(); + preservePreferredUid = true; + } else { + final JobStatus pendingJob = contextIdToJobMap[i]; + if (DEBUG) { + Slog.d(TAG, "About to run job on context " + + i + ", job: " + pendingJob); + } + for (int ic=0; ic<controllers.size(); ic++) { + controllers.get(ic).prepareForExecutionLocked(pendingJob); + } + if (!activeServices.get(i).executeRunnableJob(pendingJob)) { + Slog.d(TAG, "Error executing " + pendingJob); + } + if (pendingJobs.remove(pendingJob)) { + tracker.noteNonpending(pendingJob); + } + } + } + if (!preservePreferredUid) { + activeServices.get(i).clearPreferredUid(); + } + } + } + + private static int findJobContextIdFromMap(JobStatus jobStatus, JobStatus[] map) { + for (int i=0; i<map.length; i++) { + if (map[i] != null && map[i].matches(jobStatus.getUid(), jobStatus.getJobId())) { + return i; + } + } + return -1; + } + + @GuardedBy("mLock") + private String printPendingQueueLocked() { + StringBuilder s = new StringBuilder("Pending queue: "); + Iterator<JobStatus> it = mService.mPendingJobs.iterator(); + while (it.hasNext()) { + JobStatus js = it.next(); + s.append("(") + .append(js.getJob().getId()) + .append(", ") + .append(js.getUid()) + .append(") "); + } + return s.toString(); + } + + private static String printContextIdToJobMap(JobStatus[] map, String initial) { + StringBuilder s = new StringBuilder(initial + ": "); + for (int i=0; i<map.length; i++) { + s.append("(") + .append(map[i] == null? -1: map[i].getJobId()) + .append(map[i] == null? -1: map[i].getUid()) + .append(")" ); + } + return s.toString(); + } + + + public void dumpLocked(IndentingPrintWriter pw, long now, long nowRealtime) { + pw.println("Concurrency:"); + + pw.increaseIndent(); + try { + pw.print("Screen state: current "); + pw.print(mCurrentInteractiveState ? "ON" : "OFF"); + pw.print(" effective "); + pw.print(mEffectiveInteractiveState ? "ON" : "OFF"); + pw.println(); + + pw.print("Last screen ON: "); + TimeUtils.dumpTimeWithDelta(pw, now - nowRealtime + mLastScreenOnRealtime, now); + pw.println(); + + pw.print("Last screen OFF: "); + TimeUtils.dumpTimeWithDelta(pw, now - nowRealtime + mLastScreenOffRealtime, now); + pw.println(); + + pw.println(); + + pw.println("Current max jobs:"); + pw.println(" "); + pw.println(mJobCountTracker); + + pw.println(); + + pw.print("mLastMemoryTrimLevel: "); + pw.print(mLastMemoryTrimLevel); + pw.println(); + + mStatLogger.dump(pw); + } finally { + pw.decreaseIndent(); + } + } + + public void dumpProtoLocked(ProtoOutputStream proto, long tag, long now, long nowRealtime) { + final long token = proto.start(tag); + + proto.write(JobConcurrencyManagerProto.CURRENT_INTERACTIVE_STATE, mCurrentInteractiveState); + proto.write(JobConcurrencyManagerProto.EFFECTIVE_INTERACTIVE_STATE, + mEffectiveInteractiveState); + + proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_ON_MS, + nowRealtime - mLastScreenOnRealtime); + proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_OFF_MS, + nowRealtime - mLastScreenOffRealtime); + + mJobCountTracker.dumpProto(proto, JobConcurrencyManagerProto.JOB_COUNT_TRACKER); + + proto.write(JobConcurrencyManagerProto.MEMORY_TRIM_LEVEL, mLastMemoryTrimLevel); + + mStatLogger.dumpProto(proto, JobConcurrencyManagerProto.STATS); + + proto.end(token); + } + + /** + * This class decides, taking into account {@link #mMaxJobCounts} and how mny jos are running / + * pending, how many more job can start. + * + * Extracted for testing and logging. + */ + @VisibleForTesting + static class JobCountTracker { + private int mConfigNumMaxTotalJobs; + private int mConfigNumMaxBgJobs; + private int mConfigNumMinBgJobs; + + private int mNumRunningFgJobs; + private int mNumRunningBgJobs; + + private int mNumPendingFgJobs; + private int mNumPendingBgJobs; + + private int mNumStartingFgJobs; + private int mNumStartingBgJobs; + + private int mNumReservedForBg; + private int mNumActualMaxFgJobs; + private int mNumActualMaxBgJobs; + + void reset(int numTotalMaxJobs, int numMaxBgJobs, int numMinBgJobs) { + mConfigNumMaxTotalJobs = numTotalMaxJobs; + mConfigNumMaxBgJobs = numMaxBgJobs; + mConfigNumMinBgJobs = numMinBgJobs; + + mNumRunningFgJobs = 0; + mNumRunningBgJobs = 0; + + mNumPendingFgJobs = 0; + mNumPendingBgJobs = 0; + + mNumStartingFgJobs = 0; + mNumStartingBgJobs = 0; + + mNumReservedForBg = 0; + mNumActualMaxFgJobs = 0; + mNumActualMaxBgJobs = 0; + } + + void incrementRunningJobCount(boolean isFg) { + if (isFg) { + mNumRunningFgJobs++; + } else { + mNumRunningBgJobs++; + } + } + + void incrementPendingJobCount(boolean isFg) { + if (isFg) { + mNumPendingFgJobs++; + } else { + mNumPendingBgJobs++; + } + } + + void onStartingNewJob(boolean isFg) { + if (isFg) { + mNumStartingFgJobs++; + } else { + mNumStartingBgJobs++; + } + } + + void onCountDone() { + // Note some variables are used only here but are made class members in order to have + // them on logcat / dumpsys. + + // How many slots should we allocate to BG jobs at least? + // That's basically "getMinBg()", but if there are less jobs, decrease it. + // (e.g. even if min-bg is 2, if there's only 1 running+pending job, this has to be 1.) + final int reservedForBg = Math.min( + mConfigNumMinBgJobs, + mNumRunningBgJobs + mNumPendingBgJobs); + + // However, if there are FG jobs already running, we have to adjust it. + mNumReservedForBg = Math.min(reservedForBg, + mConfigNumMaxTotalJobs - mNumRunningFgJobs); + + // Max FG is [total - [number needed for BG jobs]] + // [number needed for BG jobs] is the bigger one of [running BG] or [reserved BG] + final int maxFg = + mConfigNumMaxTotalJobs - Math.max(mNumRunningBgJobs, mNumReservedForBg); + + // The above maxFg is the theoretical max. If there are less FG jobs, the actual + // max FG will be lower accordingly. + mNumActualMaxFgJobs = Math.min( + maxFg, + mNumRunningFgJobs + mNumPendingFgJobs); + + // Max BG is [total - actual max FG], but cap at [config max BG]. + final int maxBg = Math.min( + mConfigNumMaxBgJobs, + mConfigNumMaxTotalJobs - mNumActualMaxFgJobs); + + // If there are less BG jobs than maxBg, then reduce the actual max BG accordingly. + // This isn't needed for the logic to work, but this will give consistent output + // on logcat and dumpsys. + mNumActualMaxBgJobs = Math.min( + maxBg, + mNumRunningBgJobs + mNumPendingBgJobs); + } + + boolean canJobStart(boolean isFg) { + if (isFg) { + return mNumRunningFgJobs + mNumStartingFgJobs < mNumActualMaxFgJobs; + } else { + return mNumRunningBgJobs + mNumStartingBgJobs < mNumActualMaxBgJobs; + } + } + + public int getNumStartingFgJobs() { + return mNumStartingFgJobs; + } + + public int getNumStartingBgJobs() { + return mNumStartingBgJobs; + } + + int getTotalRunningJobCountToNote() { + return mNumRunningFgJobs + mNumRunningBgJobs + + mNumStartingFgJobs + mNumStartingBgJobs; + } + + int getFgRunningJobCountToNote() { + return mNumRunningFgJobs + mNumStartingFgJobs; + } + + void logStatus() { + if (DEBUG) { + Slog.d(TAG, "assignJobsToContexts: " + this); + } + } + + public String toString() { + final int totalFg = mNumRunningFgJobs + mNumStartingFgJobs; + final int totalBg = mNumRunningBgJobs + mNumStartingBgJobs; + return String.format( + "Config={tot=%d bg min/max=%d/%d}" + + " Running[FG/BG (total)]: %d / %d (%d)" + + " Pending: %d / %d (%d)" + + " Actual max: %d%s / %d%s (%d%s)" + + " Res BG: %d" + + " Starting: %d / %d (%d)" + + " Total: %d%s / %d%s (%d%s)", + mConfigNumMaxTotalJobs, mConfigNumMinBgJobs, mConfigNumMaxBgJobs, + + mNumRunningFgJobs, mNumRunningBgJobs, mNumRunningFgJobs + mNumRunningBgJobs, + + mNumPendingFgJobs, mNumPendingBgJobs, mNumPendingFgJobs + mNumPendingBgJobs, + + mNumActualMaxFgJobs, (totalFg <= mConfigNumMaxTotalJobs) ? "" : "*", + mNumActualMaxBgJobs, (totalBg <= mConfigNumMaxBgJobs) ? "" : "*", + mNumActualMaxFgJobs + mNumActualMaxBgJobs, + (mNumActualMaxFgJobs + mNumActualMaxBgJobs <= mConfigNumMaxTotalJobs) + ? "" : "*", + + mNumReservedForBg, + + mNumStartingFgJobs, mNumStartingBgJobs, mNumStartingFgJobs + mNumStartingBgJobs, + + totalFg, (totalFg <= mNumActualMaxFgJobs) ? "" : "*", + totalBg, (totalBg <= mNumActualMaxBgJobs) ? "" : "*", + totalFg + totalBg, (totalFg + totalBg <= mConfigNumMaxTotalJobs) ? "" : "*" + ); + } + + public void dumpProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_TOTAL_JOBS, mConfigNumMaxTotalJobs); + proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_BG_JOBS, mConfigNumMaxBgJobs); + proto.write(JobCountTrackerProto.CONFIG_NUM_MIN_BG_JOBS, mConfigNumMinBgJobs); + + proto.write(JobCountTrackerProto.NUM_RUNNING_FG_JOBS, mNumRunningFgJobs); + proto.write(JobCountTrackerProto.NUM_RUNNING_BG_JOBS, mNumRunningBgJobs); + + proto.write(JobCountTrackerProto.NUM_PENDING_FG_JOBS, mNumPendingFgJobs); + proto.write(JobCountTrackerProto.NUM_PENDING_BG_JOBS, mNumPendingBgJobs); + + proto.write(JobCountTrackerProto.NUM_ACTUAL_MAX_FG_JOBS, mNumActualMaxFgJobs); + proto.write(JobCountTrackerProto.NUM_ACTUAL_MAX_BG_JOBS, mNumActualMaxBgJobs); + + proto.write(JobCountTrackerProto.NUM_RESERVED_FOR_BG, mNumReservedForBg); + + proto.write(JobCountTrackerProto.NUM_STARTING_FG_JOBS, mNumStartingFgJobs); + proto.write(JobCountTrackerProto.NUM_STARTING_BG_JOBS, mNumStartingBgJobs); + + proto.end(token); + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java b/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java new file mode 100644 index 000000000000..d05034797f3d --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java @@ -0,0 +1,655 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import static com.android.server.job.JobSchedulerService.sSystemClock; +import static com.android.server.job.JobSchedulerService.sUptimeMillisClock; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.os.UserHandle; +import android.text.format.DateFormat; +import android.util.ArrayMap; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.RingBufferIndices; +import com.android.server.job.controllers.JobStatus; + +import java.io.PrintWriter; + +public final class JobPackageTracker { + // We batch every 30 minutes. + static final long BATCHING_TIME = 30*60*1000; + // Number of historical data sets we keep. + static final int NUM_HISTORY = 5; + + private static final int EVENT_BUFFER_SIZE = 100; + + public static final int EVENT_CMD_MASK = 0xff; + public static final int EVENT_STOP_REASON_SHIFT = 8; + public static final int EVENT_STOP_REASON_MASK = 0xff << EVENT_STOP_REASON_SHIFT; + public static final int EVENT_NULL = 0; + public static final int EVENT_START_JOB = 1; + public static final int EVENT_STOP_JOB = 2; + public static final int EVENT_START_PERIODIC_JOB = 3; + public static final int EVENT_STOP_PERIODIC_JOB = 4; + + private final RingBufferIndices mEventIndices = new RingBufferIndices(EVENT_BUFFER_SIZE); + private final int[] mEventCmds = new int[EVENT_BUFFER_SIZE]; + private final long[] mEventTimes = new long[EVENT_BUFFER_SIZE]; + private final int[] mEventUids = new int[EVENT_BUFFER_SIZE]; + private final String[] mEventTags = new String[EVENT_BUFFER_SIZE]; + private final int[] mEventJobIds = new int[EVENT_BUFFER_SIZE]; + private final String[] mEventReasons = new String[EVENT_BUFFER_SIZE]; + + public void addEvent(int cmd, int uid, String tag, int jobId, int stopReason, + String debugReason) { + int index = mEventIndices.add(); + mEventCmds[index] = cmd | ((stopReason<<EVENT_STOP_REASON_SHIFT) & EVENT_STOP_REASON_MASK); + mEventTimes[index] = sElapsedRealtimeClock.millis(); + mEventUids[index] = uid; + mEventTags[index] = tag; + mEventJobIds[index] = jobId; + mEventReasons[index] = debugReason; + } + + DataSet mCurDataSet = new DataSet(); + DataSet[] mLastDataSets = new DataSet[NUM_HISTORY]; + + final static class PackageEntry { + long pastActiveTime; + long activeStartTime; + int activeNesting; + int activeCount; + boolean hadActive; + long pastActiveTopTime; + long activeTopStartTime; + int activeTopNesting; + int activeTopCount; + boolean hadActiveTop; + long pastPendingTime; + long pendingStartTime; + int pendingNesting; + int pendingCount; + boolean hadPending; + final SparseIntArray stopReasons = new SparseIntArray(); + + public long getActiveTime(long now) { + long time = pastActiveTime; + if (activeNesting > 0) { + time += now - activeStartTime; + } + return time; + } + + public long getActiveTopTime(long now) { + long time = pastActiveTopTime; + if (activeTopNesting > 0) { + time += now - activeTopStartTime; + } + return time; + } + + public long getPendingTime(long now) { + long time = pastPendingTime; + if (pendingNesting > 0) { + time += now - pendingStartTime; + } + return time; + } + } + + final static class DataSet { + final SparseArray<ArrayMap<String, PackageEntry>> mEntries = new SparseArray<>(); + final long mStartUptimeTime; + final long mStartElapsedTime; + final long mStartClockTime; + long mSummedTime; + int mMaxTotalActive; + int mMaxFgActive; + + public DataSet(DataSet otherTimes) { + mStartUptimeTime = otherTimes.mStartUptimeTime; + mStartElapsedTime = otherTimes.mStartElapsedTime; + mStartClockTime = otherTimes.mStartClockTime; + } + + public DataSet() { + mStartUptimeTime = sUptimeMillisClock.millis(); + mStartElapsedTime = sElapsedRealtimeClock.millis(); + mStartClockTime = sSystemClock.millis(); + } + + private PackageEntry getOrCreateEntry(int uid, String pkg) { + ArrayMap<String, PackageEntry> uidMap = mEntries.get(uid); + if (uidMap == null) { + uidMap = new ArrayMap<>(); + mEntries.put(uid, uidMap); + } + PackageEntry entry = uidMap.get(pkg); + if (entry == null) { + entry = new PackageEntry(); + uidMap.put(pkg, entry); + } + return entry; + } + + public PackageEntry getEntry(int uid, String pkg) { + ArrayMap<String, PackageEntry> uidMap = mEntries.get(uid); + if (uidMap == null) { + return null; + } + return uidMap.get(pkg); + } + + long getTotalTime(long now) { + if (mSummedTime > 0) { + return mSummedTime; + } + return now - mStartUptimeTime; + } + + void incPending(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.pendingNesting == 0) { + pe.pendingStartTime = now; + pe.pendingCount++; + } + pe.pendingNesting++; + } + + void decPending(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.pendingNesting == 1) { + pe.pastPendingTime += now - pe.pendingStartTime; + } + pe.pendingNesting--; + } + + void incActive(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeNesting == 0) { + pe.activeStartTime = now; + pe.activeCount++; + } + pe.activeNesting++; + } + + void decActive(int uid, String pkg, long now, int stopReason) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeNesting == 1) { + pe.pastActiveTime += now - pe.activeStartTime; + } + pe.activeNesting--; + int count = pe.stopReasons.get(stopReason, 0); + pe.stopReasons.put(stopReason, count+1); + } + + void incActiveTop(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeTopNesting == 0) { + pe.activeTopStartTime = now; + pe.activeTopCount++; + } + pe.activeTopNesting++; + } + + void decActiveTop(int uid, String pkg, long now, int stopReason) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeTopNesting == 1) { + pe.pastActiveTopTime += now - pe.activeTopStartTime; + } + pe.activeTopNesting--; + int count = pe.stopReasons.get(stopReason, 0); + pe.stopReasons.put(stopReason, count+1); + } + + void finish(DataSet next, long now) { + for (int i = mEntries.size() - 1; i >= 0; i--) { + ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i); + for (int j = uidMap.size() - 1; j >= 0; j--) { + PackageEntry pe = uidMap.valueAt(j); + if (pe.activeNesting > 0 || pe.activeTopNesting > 0 || pe.pendingNesting > 0) { + // Propagate existing activity in to next data set. + PackageEntry nextPe = next.getOrCreateEntry(mEntries.keyAt(i), uidMap.keyAt(j)); + nextPe.activeStartTime = now; + nextPe.activeNesting = pe.activeNesting; + nextPe.activeTopStartTime = now; + nextPe.activeTopNesting = pe.activeTopNesting; + nextPe.pendingStartTime = now; + nextPe.pendingNesting = pe.pendingNesting; + // Finish it off. + if (pe.activeNesting > 0) { + pe.pastActiveTime += now - pe.activeStartTime; + pe.activeNesting = 0; + } + if (pe.activeTopNesting > 0) { + pe.pastActiveTopTime += now - pe.activeTopStartTime; + pe.activeTopNesting = 0; + } + if (pe.pendingNesting > 0) { + pe.pastPendingTime += now - pe.pendingStartTime; + pe.pendingNesting = 0; + } + } + } + } + } + + void addTo(DataSet out, long now) { + out.mSummedTime += getTotalTime(now); + for (int i = mEntries.size() - 1; i >= 0; i--) { + ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i); + for (int j = uidMap.size() - 1; j >= 0; j--) { + PackageEntry pe = uidMap.valueAt(j); + PackageEntry outPe = out.getOrCreateEntry(mEntries.keyAt(i), uidMap.keyAt(j)); + outPe.pastActiveTime += pe.pastActiveTime; + outPe.activeCount += pe.activeCount; + outPe.pastActiveTopTime += pe.pastActiveTopTime; + outPe.activeTopCount += pe.activeTopCount; + outPe.pastPendingTime += pe.pastPendingTime; + outPe.pendingCount += pe.pendingCount; + if (pe.activeNesting > 0) { + outPe.pastActiveTime += now - pe.activeStartTime; + outPe.hadActive = true; + } + if (pe.activeTopNesting > 0) { + outPe.pastActiveTopTime += now - pe.activeTopStartTime; + outPe.hadActiveTop = true; + } + if (pe.pendingNesting > 0) { + outPe.pastPendingTime += now - pe.pendingStartTime; + outPe.hadPending = true; + } + for (int k = pe.stopReasons.size()-1; k >= 0; k--) { + int type = pe.stopReasons.keyAt(k); + outPe.stopReasons.put(type, outPe.stopReasons.get(type, 0) + + pe.stopReasons.valueAt(k)); + } + } + } + if (mMaxTotalActive > out.mMaxTotalActive) { + out.mMaxTotalActive = mMaxTotalActive; + } + if (mMaxFgActive > out.mMaxFgActive) { + out.mMaxFgActive = mMaxFgActive; + } + } + + void printDuration(PrintWriter pw, long period, long duration, int count, String suffix) { + float fraction = duration / (float) period; + int percent = (int) ((fraction * 100) + .5f); + if (percent > 0) { + pw.print(" "); + pw.print(percent); + pw.print("% "); + pw.print(count); + pw.print("x "); + pw.print(suffix); + } else if (count > 0) { + pw.print(" "); + pw.print(count); + pw.print("x "); + pw.print(suffix); + } + } + + void dump(PrintWriter pw, String header, String prefix, long now, long nowElapsed, + int filterUid) { + final long period = getTotalTime(now); + pw.print(prefix); pw.print(header); pw.print(" at "); + pw.print(DateFormat.format("yyyy-MM-dd-HH-mm-ss", mStartClockTime).toString()); + pw.print(" ("); + TimeUtils.formatDuration(mStartElapsedTime, nowElapsed, pw); + pw.print(") over "); + TimeUtils.formatDuration(period, pw); + pw.println(":"); + final int NE = mEntries.size(); + for (int i = 0; i < NE; i++) { + int uid = mEntries.keyAt(i); + if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) { + continue; + } + ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i); + final int NP = uidMap.size(); + for (int j = 0; j < NP; j++) { + PackageEntry pe = uidMap.valueAt(j); + pw.print(prefix); pw.print(" "); + UserHandle.formatUid(pw, uid); + pw.print(" / "); pw.print(uidMap.keyAt(j)); + pw.println(":"); + pw.print(prefix); pw.print(" "); + printDuration(pw, period, pe.getPendingTime(now), pe.pendingCount, "pending"); + printDuration(pw, period, pe.getActiveTime(now), pe.activeCount, "active"); + printDuration(pw, period, pe.getActiveTopTime(now), pe.activeTopCount, + "active-top"); + if (pe.pendingNesting > 0 || pe.hadPending) { + pw.print(" (pending)"); + } + if (pe.activeNesting > 0 || pe.hadActive) { + pw.print(" (active)"); + } + if (pe.activeTopNesting > 0 || pe.hadActiveTop) { + pw.print(" (active-top)"); + } + pw.println(); + if (pe.stopReasons.size() > 0) { + pw.print(prefix); pw.print(" "); + for (int k = 0; k < pe.stopReasons.size(); k++) { + if (k > 0) { + pw.print(", "); + } + pw.print(pe.stopReasons.valueAt(k)); + pw.print("x "); + pw.print(JobParameters + .getReasonCodeDescription(pe.stopReasons.keyAt(k))); + } + pw.println(); + } + } + } + pw.print(prefix); pw.print(" Max concurrency: "); + pw.print(mMaxTotalActive); pw.print(" total, "); + pw.print(mMaxFgActive); pw.println(" foreground"); + } + + private void printPackageEntryState(ProtoOutputStream proto, long fieldId, + long duration, int count) { + final long token = proto.start(fieldId); + proto.write(DataSetProto.PackageEntryProto.State.DURATION_MS, duration); + proto.write(DataSetProto.PackageEntryProto.State.COUNT, count); + proto.end(token); + } + + void dump(ProtoOutputStream proto, long fieldId, long now, long nowElapsed, int filterUid) { + final long token = proto.start(fieldId); + final long period = getTotalTime(now); + + proto.write(DataSetProto.START_CLOCK_TIME_MS, mStartClockTime); + proto.write(DataSetProto.ELAPSED_TIME_MS, nowElapsed - mStartElapsedTime); + proto.write(DataSetProto.PERIOD_MS, period); + + final int NE = mEntries.size(); + for (int i = 0; i < NE; i++) { + int uid = mEntries.keyAt(i); + if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) { + continue; + } + ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i); + final int NP = uidMap.size(); + for (int j = 0; j < NP; j++) { + final long peToken = proto.start(DataSetProto.PACKAGE_ENTRIES); + PackageEntry pe = uidMap.valueAt(j); + + proto.write(DataSetProto.PackageEntryProto.UID, uid); + proto.write(DataSetProto.PackageEntryProto.PACKAGE_NAME, uidMap.keyAt(j)); + + printPackageEntryState(proto, DataSetProto.PackageEntryProto.PENDING_STATE, + pe.getPendingTime(now), pe.pendingCount); + printPackageEntryState(proto, DataSetProto.PackageEntryProto.ACTIVE_STATE, + pe.getActiveTime(now), pe.activeCount); + printPackageEntryState(proto, DataSetProto.PackageEntryProto.ACTIVE_TOP_STATE, + pe.getActiveTopTime(now), pe.activeTopCount); + + proto.write(DataSetProto.PackageEntryProto.PENDING, + pe.pendingNesting > 0 || pe.hadPending); + proto.write(DataSetProto.PackageEntryProto.ACTIVE, + pe.activeNesting > 0 || pe.hadActive); + proto.write(DataSetProto.PackageEntryProto.ACTIVE_TOP, + pe.activeTopNesting > 0 || pe.hadActiveTop); + + for (int k = 0; k < pe.stopReasons.size(); k++) { + final long srcToken = + proto.start(DataSetProto.PackageEntryProto.STOP_REASONS); + + proto.write(DataSetProto.PackageEntryProto.StopReasonCount.REASON, + pe.stopReasons.keyAt(k)); + proto.write(DataSetProto.PackageEntryProto.StopReasonCount.COUNT, + pe.stopReasons.valueAt(k)); + + proto.end(srcToken); + } + + proto.end(peToken); + } + } + + proto.write(DataSetProto.MAX_CONCURRENCY, mMaxTotalActive); + proto.write(DataSetProto.MAX_FOREGROUND_CONCURRENCY, mMaxFgActive); + + proto.end(token); + } + } + + void rebatchIfNeeded(long now) { + long totalTime = mCurDataSet.getTotalTime(now); + if (totalTime > BATCHING_TIME) { + DataSet last = mCurDataSet; + last.mSummedTime = totalTime; + mCurDataSet = new DataSet(); + last.finish(mCurDataSet, now); + System.arraycopy(mLastDataSets, 0, mLastDataSets, 1, mLastDataSets.length-1); + mLastDataSets[0] = last; + } + } + + public void notePending(JobStatus job) { + final long now = sUptimeMillisClock.millis(); + job.madePending = now; + rebatchIfNeeded(now); + mCurDataSet.incPending(job.getSourceUid(), job.getSourcePackageName(), now); + } + + public void noteNonpending(JobStatus job) { + final long now = sUptimeMillisClock.millis(); + mCurDataSet.decPending(job.getSourceUid(), job.getSourcePackageName(), now); + rebatchIfNeeded(now); + } + + public void noteActive(JobStatus job) { + final long now = sUptimeMillisClock.millis(); + job.madeActive = now; + rebatchIfNeeded(now); + if (job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) { + mCurDataSet.incActiveTop(job.getSourceUid(), job.getSourcePackageName(), now); + } else { + mCurDataSet.incActive(job.getSourceUid(), job.getSourcePackageName(), now); + } + addEvent(job.getJob().isPeriodic() ? EVENT_START_PERIODIC_JOB : EVENT_START_JOB, + job.getSourceUid(), job.getBatteryName(), job.getJobId(), 0, null); + } + + public void noteInactive(JobStatus job, int stopReason, String debugReason) { + final long now = sUptimeMillisClock.millis(); + if (job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) { + mCurDataSet.decActiveTop(job.getSourceUid(), job.getSourcePackageName(), now, + stopReason); + } else { + mCurDataSet.decActive(job.getSourceUid(), job.getSourcePackageName(), now, stopReason); + } + rebatchIfNeeded(now); + addEvent(job.getJob().isPeriodic() ? EVENT_STOP_JOB : EVENT_STOP_PERIODIC_JOB, + job.getSourceUid(), job.getBatteryName(), job.getJobId(), stopReason, debugReason); + } + + public void noteConcurrency(int totalActive, int fgActive) { + if (totalActive > mCurDataSet.mMaxTotalActive) { + mCurDataSet.mMaxTotalActive = totalActive; + } + if (fgActive > mCurDataSet.mMaxFgActive) { + mCurDataSet.mMaxFgActive = fgActive; + } + } + + public float getLoadFactor(JobStatus job) { + final int uid = job.getSourceUid(); + final String pkg = job.getSourcePackageName(); + PackageEntry cur = mCurDataSet.getEntry(uid, pkg); + PackageEntry last = mLastDataSets[0] != null ? mLastDataSets[0].getEntry(uid, pkg) : null; + if (cur == null && last == null) { + return 0; + } + final long now = sUptimeMillisClock.millis(); + long time = 0; + if (cur != null) { + time += cur.getActiveTime(now) + cur.getPendingTime(now); + } + long period = mCurDataSet.getTotalTime(now); + if (last != null) { + time += last.getActiveTime(now) + last.getPendingTime(now); + period += mLastDataSets[0].getTotalTime(now); + } + return time / (float)period; + } + + public void dump(PrintWriter pw, String prefix, int filterUid) { + final long now = sUptimeMillisClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + final DataSet total; + if (mLastDataSets[0] != null) { + total = new DataSet(mLastDataSets[0]); + mLastDataSets[0].addTo(total, now); + } else { + total = new DataSet(mCurDataSet); + } + mCurDataSet.addTo(total, now); + for (int i = 1; i < mLastDataSets.length; i++) { + if (mLastDataSets[i] != null) { + mLastDataSets[i].dump(pw, "Historical stats", prefix, now, nowElapsed, filterUid); + pw.println(); + } + } + total.dump(pw, "Current stats", prefix, now, nowElapsed, filterUid); + } + + public void dump(ProtoOutputStream proto, long fieldId, int filterUid) { + final long token = proto.start(fieldId); + final long now = sUptimeMillisClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + + final DataSet total; + if (mLastDataSets[0] != null) { + total = new DataSet(mLastDataSets[0]); + mLastDataSets[0].addTo(total, now); + } else { + total = new DataSet(mCurDataSet); + } + mCurDataSet.addTo(total, now); + + for (int i = 1; i < mLastDataSets.length; i++) { + if (mLastDataSets[i] != null) { + mLastDataSets[i].dump(proto, JobPackageTrackerDumpProto.HISTORICAL_STATS, + now, nowElapsed, filterUid); + } + } + total.dump(proto, JobPackageTrackerDumpProto.CURRENT_STATS, + now, nowElapsed, filterUid); + + proto.end(token); + } + + public boolean dumpHistory(PrintWriter pw, String prefix, int filterUid) { + final int size = mEventIndices.size(); + if (size <= 0) { + return false; + } + pw.println(" Job history:"); + final long now = sElapsedRealtimeClock.millis(); + for (int i=0; i<size; i++) { + final int index = mEventIndices.indexOf(i); + final int uid = mEventUids[index]; + if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) { + continue; + } + final int cmd = mEventCmds[index] & EVENT_CMD_MASK; + if (cmd == EVENT_NULL) { + continue; + } + final String label; + switch (cmd) { + case EVENT_START_JOB: label = " START"; break; + case EVENT_STOP_JOB: label = " STOP"; break; + case EVENT_START_PERIODIC_JOB: label = "START-P"; break; + case EVENT_STOP_PERIODIC_JOB: label = " STOP-P"; break; + default: label = " ??"; break; + } + pw.print(prefix); + TimeUtils.formatDuration(mEventTimes[index]-now, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN); + pw.print(" "); + pw.print(label); + pw.print(": #"); + UserHandle.formatUid(pw, uid); + pw.print("/"); + pw.print(mEventJobIds[index]); + pw.print(" "); + pw.print(mEventTags[index]); + if (cmd == EVENT_STOP_JOB || cmd == EVENT_STOP_PERIODIC_JOB) { + pw.print(" "); + final String reason = mEventReasons[index]; + if (reason != null) { + pw.print(mEventReasons[index]); + } else { + pw.print(JobParameters.getReasonCodeDescription( + (mEventCmds[index] & EVENT_STOP_REASON_MASK) + >> EVENT_STOP_REASON_SHIFT)); + } + } + pw.println(); + } + return true; + } + + public void dumpHistory(ProtoOutputStream proto, long fieldId, int filterUid) { + final int size = mEventIndices.size(); + if (size == 0) { + return; + } + final long token = proto.start(fieldId); + + final long now = sElapsedRealtimeClock.millis(); + for (int i = 0; i < size; i++) { + final int index = mEventIndices.indexOf(i); + final int uid = mEventUids[index]; + if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) { + continue; + } + final int cmd = mEventCmds[index] & EVENT_CMD_MASK; + if (cmd == EVENT_NULL) { + continue; + } + final long heToken = proto.start(JobPackageHistoryProto.HISTORY_EVENT); + + proto.write(JobPackageHistoryProto.HistoryEvent.EVENT, cmd); + proto.write(JobPackageHistoryProto.HistoryEvent.TIME_SINCE_EVENT_MS, now - mEventTimes[index]); + proto.write(JobPackageHistoryProto.HistoryEvent.UID, uid); + proto.write(JobPackageHistoryProto.HistoryEvent.JOB_ID, mEventJobIds[index]); + proto.write(JobPackageHistoryProto.HistoryEvent.TAG, mEventTags[index]); + if (cmd == EVENT_STOP_JOB || cmd == EVENT_STOP_PERIODIC_JOB) { + proto.write(JobPackageHistoryProto.HistoryEvent.STOP_REASON, + (mEventCmds[index] & EVENT_STOP_REASON_MASK) >> EVENT_STOP_REASON_SHIFT); + } + + proto.end(heToken); + } + + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java new file mode 100644 index 000000000000..871e40fc9dfe --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -0,0 +1,3492 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job; + +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.app.AppGlobals; +import android.app.IUidObserver; +import android.app.job.IJobScheduler; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobProtoEnums; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.app.job.JobSnapshot; +import android.app.job.JobWorkItem; +import android.app.usage.UsageStatsManager; +import android.app.usage.UsageStatsManagerInternal; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ParceledListSlice; +import android.content.pm.ServiceInfo; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.BatteryStats; +import android.os.BatteryStatsInternal; +import android.os.Binder; +import android.os.Handler; +import android.os.LimitExceededException; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.UserManagerInternal; +import android.os.WorkSource; +import android.provider.Settings; +import android.text.format.DateUtils; +import android.util.ArrayMap; +import android.util.KeyValueListParser; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IBatteryStats; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.DumpUtils; +import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.AppStateTracker; +import com.android.server.DeviceIdleInternal; +import com.android.server.FgThread; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerServiceDumpProto.ActiveJob; +import com.android.server.job.JobSchedulerServiceDumpProto.PendingJob; +import com.android.server.job.controllers.BackgroundJobsController; +import com.android.server.job.controllers.BatteryController; +import com.android.server.job.controllers.ConnectivityController; +import com.android.server.job.controllers.ContentObserverController; +import com.android.server.job.controllers.DeviceIdleJobsController; +import com.android.server.job.controllers.IdleController; +import com.android.server.job.controllers.JobStatus; +import com.android.server.job.controllers.QuotaController; +import com.android.server.job.controllers.RestrictingController; +import com.android.server.job.controllers.StateController; +import com.android.server.job.controllers.StorageController; +import com.android.server.job.controllers.TimeController; +import com.android.server.job.restrictions.JobRestriction; +import com.android.server.job.restrictions.ThermalStatusRestriction; +import com.android.server.usage.AppStandbyInternal; +import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; +import com.android.server.utils.quota.Categorizer; +import com.android.server.utils.quota.Category; +import com.android.server.utils.quota.CountQuotaTracker; + +import libcore.util.EmptyArray; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Responsible for taking jobs representing work to be performed by a client app, and determining + * based on the criteria specified when that job should be run against the client application's + * endpoint. + * Implements logic for scheduling, and rescheduling jobs. The JobSchedulerService knows nothing + * about constraints, or the state of active jobs. It receives callbacks from the various + * controllers and completed jobs and operates accordingly. + * + * Note on locking: Any operations that manipulate {@link #mJobs} need to lock on that object. + * Any function with the suffix 'Locked' also needs to lock on {@link #mJobs}. + * @hide + */ +public class JobSchedulerService extends com.android.server.SystemService + implements StateChangedListener, JobCompletedListener { + public static final String TAG = "JobScheduler"; + public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + public static final boolean DEBUG_STANDBY = DEBUG || false; + + /** The maximum number of concurrent jobs we run at one time. */ + static final int MAX_JOB_CONTEXTS_COUNT = 16; + /** Enforce a per-app limit on scheduled jobs? */ + private static final boolean ENFORCE_MAX_JOBS = true; + /** The maximum number of jobs that we allow an unprivileged app to schedule */ + private static final int MAX_JOBS_PER_APP = 100; + + @VisibleForTesting + public static Clock sSystemClock = Clock.systemUTC(); + + private abstract static class MySimpleClock extends Clock { + private final ZoneId mZoneId; + + MySimpleClock(ZoneId zoneId) { + this.mZoneId = zoneId; + } + + @Override + public ZoneId getZone() { + return mZoneId; + } + + @Override + public Clock withZone(ZoneId zone) { + return new MySimpleClock(zone) { + @Override + public long millis() { + return MySimpleClock.this.millis(); + } + }; + } + + @Override + public abstract long millis(); + + @Override + public Instant instant() { + return Instant.ofEpochMilli(millis()); + } + } + + @VisibleForTesting + public static Clock sUptimeMillisClock = new MySimpleClock(ZoneOffset.UTC) { + @Override + public long millis() { + return SystemClock.uptimeMillis(); + } + }; + + @VisibleForTesting + public static Clock sElapsedRealtimeClock = new MySimpleClock(ZoneOffset.UTC) { + @Override + public long millis() { + return SystemClock.elapsedRealtime(); + } + }; + + /** Global local for all job scheduler state. */ + final Object mLock = new Object(); + /** Master list of jobs. */ + final JobStore mJobs; + /** Tracking the standby bucket state of each app */ + final StandbyTracker mStandbyTracker; + /** Tracking amount of time each package runs for. */ + final JobPackageTracker mJobPackageTracker = new JobPackageTracker(); + final JobConcurrencyManager mConcurrencyManager; + + static final int MSG_JOB_EXPIRED = 0; + static final int MSG_CHECK_JOB = 1; + static final int MSG_STOP_JOB = 2; + static final int MSG_CHECK_JOB_GREEDY = 3; + static final int MSG_UID_STATE_CHANGED = 4; + static final int MSG_UID_GONE = 5; + static final int MSG_UID_ACTIVE = 6; + static final int MSG_UID_IDLE = 7; + + /** + * Track Services that have currently active or pending jobs. The index is provided by + * {@link JobStatus#getServiceToken()} + */ + final List<JobServiceContext> mActiveServices = new ArrayList<>(); + + /** List of controllers that will notify this service of updates to jobs. */ + final List<StateController> mControllers; + /** + * List of controllers that will apply to all jobs in the RESTRICTED bucket. This is a subset of + * {@link #mControllers}. + */ + private final List<RestrictingController> mRestrictiveControllers; + /** Need direct access to this for testing. */ + private final BatteryController mBatteryController; + /** Need direct access to this for testing. */ + private final StorageController mStorageController; + /** Need directly for sending uid state changes */ + private final DeviceIdleJobsController mDeviceIdleJobsController; + /** Needed to get remaining quota time. */ + private final QuotaController mQuotaController; + /** + * List of restrictions. + * Note: do not add to or remove from this list at runtime except in the constructor, because we + * do not synchronize access to this list. + */ + private final List<JobRestriction> mJobRestrictions; + + @NonNull + private final String mSystemGalleryPackage; + + private final CountQuotaTracker mQuotaTracker; + private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()"; + private static final String QUOTA_TRACKER_SCHEDULE_LOGGED = + ".schedulePersisted out-of-quota logged"; + private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED = new Category( + ".schedulePersisted()"); + private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED = new Category( + ".schedulePersisted out-of-quota logged"); + private static final Categorizer QUOTA_CATEGORIZER = (userId, packageName, tag) -> { + if (QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG.equals(tag)) { + return QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED; + } + return QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED; + }; + + /** + * Queue of pending jobs. The JobServiceContext class will receive jobs from this list + * when ready to execute them. + */ + final ArrayList<JobStatus> mPendingJobs = new ArrayList<>(); + + int[] mStartedUsers = EmptyArray.INT; + + final JobHandler mHandler; + final JobSchedulerStub mJobSchedulerStub; + + PackageManagerInternal mLocalPM; + ActivityManagerInternal mActivityManagerInternal; + IBatteryStats mBatteryStats; + DeviceIdleInternal mLocalDeviceIdleController; + @VisibleForTesting + AppStateTracker mAppStateTracker; + final UsageStatsManagerInternal mUsageStats; + private final AppStandbyInternal mAppStandbyInternal; + + /** + * Set to true once we are allowed to run third party apps. + */ + boolean mReadyToRock; + + /** + * What we last reported to DeviceIdleController about whether we are active. + */ + boolean mReportedActive; + + /** + * A mapping of which uids are currently in the foreground to their effective priority. + */ + final SparseIntArray mUidPriorityOverride = new SparseIntArray(); + + /** + * Which uids are currently performing backups, so we shouldn't allow their jobs to run. + */ + final SparseIntArray mBackingUpUids = new SparseIntArray(); + + /** + * Cache of debuggable app status. + */ + final ArrayMap<String, Boolean> mDebuggableApps = new ArrayMap<>(); + + /** + * Named indices into standby bucket arrays, for clarity in referring to + * specific buckets' bookkeeping. + */ + public static final int ACTIVE_INDEX = 0; + public static final int WORKING_INDEX = 1; + public static final int FREQUENT_INDEX = 2; + public static final int RARE_INDEX = 3; + public static final int NEVER_INDEX = 4; + // Putting RESTRICTED_INDEX after NEVER_INDEX to make it easier for proto dumping + // (ScheduledJobStateChanged and JobStatusDumpProto). + public static final int RESTRICTED_INDEX = 5; + + // -- Pre-allocated temporaries only for use in assignJobsToContextsLocked -- + + private class ConstantsObserver extends ContentObserver { + private ContentResolver mResolver; + + public ConstantsObserver(Handler handler) { + super(handler); + } + + public void start(ContentResolver resolver) { + mResolver = resolver; + mResolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.JOB_SCHEDULER_CONSTANTS), false, this); + updateConstants(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + updateConstants(); + } + + private void updateConstants() { + synchronized (mLock) { + try { + mConstants.updateConstantsLocked(Settings.Global.getString(mResolver, + Settings.Global.JOB_SCHEDULER_CONSTANTS)); + for (int controller = 0; controller < mControllers.size(); controller++) { + final StateController sc = mControllers.get(controller); + sc.onConstantsUpdatedLocked(); + } + updateQuotaTracker(); + } catch (IllegalArgumentException e) { + // Failed to parse the settings string, log this and move on + // with defaults. + Slog.e(TAG, "Bad jobscheduler settings", e); + } + } + } + } + + @VisibleForTesting + void updateQuotaTracker() { + mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, + mConstants.API_QUOTA_SCHEDULE_COUNT, + mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); + } + + static class MaxJobCounts { + private final KeyValueListParser.IntValue mTotal; + private final KeyValueListParser.IntValue mMaxBg; + private final KeyValueListParser.IntValue mMinBg; + + MaxJobCounts(int totalDefault, String totalKey, + int maxBgDefault, String maxBgKey, int minBgDefault, String minBgKey) { + mTotal = new KeyValueListParser.IntValue(totalKey, totalDefault); + mMaxBg = new KeyValueListParser.IntValue(maxBgKey, maxBgDefault); + mMinBg = new KeyValueListParser.IntValue(minBgKey, minBgDefault); + } + + public void parse(KeyValueListParser parser) { + mTotal.parse(parser); + mMaxBg.parse(parser); + mMinBg.parse(parser); + + if (mTotal.getValue() < 1) { + mTotal.setValue(1); + } else if (mTotal.getValue() > MAX_JOB_CONTEXTS_COUNT) { + mTotal.setValue(MAX_JOB_CONTEXTS_COUNT); + } + + if (mMaxBg.getValue() < 1) { + mMaxBg.setValue(1); + } else if (mMaxBg.getValue() > mTotal.getValue()) { + mMaxBg.setValue(mTotal.getValue()); + } + if (mMinBg.getValue() < 0) { + mMinBg.setValue(0); + } else { + if (mMinBg.getValue() > mMaxBg.getValue()) { + mMinBg.setValue(mMaxBg.getValue()); + } + if (mMinBg.getValue() >= mTotal.getValue()) { + mMinBg.setValue(mTotal.getValue() - 1); + } + } + } + + /** Total number of jobs to run simultaneously. */ + public int getMaxTotal() { + return mTotal.getValue(); + } + + /** Max number of BG (== owned by non-TOP apps) jobs to run simultaneously. */ + public int getMaxBg() { + return mMaxBg.getValue(); + } + + /** + * We try to run at least this many BG (== owned by non-TOP apps) jobs, when there are any + * pending, rather than always running the TOTAL number of FG jobs. + */ + public int getMinBg() { + return mMinBg.getValue(); + } + + public void dump(PrintWriter pw, String prefix) { + mTotal.dump(pw, prefix); + mMaxBg.dump(pw, prefix); + mMinBg.dump(pw, prefix); + } + + public void dumpProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + mTotal.dumpProto(proto, MaxJobCountsProto.TOTAL_JOBS); + mMaxBg.dumpProto(proto, MaxJobCountsProto.MAX_BG); + mMinBg.dumpProto(proto, MaxJobCountsProto.MIN_BG); + proto.end(token); + } + } + + /** {@link MaxJobCounts} for each memory trim level. */ + static class MaxJobCountsPerMemoryTrimLevel { + public final MaxJobCounts normal; + public final MaxJobCounts moderate; + public final MaxJobCounts low; + public final MaxJobCounts critical; + + MaxJobCountsPerMemoryTrimLevel( + MaxJobCounts normal, + MaxJobCounts moderate, MaxJobCounts low, + MaxJobCounts critical) { + this.normal = normal; + this.moderate = moderate; + this.low = low; + this.critical = critical; + } + + public void dumpProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + normal.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.NORMAL); + moderate.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.MODERATE); + low.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.LOW); + critical.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.CRITICAL); + proto.end(token); + } + } + + /** + * All times are in milliseconds. These constants are kept synchronized with the system + * global Settings. Any access to this class or its fields should be done while + * holding the JobSchedulerService.mLock lock. + */ + public static class Constants { + // Key names stored in the settings value. + // TODO(124466289): remove deprecated flags when we migrate to DeviceConfig + private static final String DEPRECATED_KEY_MIN_IDLE_COUNT = "min_idle_count"; + private static final String DEPRECATED_KEY_MIN_CHARGING_COUNT = "min_charging_count"; + private static final String DEPRECATED_KEY_MIN_BATTERY_NOT_LOW_COUNT = + "min_battery_not_low_count"; + private static final String DEPRECATED_KEY_MIN_STORAGE_NOT_LOW_COUNT = + "min_storage_not_low_count"; + private static final String DEPRECATED_KEY_MIN_CONNECTIVITY_COUNT = + "min_connectivity_count"; + private static final String DEPRECATED_KEY_MIN_CONTENT_COUNT = "min_content_count"; + private static final String DEPRECATED_KEY_MIN_READY_JOBS_COUNT = "min_ready_jobs_count"; + private static final String KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT = + "min_ready_non_active_jobs_count"; + private static final String KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = + "max_non_active_job_batch_delay_ms"; + private static final String KEY_HEAVY_USE_FACTOR = "heavy_use_factor"; + private static final String KEY_MODERATE_USE_FACTOR = "moderate_use_factor"; + + // The following values used to be used on P and below. Do not reuse them. + private static final String DEPRECATED_KEY_FG_JOB_COUNT = "fg_job_count"; + private static final String DEPRECATED_KEY_BG_NORMAL_JOB_COUNT = "bg_normal_job_count"; + private static final String DEPRECATED_KEY_BG_MODERATE_JOB_COUNT = "bg_moderate_job_count"; + private static final String DEPRECATED_KEY_BG_LOW_JOB_COUNT = "bg_low_job_count"; + private static final String DEPRECATED_KEY_BG_CRITICAL_JOB_COUNT = "bg_critical_job_count"; + + private static final String DEPRECATED_KEY_MAX_STANDARD_RESCHEDULE_COUNT + = "max_standard_reschedule_count"; + private static final String DEPRECATED_KEY_MAX_WORK_RESCHEDULE_COUNT = + "max_work_reschedule_count"; + private static final String KEY_MIN_LINEAR_BACKOFF_TIME = "min_linear_backoff_time"; + private static final String KEY_MIN_EXP_BACKOFF_TIME = "min_exp_backoff_time"; + private static final String DEPRECATED_KEY_STANDBY_HEARTBEAT_TIME = + "standby_heartbeat_time"; + private static final String DEPRECATED_KEY_STANDBY_WORKING_BEATS = "standby_working_beats"; + private static final String DEPRECATED_KEY_STANDBY_FREQUENT_BEATS = + "standby_frequent_beats"; + private static final String DEPRECATED_KEY_STANDBY_RARE_BEATS = "standby_rare_beats"; + private static final String KEY_CONN_CONGESTION_DELAY_FRAC = "conn_congestion_delay_frac"; + private static final String KEY_CONN_PREFETCH_RELAX_FRAC = "conn_prefetch_relax_frac"; + private static final String DEPRECATED_KEY_USE_HEARTBEATS = "use_heartbeats"; + private static final String KEY_ENABLE_API_QUOTAS = "enable_api_quotas"; + private static final String KEY_API_QUOTA_SCHEDULE_COUNT = "aq_schedule_count"; + private static final String KEY_API_QUOTA_SCHEDULE_WINDOW_MS = "aq_schedule_window_ms"; + private static final String KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION = + "aq_schedule_throw_exception"; + private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = + "aq_schedule_return_failure"; + + private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5; + private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS; + private static final float DEFAULT_HEAVY_USE_FACTOR = .9f; + private static final float DEFAULT_MODERATE_USE_FACTOR = .5f; + private static final long DEFAULT_MIN_LINEAR_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS; + private static final long DEFAULT_MIN_EXP_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS; + private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f; + private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f; + private static final boolean DEFAULT_ENABLE_API_QUOTAS = true; + private static final int DEFAULT_API_QUOTA_SCHEDULE_COUNT = 250; + private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS; + private static final boolean DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION = true; + private static final boolean DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; + + /** + * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early. + */ + int MIN_READY_NON_ACTIVE_JOBS_COUNT = DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT; + + /** + * Don't batch a non-ACTIVE job if it's been delayed due to force batching attempts for + * at least this amount of time. + */ + long MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS; + + /** + * This is the job execution factor that is considered to be heavy use of the system. + */ + float HEAVY_USE_FACTOR = DEFAULT_HEAVY_USE_FACTOR; + /** + * This is the job execution factor that is considered to be moderate use of the system. + */ + float MODERATE_USE_FACTOR = DEFAULT_MODERATE_USE_FACTOR; + + // Max job counts for screen on / off, for each memory trim level. + final MaxJobCountsPerMemoryTrimLevel MAX_JOB_COUNTS_SCREEN_ON = + new MaxJobCountsPerMemoryTrimLevel( + new MaxJobCounts( + 8, "max_job_total_on_normal", + 6, "max_job_max_bg_on_normal", + 2, "max_job_min_bg_on_normal"), + new MaxJobCounts( + 8, "max_job_total_on_moderate", + 4, "max_job_max_bg_on_moderate", + 2, "max_job_min_bg_on_moderate"), + new MaxJobCounts( + 5, "max_job_total_on_low", + 1, "max_job_max_bg_on_low", + 1, "max_job_min_bg_on_low"), + new MaxJobCounts( + 5, "max_job_total_on_critical", + 1, "max_job_max_bg_on_critical", + 1, "max_job_min_bg_on_critical")); + + final MaxJobCountsPerMemoryTrimLevel MAX_JOB_COUNTS_SCREEN_OFF = + new MaxJobCountsPerMemoryTrimLevel( + new MaxJobCounts( + 10, "max_job_total_off_normal", + 6, "max_job_max_bg_off_normal", + 2, "max_job_min_bg_off_normal"), + new MaxJobCounts( + 10, "max_job_total_off_moderate", + 4, "max_job_max_bg_off_moderate", + 2, "max_job_min_bg_off_moderate"), + new MaxJobCounts( + 5, "max_job_total_off_low", + 1, "max_job_max_bg_off_low", + 1, "max_job_min_bg_off_low"), + new MaxJobCounts( + 5, "max_job_total_off_critical", + 1, "max_job_max_bg_off_critical", + 1, "max_job_min_bg_off_critical")); + + + /** Wait for this long after screen off before increasing the job concurrency. */ + final KeyValueListParser.IntValue SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS = + new KeyValueListParser.IntValue( + "screen_off_job_concurrency_increase_delay_ms", 30_000); + + /** + * The minimum backoff time to allow for linear backoff. + */ + long MIN_LINEAR_BACKOFF_TIME = DEFAULT_MIN_LINEAR_BACKOFF_TIME; + /** + * The minimum backoff time to allow for exponential backoff. + */ + long MIN_EXP_BACKOFF_TIME = DEFAULT_MIN_EXP_BACKOFF_TIME; + + /** + * The fraction of a job's running window that must pass before we + * consider running it when the network is congested. + */ + public float CONN_CONGESTION_DELAY_FRAC = DEFAULT_CONN_CONGESTION_DELAY_FRAC; + /** + * The fraction of a prefetch job's running window that must pass before + * we consider matching it against a metered network. + */ + public float CONN_PREFETCH_RELAX_FRAC = DEFAULT_CONN_PREFETCH_RELAX_FRAC; + + /** + * Whether to enable quota limits on APIs. + */ + public boolean ENABLE_API_QUOTAS = DEFAULT_ENABLE_API_QUOTAS; + /** + * The maximum number of schedule() calls an app can make in a set amount of time. + */ + public int API_QUOTA_SCHEDULE_COUNT = DEFAULT_API_QUOTA_SCHEDULE_COUNT; + /** + * The time window that {@link #API_QUOTA_SCHEDULE_COUNT} should be evaluated over. + */ + public long API_QUOTA_SCHEDULE_WINDOW_MS = DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS; + /** + * Whether to throw an exception when an app hits its schedule quota limit. + */ + public boolean API_QUOTA_SCHEDULE_THROW_EXCEPTION = + DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION; + /** + * Whether or not to return a failure result when an app hits its schedule quota limit. + */ + public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = + DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT; + + private final KeyValueListParser mParser = new KeyValueListParser(','); + + void updateConstantsLocked(String value) { + try { + mParser.setString(value); + } catch (Exception e) { + // Failed to parse the settings string, log this and move on + // with defaults. + Slog.e(TAG, "Bad jobscheduler settings", e); + } + + MIN_READY_NON_ACTIVE_JOBS_COUNT = mParser.getInt( + KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT, + DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT); + MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = mParser.getLong( + KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS, + DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS); + HEAVY_USE_FACTOR = mParser.getFloat(KEY_HEAVY_USE_FACTOR, + DEFAULT_HEAVY_USE_FACTOR); + MODERATE_USE_FACTOR = mParser.getFloat(KEY_MODERATE_USE_FACTOR, + DEFAULT_MODERATE_USE_FACTOR); + + MAX_JOB_COUNTS_SCREEN_ON.normal.parse(mParser); + MAX_JOB_COUNTS_SCREEN_ON.moderate.parse(mParser); + MAX_JOB_COUNTS_SCREEN_ON.low.parse(mParser); + MAX_JOB_COUNTS_SCREEN_ON.critical.parse(mParser); + + MAX_JOB_COUNTS_SCREEN_OFF.normal.parse(mParser); + MAX_JOB_COUNTS_SCREEN_OFF.moderate.parse(mParser); + MAX_JOB_COUNTS_SCREEN_OFF.low.parse(mParser); + MAX_JOB_COUNTS_SCREEN_OFF.critical.parse(mParser); + + SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.parse(mParser); + + MIN_LINEAR_BACKOFF_TIME = mParser.getDurationMillis(KEY_MIN_LINEAR_BACKOFF_TIME, + DEFAULT_MIN_LINEAR_BACKOFF_TIME); + MIN_EXP_BACKOFF_TIME = mParser.getDurationMillis(KEY_MIN_EXP_BACKOFF_TIME, + DEFAULT_MIN_EXP_BACKOFF_TIME); + CONN_CONGESTION_DELAY_FRAC = mParser.getFloat(KEY_CONN_CONGESTION_DELAY_FRAC, + DEFAULT_CONN_CONGESTION_DELAY_FRAC); + CONN_PREFETCH_RELAX_FRAC = mParser.getFloat(KEY_CONN_PREFETCH_RELAX_FRAC, + DEFAULT_CONN_PREFETCH_RELAX_FRAC); + + ENABLE_API_QUOTAS = mParser.getBoolean(KEY_ENABLE_API_QUOTAS, + DEFAULT_ENABLE_API_QUOTAS); + // Set a minimum value on the quota limit so it's not so low that it interferes with + // legitimate use cases. + API_QUOTA_SCHEDULE_COUNT = Math.max(250, + mParser.getInt(KEY_API_QUOTA_SCHEDULE_COUNT, DEFAULT_API_QUOTA_SCHEDULE_COUNT)); + API_QUOTA_SCHEDULE_WINDOW_MS = mParser.getDurationMillis( + KEY_API_QUOTA_SCHEDULE_WINDOW_MS, DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS); + API_QUOTA_SCHEDULE_THROW_EXCEPTION = mParser.getBoolean( + KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION, + DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION); + API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = mParser.getBoolean( + KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, + DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); + } + + void dump(IndentingPrintWriter pw) { + pw.println("Settings:"); + pw.increaseIndent(); + pw.printPair(KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT, + MIN_READY_NON_ACTIVE_JOBS_COUNT).println(); + pw.printPair(KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS, + MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS).println(); + pw.printPair(KEY_HEAVY_USE_FACTOR, HEAVY_USE_FACTOR).println(); + pw.printPair(KEY_MODERATE_USE_FACTOR, MODERATE_USE_FACTOR).println(); + + MAX_JOB_COUNTS_SCREEN_ON.normal.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_ON.moderate.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_ON.low.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_ON.critical.dump(pw, ""); + + MAX_JOB_COUNTS_SCREEN_OFF.normal.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_OFF.moderate.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_OFF.low.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_OFF.critical.dump(pw, ""); + + SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.dump(pw, ""); + + pw.printPair(KEY_MIN_LINEAR_BACKOFF_TIME, MIN_LINEAR_BACKOFF_TIME).println(); + pw.printPair(KEY_MIN_EXP_BACKOFF_TIME, MIN_EXP_BACKOFF_TIME).println(); + pw.printPair(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println(); + pw.printPair(KEY_CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC).println(); + + pw.printPair(KEY_ENABLE_API_QUOTAS, ENABLE_API_QUOTAS).println(); + pw.printPair(KEY_API_QUOTA_SCHEDULE_COUNT, API_QUOTA_SCHEDULE_COUNT).println(); + pw.printPair(KEY_API_QUOTA_SCHEDULE_WINDOW_MS, API_QUOTA_SCHEDULE_WINDOW_MS).println(); + pw.printPair(KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION, + API_QUOTA_SCHEDULE_THROW_EXCEPTION).println(); + pw.printPair(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, + API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println(); + + pw.decreaseIndent(); + } + + void dump(ProtoOutputStream proto) { + proto.write(ConstantsProto.MIN_READY_NON_ACTIVE_JOBS_COUNT, + MIN_READY_NON_ACTIVE_JOBS_COUNT); + proto.write(ConstantsProto.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS, + MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS); + proto.write(ConstantsProto.HEAVY_USE_FACTOR, HEAVY_USE_FACTOR); + proto.write(ConstantsProto.MODERATE_USE_FACTOR, MODERATE_USE_FACTOR); + + MAX_JOB_COUNTS_SCREEN_ON.dumpProto(proto, ConstantsProto.MAX_JOB_COUNTS_SCREEN_ON); + MAX_JOB_COUNTS_SCREEN_OFF.dumpProto(proto, ConstantsProto.MAX_JOB_COUNTS_SCREEN_OFF); + + SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.dumpProto(proto, + ConstantsProto.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS); + + proto.write(ConstantsProto.MIN_LINEAR_BACKOFF_TIME_MS, MIN_LINEAR_BACKOFF_TIME); + proto.write(ConstantsProto.MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME); + proto.write(ConstantsProto.CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC); + proto.write(ConstantsProto.CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC); + + proto.write(ConstantsProto.ENABLE_API_QUOTAS, ENABLE_API_QUOTAS); + proto.write(ConstantsProto.API_QUOTA_SCHEDULE_COUNT, API_QUOTA_SCHEDULE_COUNT); + proto.write(ConstantsProto.API_QUOTA_SCHEDULE_WINDOW_MS, API_QUOTA_SCHEDULE_WINDOW_MS); + proto.write(ConstantsProto.API_QUOTA_SCHEDULE_THROW_EXCEPTION, + API_QUOTA_SCHEDULE_THROW_EXCEPTION); + proto.write(ConstantsProto.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, + API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); + } + } + + final Constants mConstants; + final ConstantsObserver mConstantsObserver; + + private static final Comparator<JobStatus> sPendingJobComparator = (o1, o2) -> { + // Jobs with an override state set (via adb) should be put first as tests/developers + // expect the jobs to run immediately. + if (o1.overrideState != o2.overrideState) { + // Higher override state (OVERRIDE_FULL) should be before lower state (OVERRIDE_SOFT) + return o2.overrideState - o1.overrideState; + } + if (o1.enqueueTime < o2.enqueueTime) { + return -1; + } + return o1.enqueueTime > o2.enqueueTime ? 1 : 0; + }; + + static <T> void addOrderedItem(ArrayList<T> array, T newItem, Comparator<T> comparator) { + int where = Collections.binarySearch(array, newItem, comparator); + if (where < 0) { + where = ~where; + } + array.add(where, newItem); + } + + /** + * Cleans up outstanding jobs when a package is removed. Even if it's being replaced later we + * still clean up. On reinstall the package will have a new uid. + */ + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (DEBUG) { + Slog.d(TAG, "Receieved: " + action); + } + final String pkgName = getPackageName(intent); + final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); + + if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { + // Purge the app's jobs if the whole package was just disabled. When this is + // the case the component name will be a bare package name. + if (pkgName != null && pkgUid != -1) { + final String[] changedComponents = intent.getStringArrayExtra( + Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); + if (changedComponents != null) { + for (String component : changedComponents) { + if (component.equals(pkgName)) { + if (DEBUG) { + Slog.d(TAG, "Package state change: " + pkgName); + } + try { + final int userId = UserHandle.getUserId(pkgUid); + IPackageManager pm = AppGlobals.getPackageManager(); + final int state = pm.getApplicationEnabledSetting(pkgName, userId); + if (state == COMPONENT_ENABLED_STATE_DISABLED + || state == COMPONENT_ENABLED_STATE_DISABLED_USER) { + if (DEBUG) { + Slog.d(TAG, "Removing jobs for package " + pkgName + + " in user " + userId); + } + cancelJobsForPackageAndUid(pkgName, pkgUid, + "app disabled"); + } + } catch (RemoteException|IllegalArgumentException e) { + /* + * IllegalArgumentException means that the package doesn't exist. + * This arises when PACKAGE_CHANGED broadcast delivery has lagged + * behind outright uninstall, so by the time we try to act it's gone. + * We don't need to act on this PACKAGE_CHANGED when this happens; + * we'll get a PACKAGE_REMOVED later and clean up then. + * + * RemoteException can't actually happen; the package manager is + * running in this same process. + */ + } + break; + } + } + if (DEBUG) { + Slog.d(TAG, "Something in " + pkgName + + " changed. Reevaluating controller states."); + } + synchronized (mLock) { + for (int c = mControllers.size() - 1; c >= 0; --c) { + mControllers.get(c).reevaluateStateLocked(pkgUid); + } + } + } + } else { + Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid); + } + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + // If this is an outright uninstall rather than the first half of an + // app update sequence, cancel the jobs associated with the app. + if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + int uidRemoved = intent.getIntExtra(Intent.EXTRA_UID, -1); + if (DEBUG) { + Slog.d(TAG, "Removing jobs for uid: " + uidRemoved); + } + cancelJobsForPackageAndUid(pkgName, uidRemoved, "app uninstalled"); + synchronized (mLock) { + for (int c = 0; c < mControllers.size(); ++c) { + mControllers.get(c).onAppRemovedLocked(pkgName, pkgUid); + } + mDebuggableApps.remove(pkgName); + } + } + } else if (Intent.ACTION_USER_REMOVED.equals(action)) { + final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); + if (DEBUG) { + Slog.d(TAG, "Removing jobs for user: " + userId); + } + cancelJobsForUser(userId); + synchronized (mLock) { + for (int c = 0; c < mControllers.size(); ++c) { + mControllers.get(c).onUserRemovedLocked(userId); + } + } + } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) { + // Has this package scheduled any jobs, such that we will take action + // if it were to be force-stopped? + if (pkgUid != -1) { + List<JobStatus> jobsForUid; + synchronized (mLock) { + jobsForUid = mJobs.getJobsByUid(pkgUid); + } + for (int i = jobsForUid.size() - 1; i >= 0; i--) { + if (jobsForUid.get(i).getSourcePackageName().equals(pkgName)) { + if (DEBUG) { + Slog.d(TAG, "Restart query: package " + pkgName + " at uid " + + pkgUid + " has jobs"); + } + setResultCode(Activity.RESULT_OK); + break; + } + } + } + } else if (Intent.ACTION_PACKAGE_RESTARTED.equals(action)) { + // possible force-stop + if (pkgUid != -1) { + if (DEBUG) { + Slog.d(TAG, "Removing jobs for pkg " + pkgName + " at uid " + pkgUid); + } + cancelJobsForPackageAndUid(pkgName, pkgUid, "app force stopped"); + } + } + } + }; + + private String getPackageName(Intent intent) { + Uri uri = intent.getData(); + String pkg = uri != null ? uri.getSchemeSpecificPart() : null; + return pkg; + } + + final private IUidObserver mUidObserver = new IUidObserver.Stub() { + @Override public void onUidStateChanged(int uid, int procState, long procStateSeq, + int capability) { + mHandler.obtainMessage(MSG_UID_STATE_CHANGED, uid, procState).sendToTarget(); + } + + @Override public void onUidGone(int uid, boolean disabled) { + mHandler.obtainMessage(MSG_UID_GONE, uid, disabled ? 1 : 0).sendToTarget(); + } + + @Override public void onUidActive(int uid) throws RemoteException { + mHandler.obtainMessage(MSG_UID_ACTIVE, uid, 0).sendToTarget(); + } + + @Override public void onUidIdle(int uid, boolean disabled) { + mHandler.obtainMessage(MSG_UID_IDLE, uid, disabled ? 1 : 0).sendToTarget(); + } + + @Override public void onUidCachedChanged(int uid, boolean cached) { + } + }; + + public Context getTestableContext() { + return getContext(); + } + + public Object getLock() { + return mLock; + } + + public JobStore getJobStore() { + return mJobs; + } + + public Constants getConstants() { + return mConstants; + } + + public boolean isChainedAttributionEnabled() { + return WorkSource.isChainedBatteryAttributionEnabled(getContext()); + } + + @Override + public void onStartUser(int userHandle) { + synchronized (mLock) { + mStartedUsers = ArrayUtils.appendInt(mStartedUsers, userHandle); + } + // Let's kick any outstanding jobs for this user. + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + + @Override + public void onUnlockUser(int userHandle) { + // Let's kick any outstanding jobs for this user. + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + + @Override + public void onStopUser(int userHandle) { + synchronized (mLock) { + mStartedUsers = ArrayUtils.removeInt(mStartedUsers, userHandle); + } + } + + /** + * Return whether an UID is active or idle. + */ + private boolean isUidActive(int uid) { + return mAppStateTracker.isUidActiveSynced(uid); + } + + private final Predicate<Integer> mIsUidActivePredicate = this::isUidActive; + + public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName, + int userId, String tag) { + final String servicePkg = job.getService().getPackageName(); + if (job.isPersisted() && (packageName == null || packageName.equals(servicePkg))) { + // Only limit schedule calls for persisted jobs scheduled by the app itself. + final String pkg = + packageName == null ? job.getService().getPackageName() : packageName; + if (!mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG)) { + if (mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_LOGGED)) { + // Don't log too frequently + Slog.wtf(TAG, userId + "-" + pkg + " has called schedule() too many times"); + mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_LOGGED); + } + mAppStandbyInternal.restrictApp( + pkg, userId, UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); + if (mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION) { + final boolean isDebuggable; + synchronized (mLock) { + if (!mDebuggableApps.containsKey(packageName)) { + try { + final ApplicationInfo appInfo = AppGlobals.getPackageManager() + .getApplicationInfo(pkg, 0, userId); + if (appInfo != null) { + mDebuggableApps.put(packageName, + (appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0); + } else { + return JobScheduler.RESULT_FAILURE; + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + isDebuggable = mDebuggableApps.get(packageName); + } + if (isDebuggable) { + // Only throw the exception for debuggable apps. + throw new LimitExceededException( + "schedule()/enqueue() called more than " + + mQuotaTracker.getLimit( + QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED) + + " times in the past " + + mQuotaTracker.getWindowSizeMs( + QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED) + + "ms. See the documentation for more information."); + } + } + if (mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT) { + return JobScheduler.RESULT_FAILURE; + } + } + mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG); + } + + try { + if (ActivityManager.getService().isAppStartModeDisabled(uId, + job.getService().getPackageName())) { + Slog.w(TAG, "Not scheduling job " + uId + ":" + job.toString() + + " -- package not allowed to start"); + return JobScheduler.RESULT_FAILURE; + } + } catch (RemoteException e) { + } + + synchronized (mLock) { + final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, job.getId()); + + if (work != null && toCancel != null) { + // Fast path: we are adding work to an existing job, and the JobInfo is not + // changing. We can just directly enqueue this work in to the job. + if (toCancel.getJob().equals(job)) { + + toCancel.enqueueWorkLocked(work); + + // If any of work item is enqueued when the source is in the foreground, + // exempt the entire job. + toCancel.maybeAddForegroundExemption(mIsUidActivePredicate); + + return JobScheduler.RESULT_SUCCESS; + } + } + + JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag); + + // Give exemption if the source is in the foreground just now. + // Note if it's a sync job, this method is called on the handler so it's not exactly + // the state when requestSync() was called, but that should be fine because of the + // 1 minute foreground grace period. + jobStatus.maybeAddForegroundExemption(mIsUidActivePredicate); + + if (DEBUG) Slog.d(TAG, "SCHEDULE: " + jobStatus.toShortString()); + // Jobs on behalf of others don't apply to the per-app job cap + if (ENFORCE_MAX_JOBS && packageName == null) { + if (mJobs.countJobsForUid(uId) > MAX_JOBS_PER_APP) { + Slog.w(TAG, "Too many jobs for uid " + uId); + throw new IllegalStateException("Apps may not schedule more than " + + MAX_JOBS_PER_APP + " distinct jobs"); + } + } + + // This may throw a SecurityException. + jobStatus.prepareLocked(); + + if (toCancel != null) { + // Implicitly replaces the existing job record with the new instance + cancelJobImplLocked(toCancel, jobStatus, "job rescheduled by app"); + } else { + startTrackingJobLocked(jobStatus, null); + } + + if (work != null) { + // If work has been supplied, enqueue it into the new job. + jobStatus.enqueueWorkLocked(work); + } + + FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED, + uId, null, jobStatus.getBatteryName(), + FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__SCHEDULED, + JobProtoEnums.STOP_REASON_CANCELLED, jobStatus.getStandbyBucket(), + jobStatus.getJobId(), + jobStatus.hasChargingConstraint(), + jobStatus.hasBatteryNotLowConstraint(), + jobStatus.hasStorageNotLowConstraint(), + jobStatus.hasTimingDelayConstraint(), + jobStatus.hasDeadlineConstraint(), + jobStatus.hasIdleConstraint(), + jobStatus.hasConnectivityConstraint(), + jobStatus.hasContentTriggerConstraint()); + + // If the job is immediately ready to run, then we can just immediately + // put it in the pending list and try to schedule it. This is especially + // important for jobs with a 0 deadline constraint, since they will happen a fair + // amount, we want to handle them as quickly as possible, and semantically we want to + // make sure we have started holding the wake lock for the job before returning to + // the caller. + // If the job is not yet ready to run, there is nothing more to do -- we are + // now just waiting for one of its controllers to change state and schedule + // the job appropriately. + if (isReadyToBeExecutedLocked(jobStatus)) { + // This is a new job, we can just immediately put it on the pending + // list and try to run it. + mJobPackageTracker.notePending(jobStatus); + addOrderedItem(mPendingJobs, jobStatus, sPendingJobComparator); + maybeRunPendingJobsLocked(); + } else { + evaluateControllerStatesLocked(jobStatus); + } + } + return JobScheduler.RESULT_SUCCESS; + } + + public List<JobInfo> getPendingJobs(int uid) { + synchronized (mLock) { + List<JobStatus> jobs = mJobs.getJobsByUid(uid); + ArrayList<JobInfo> outList = new ArrayList<JobInfo>(jobs.size()); + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.get(i); + outList.add(job.getJob()); + } + return outList; + } + } + + public JobInfo getPendingJob(int uid, int jobId) { + synchronized (mLock) { + List<JobStatus> jobs = mJobs.getJobsByUid(uid); + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.get(i); + if (job.getJobId() == jobId) { + return job.getJob(); + } + } + return null; + } + } + + void cancelJobsForUser(int userHandle) { + synchronized (mLock) { + final List<JobStatus> jobsForUser = mJobs.getJobsByUser(userHandle); + for (int i=0; i<jobsForUser.size(); i++) { + JobStatus toRemove = jobsForUser.get(i); + cancelJobImplLocked(toRemove, null, "user removed"); + } + } + } + + private void cancelJobsForNonExistentUsers() { + UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class); + synchronized (mLock) { + mJobs.removeJobsOfNonUsers(umi.getUserIds()); + } + } + + void cancelJobsForPackageAndUid(String pkgName, int uid, String reason) { + if ("android".equals(pkgName)) { + Slog.wtfStack(TAG, "Can't cancel all jobs for system package"); + return; + } + synchronized (mLock) { + final List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid); + for (int i = jobsForUid.size() - 1; i >= 0; i--) { + final JobStatus job = jobsForUid.get(i); + if (job.getSourcePackageName().equals(pkgName)) { + cancelJobImplLocked(job, null, reason); + } + } + } + } + + /** + * Entry point from client to cancel all jobs originating from their uid. + * This will remove the job from the master list, and cancel the job if it was staged for + * execution or being executed. + * @param uid Uid to check against for removal of a job. + * + */ + public boolean cancelJobsForUid(int uid, String reason) { + if (uid == Process.SYSTEM_UID) { + Slog.wtfStack(TAG, "Can't cancel all jobs for system uid"); + return false; + } + + boolean jobsCanceled = false; + synchronized (mLock) { + final List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid); + for (int i=0; i<jobsForUid.size(); i++) { + JobStatus toRemove = jobsForUid.get(i); + cancelJobImplLocked(toRemove, null, reason); + jobsCanceled = true; + } + } + return jobsCanceled; + } + + /** + * Entry point from client to cancel the job corresponding to the jobId provided. + * This will remove the job from the master list, and cancel the job if it was staged for + * execution or being executed. + * @param uid Uid of the calling client. + * @param jobId Id of the job, provided at schedule-time. + */ + public boolean cancelJob(int uid, int jobId, int callingUid) { + JobStatus toCancel; + synchronized (mLock) { + toCancel = mJobs.getJobByUidAndJobId(uid, jobId); + if (toCancel != null) { + cancelJobImplLocked(toCancel, null, + "cancel() called by app, callingUid=" + callingUid + + " uid=" + uid + " jobId=" + jobId); + } + return (toCancel != null); + } + } + + /** + * Cancel the given job, stopping it if it's currently executing. If {@code incomingJob} + * is null, the cancelled job is removed outright from the system. If + * {@code incomingJob} is non-null, it replaces {@code cancelled} in the store of + * currently scheduled jobs. + */ + private void cancelJobImplLocked(JobStatus cancelled, JobStatus incomingJob, String reason) { + if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString()); + cancelled.unprepareLocked(); + stopTrackingJobLocked(cancelled, incomingJob, true /* writeBack */); + // Remove from pending queue. + if (mPendingJobs.remove(cancelled)) { + mJobPackageTracker.noteNonpending(cancelled); + } + // Cancel if running. + stopJobOnServiceContextLocked(cancelled, JobParameters.REASON_CANCELED, reason); + // If this is a replacement, bring in the new version of the job + if (incomingJob != null) { + if (DEBUG) Slog.i(TAG, "Tracking replacement job " + incomingJob.toShortString()); + startTrackingJobLocked(incomingJob, cancelled); + } + reportActiveLocked(); + } + + void updateUidState(int uid, int procState) { + synchronized (mLock) { + if (procState == ActivityManager.PROCESS_STATE_TOP) { + // Only use this if we are exactly the top app. All others can live + // with just the foreground priority. This means that persistent processes + // can never be the top app priority... that is fine. + mUidPriorityOverride.put(uid, JobInfo.PRIORITY_TOP_APP); + } else if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { + mUidPriorityOverride.put(uid, JobInfo.PRIORITY_FOREGROUND_SERVICE); + } else if (procState <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE) { + mUidPriorityOverride.put(uid, JobInfo.PRIORITY_BOUND_FOREGROUND_SERVICE); + } else { + mUidPriorityOverride.delete(uid); + } + } + } + + @Override + public void onDeviceIdleStateChanged(boolean deviceIdle) { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "Doze state changed: " + deviceIdle); + } + if (deviceIdle) { + // When becoming idle, make sure no jobs are actively running, + // except those using the idle exemption flag. + for (int i=0; i<mActiveServices.size(); i++) { + JobServiceContext jsc = mActiveServices.get(i); + final JobStatus executing = jsc.getRunningJobLocked(); + if (executing != null + && (executing.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) == 0) { + jsc.cancelExecutingJobLocked(JobParameters.REASON_DEVICE_IDLE, + "cancelled due to doze"); + } + } + } else { + // When coming out of idle, allow thing to start back up. + if (mReadyToRock) { + if (mLocalDeviceIdleController != null) { + if (!mReportedActive) { + mReportedActive = true; + mLocalDeviceIdleController.setJobsActive(true); + } + } + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + } + } + } + + @Override + public void onRestrictedBucketChanged(List<JobStatus> jobs) { + final int len = jobs.size(); + if (len == 0) { + Slog.wtf(TAG, "onRestrictedBucketChanged called with no jobs"); + return; + } + synchronized (mLock) { + for (int i = 0; i < len; ++i) { + JobStatus js = jobs.get(i); + for (int j = mRestrictiveControllers.size() - 1; j >= 0; --j) { + // Effective standby bucket can change after this in some situations so use + // the real bucket so that the job is tracked by the controllers. + if (js.getStandbyBucket() == RESTRICTED_INDEX) { + mRestrictiveControllers.get(j).startTrackingRestrictedJobLocked(js); + } else { + mRestrictiveControllers.get(j).stopTrackingRestrictedJobLocked(js); + } + } + } + } + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + + void reportActiveLocked() { + // active is true if pending queue contains jobs OR some job is running. + boolean active = mPendingJobs.size() > 0; + if (mPendingJobs.size() <= 0) { + for (int i=0; i<mActiveServices.size(); i++) { + final JobServiceContext jsc = mActiveServices.get(i); + final JobStatus job = jsc.getRunningJobLocked(); + if (job != null + && (job.getJob().getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) == 0 + && !job.dozeWhitelisted + && !job.uidActive) { + // We will report active if we have a job running and it is not an exception + // due to being in the foreground or whitelisted. + active = true; + break; + } + } + } + + if (mReportedActive != active) { + mReportedActive = active; + if (mLocalDeviceIdleController != null) { + mLocalDeviceIdleController.setJobsActive(active); + } + } + } + + void reportAppUsage(String packageName, int userId) { + // This app just transitioned into interactive use or near equivalent, so we should + // take a look at its job state for feedback purposes. + } + + /** + * Initializes the system service. + * <p> + * Subclasses must define a single argument constructor that accepts the context + * and passes it to super. + * </p> + * + * @param context The system server context. + */ + public JobSchedulerService(Context context) { + super(context); + + mLocalPM = LocalServices.getService(PackageManagerInternal.class); + mActivityManagerInternal = Objects.requireNonNull( + LocalServices.getService(ActivityManagerInternal.class)); + + mHandler = new JobHandler(context.getMainLooper()); + mConstants = new Constants(); + mConstantsObserver = new ConstantsObserver(mHandler); + mJobSchedulerStub = new JobSchedulerStub(); + + mConcurrencyManager = new JobConcurrencyManager(this); + + // Set up the app standby bucketing tracker + mStandbyTracker = new StandbyTracker(); + mUsageStats = LocalServices.getService(UsageStatsManagerInternal.class); + mQuotaTracker = new CountQuotaTracker(context, QUOTA_CATEGORIZER); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, + mConstants.API_QUOTA_SCHEDULE_COUNT, + mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); + // Log at most once per minute. + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED, 1, 60_000); + + mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class); + mAppStandbyInternal.addListener(mStandbyTracker); + + // The job store needs to call back + publishLocalService(JobSchedulerInternal.class, new LocalService()); + + // Initialize the job store and set up any persisted jobs + mJobs = JobStore.initAndGet(this); + + // Create the controllers. + mControllers = new ArrayList<StateController>(); + final ConnectivityController connectivityController = new ConnectivityController(this); + mControllers.add(connectivityController); + mControllers.add(new TimeController(this)); + final IdleController idleController = new IdleController(this); + mControllers.add(idleController); + mBatteryController = new BatteryController(this); + mControllers.add(mBatteryController); + mStorageController = new StorageController(this); + mControllers.add(mStorageController); + mControllers.add(new BackgroundJobsController(this)); + mControllers.add(new ContentObserverController(this)); + mDeviceIdleJobsController = new DeviceIdleJobsController(this); + mControllers.add(mDeviceIdleJobsController); + mQuotaController = new QuotaController(this); + mControllers.add(mQuotaController); + + mRestrictiveControllers = new ArrayList<>(); + mRestrictiveControllers.add(mBatteryController); + mRestrictiveControllers.add(connectivityController); + mRestrictiveControllers.add(idleController); + + // Create restrictions + mJobRestrictions = new ArrayList<>(); + mJobRestrictions.add(new ThermalStatusRestriction(this)); + + mSystemGalleryPackage = Objects.requireNonNull( + context.getString(R.string.config_systemGallery)); + + // If the job store determined that it can't yet reschedule persisted jobs, + // we need to start watching the clock. + if (!mJobs.jobTimesInflatedValid()) { + Slog.w(TAG, "!!! RTC not yet good; tracking time updates for job scheduling"); + context.registerReceiver(mTimeSetReceiver, new IntentFilter(Intent.ACTION_TIME_CHANGED)); + } + } + + private final BroadcastReceiver mTimeSetReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_TIME_CHANGED.equals(intent.getAction())) { + // When we reach clock sanity, recalculate the temporal windows + // of all affected jobs. + if (mJobs.clockNowValidToInflate(sSystemClock.millis())) { + Slog.i(TAG, "RTC now valid; recalculating persisted job windows"); + + // We've done our job now, so stop watching the time. + context.unregisterReceiver(this); + + // And kick off the work to update the affected jobs, using a secondary + // thread instead of chugging away here on the main looper thread. + FgThread.getHandler().post(mJobTimeUpdater); + } + } + } + }; + + private final Runnable mJobTimeUpdater = () -> { + final ArrayList<JobStatus> toRemove = new ArrayList<>(); + final ArrayList<JobStatus> toAdd = new ArrayList<>(); + synchronized (mLock) { + // Note: we intentionally both look up the existing affected jobs and replace them + // with recalculated ones inside the same lock lifetime. + getJobStore().getRtcCorrectedJobsLocked(toAdd, toRemove); + + // Now, at each position [i], we have both the existing JobStatus + // and the one that replaces it. + final int N = toAdd.size(); + for (int i = 0; i < N; i++) { + final JobStatus oldJob = toRemove.get(i); + final JobStatus newJob = toAdd.get(i); + if (DEBUG) { + Slog.v(TAG, " replacing " + oldJob + " with " + newJob); + } + cancelJobImplLocked(oldJob, newJob, "deferred rtc calculation"); + } + } + }; + + @Override + public void onStart() { + publishBinderService(Context.JOB_SCHEDULER_SERVICE, mJobSchedulerStub); + } + + @Override + public void onBootPhase(int phase) { + if (PHASE_SYSTEM_SERVICES_READY == phase) { + mConstantsObserver.start(getContext().getContentResolver()); + for (StateController controller : mControllers) { + controller.onSystemServicesReady(); + } + + mAppStateTracker = Objects.requireNonNull( + LocalServices.getService(AppStateTracker.class)); + + // Register br for package removals and user removals. + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_RESTARTED); + filter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART); + filter.addDataScheme("package"); + getContext().registerReceiverAsUser( + mBroadcastReceiver, UserHandle.ALL, filter, null, null); + final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED); + getContext().registerReceiverAsUser( + mBroadcastReceiver, UserHandle.ALL, userFilter, null, null); + try { + ActivityManager.getService().registerUidObserver(mUidObserver, + ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE + | ActivityManager.UID_OBSERVER_IDLE | ActivityManager.UID_OBSERVER_ACTIVE, + ActivityManager.PROCESS_STATE_UNKNOWN, null); + } catch (RemoteException e) { + // ignored; both services live in system_server + } + + mConcurrencyManager.onSystemReady(); + + // Remove any jobs that are not associated with any of the current users. + cancelJobsForNonExistentUsers(); + + for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { + mJobRestrictions.get(i).onSystemServicesReady(); + } + } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + synchronized (mLock) { + // Let's go! + mReadyToRock = true; + mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService( + BatteryStats.SERVICE_NAME)); + mLocalDeviceIdleController = + LocalServices.getService(DeviceIdleInternal.class); + // Create the "runners". + for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { + mActiveServices.add( + new JobServiceContext(this, mBatteryStats, mJobPackageTracker, + getContext().getMainLooper())); + } + // Attach jobs to their controllers. + mJobs.forEachJob((job) -> { + for (int controller = 0; controller < mControllers.size(); controller++) { + final StateController sc = mControllers.get(controller); + sc.maybeStartTrackingJobLocked(job, null); + } + }); + // GO GO GO! + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + } + } + + /** + * Called when we have a job status object that we need to insert in our + * {@link com.android.server.job.JobStore}, and make sure all the relevant controllers know + * about. + */ + private void startTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { + if (!jobStatus.isPreparedLocked()) { + Slog.wtf(TAG, "Not yet prepared when started tracking: " + jobStatus); + } + jobStatus.enqueueTime = sElapsedRealtimeClock.millis(); + final boolean update = mJobs.add(jobStatus); + if (mReadyToRock) { + for (int i = 0; i < mControllers.size(); i++) { + StateController controller = mControllers.get(i); + if (update) { + controller.maybeStopTrackingJobLocked(jobStatus, null, true); + } + controller.maybeStartTrackingJobLocked(jobStatus, lastJob); + } + } + } + + /** + * Called when we want to remove a JobStatus object that we've finished executing. + * @return true if the job was removed. + */ + private boolean stopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean removeFromPersisted) { + // Deal with any remaining work items in the old job. + jobStatus.stopTrackingJobLocked(incomingJob); + + // Remove from store as well as controllers. + final boolean removed = mJobs.remove(jobStatus, removeFromPersisted); + if (removed && mReadyToRock) { + for (int i=0; i<mControllers.size(); i++) { + StateController controller = mControllers.get(i); + controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false); + } + } + return removed; + } + + private boolean stopJobOnServiceContextLocked(JobStatus job, int reason, String debugReason) { + for (int i=0; i<mActiveServices.size(); i++) { + JobServiceContext jsc = mActiveServices.get(i); + final JobStatus executing = jsc.getRunningJobLocked(); + if (executing != null && executing.matches(job.getUid(), job.getJobId())) { + jsc.cancelExecutingJobLocked(reason, debugReason); + return true; + } + } + return false; + } + + /** + * @param job JobStatus we are querying against. + * @return Whether or not the job represented by the status object is currently being run or + * is pending. + */ + private boolean isCurrentlyActiveLocked(JobStatus job) { + for (int i=0; i<mActiveServices.size(); i++) { + JobServiceContext serviceContext = mActiveServices.get(i); + final JobStatus running = serviceContext.getRunningJobLocked(); + if (running != null && running.matches(job.getUid(), job.getJobId())) { + return true; + } + } + return false; + } + + void noteJobsPending(List<JobStatus> jobs) { + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.get(i); + mJobPackageTracker.notePending(job); + } + } + + void noteJobsNonpending(List<JobStatus> jobs) { + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.get(i); + mJobPackageTracker.noteNonpending(job); + } + } + + /** + * Reschedules the given job based on the job's backoff policy. It doesn't make sense to + * specify an override deadline on a failed job (the failed job will run even though it's not + * ready), so we reschedule it with {@link JobStatus#NO_LATEST_RUNTIME}, but specify that any + * ready job with {@link JobStatus#getNumFailures()} > 0 will be executed. + * + * @param failureToReschedule Provided job status that we will reschedule. + * @return A newly instantiated JobStatus with the same constraints as the last job except + * with adjusted timing constraints. + * + * @see #maybeQueueReadyJobsForExecutionLocked + */ + @VisibleForTesting + JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule) { + final long elapsedNowMillis = sElapsedRealtimeClock.millis(); + final JobInfo job = failureToReschedule.getJob(); + + final long initialBackoffMillis = job.getInitialBackoffMillis(); + final int backoffAttempts = failureToReschedule.getNumFailures() + 1; + long delayMillis; + + switch (job.getBackoffPolicy()) { + case JobInfo.BACKOFF_POLICY_LINEAR: { + long backoff = initialBackoffMillis; + if (backoff < mConstants.MIN_LINEAR_BACKOFF_TIME) { + backoff = mConstants.MIN_LINEAR_BACKOFF_TIME; + } + delayMillis = backoff * backoffAttempts; + } break; + default: + if (DEBUG) { + Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential."); + } + case JobInfo.BACKOFF_POLICY_EXPONENTIAL: { + long backoff = initialBackoffMillis; + if (backoff < mConstants.MIN_EXP_BACKOFF_TIME) { + backoff = mConstants.MIN_EXP_BACKOFF_TIME; + } + delayMillis = (long) Math.scalb(backoff, backoffAttempts - 1); + } break; + } + delayMillis = + Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS); + JobStatus newJob = new JobStatus(failureToReschedule, + elapsedNowMillis + delayMillis, + JobStatus.NO_LATEST_RUNTIME, backoffAttempts, + failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis()); + if (job.isPeriodic()) { + newJob.setOriginalLatestRunTimeElapsed( + failureToReschedule.getOriginalLatestRunTimeElapsed()); + } + for (int ic=0; ic<mControllers.size(); ic++) { + StateController controller = mControllers.get(ic); + controller.rescheduleForFailureLocked(newJob, failureToReschedule); + } + return newJob; + } + + /** + * Maximum time buffer in which JobScheduler will try to optimize periodic job scheduling. This + * does not cause a job's period to be larger than requested (eg: if the requested period is + * shorter than this buffer). This is used to put a limit on when JobScheduler will intervene + * and try to optimize scheduling if the current job finished less than this amount of time to + * the start of the next period + */ + private static final long PERIODIC_JOB_WINDOW_BUFFER = 30 * MINUTE_IN_MILLIS; + + /** The maximum period a periodic job can have. Anything higher will be clamped down to this. */ + public static final long MAX_ALLOWED_PERIOD_MS = 365 * 24 * 60 * 60 * 1000L; + + /** + * Called after a periodic has executed so we can reschedule it. We take the last execution + * time of the job to be the time of completion (i.e. the time at which this function is + * called). + * <p>This could be inaccurate b/c the job can run for as long as + * {@link com.android.server.job.JobServiceContext#EXECUTING_TIMESLICE_MILLIS}, but will lead + * to underscheduling at least, rather than if we had taken the last execution time to be the + * start of the execution. + * + * @return A new job representing the execution criteria for this instantiation of the + * recurring job. + */ + @VisibleForTesting + JobStatus getRescheduleJobForPeriodic(JobStatus periodicToReschedule) { + final long elapsedNow = sElapsedRealtimeClock.millis(); + final long newLatestRuntimeElapsed; + // Make sure period is in the interval [min_possible_period, max_possible_period]. + final long period = Math.max(JobInfo.getMinPeriodMillis(), + Math.min(MAX_ALLOWED_PERIOD_MS, periodicToReschedule.getJob().getIntervalMillis())); + // Make sure flex is in the interval [min_possible_flex, period]. + final long flex = Math.max(JobInfo.getMinFlexMillis(), + Math.min(period, periodicToReschedule.getJob().getFlexMillis())); + long rescheduleBuffer = 0; + + long olrte = periodicToReschedule.getOriginalLatestRunTimeElapsed(); + if (olrte < 0 || olrte == JobStatus.NO_LATEST_RUNTIME) { + Slog.wtf(TAG, "Invalid periodic job original latest run time: " + olrte); + olrte = elapsedNow; + } + final long latestRunTimeElapsed = olrte; + + final long diffMs = Math.abs(elapsedNow - latestRunTimeElapsed); + if (elapsedNow > latestRunTimeElapsed) { + // The job ran past its expected run window. Have it count towards the current window + // and schedule a new job for the next window. + if (DEBUG) { + Slog.i(TAG, "Periodic job ran after its intended window."); + } + long numSkippedWindows = (diffMs / period) + 1; // +1 to include original window + if (period != flex && diffMs > Math.min(PERIODIC_JOB_WINDOW_BUFFER, + (period - flex) / 2)) { + if (DEBUG) { + Slog.d(TAG, "Custom flex job ran too close to next window."); + } + // For custom flex periods, if the job was run too close to the next window, + // skip the next window and schedule for the following one. + numSkippedWindows += 1; + } + newLatestRuntimeElapsed = latestRunTimeElapsed + (period * numSkippedWindows); + } else { + newLatestRuntimeElapsed = latestRunTimeElapsed + period; + if (diffMs < PERIODIC_JOB_WINDOW_BUFFER && diffMs < period / 6) { + // Add a little buffer to the start of the next window so the job doesn't run + // too soon after this completed one. + rescheduleBuffer = Math.min(PERIODIC_JOB_WINDOW_BUFFER, period / 6 - diffMs); + } + } + + if (newLatestRuntimeElapsed < elapsedNow) { + Slog.wtf(TAG, "Rescheduling calculated latest runtime in the past: " + + newLatestRuntimeElapsed); + return new JobStatus(periodicToReschedule, + elapsedNow + period - flex, elapsedNow + period, + 0 /* backoffAttempt */, + sSystemClock.millis() /* lastSuccessfulRunTime */, + periodicToReschedule.getLastFailedRunTime()); + } + + final long newEarliestRunTimeElapsed = newLatestRuntimeElapsed + - Math.min(flex, period - rescheduleBuffer); + + if (DEBUG) { + Slog.v(TAG, "Rescheduling executed periodic. New execution window [" + + newEarliestRunTimeElapsed / 1000 + ", " + newLatestRuntimeElapsed / 1000 + + "]s"); + } + return new JobStatus(periodicToReschedule, + newEarliestRunTimeElapsed, newLatestRuntimeElapsed, + 0 /* backoffAttempt */, + sSystemClock.millis() /* lastSuccessfulRunTime */, + periodicToReschedule.getLastFailedRunTime()); + } + + // JobCompletedListener implementations. + + /** + * A job just finished executing. We fetch the + * {@link com.android.server.job.controllers.JobStatus} from the store and depending on + * whether we want to reschedule we re-add it to the controllers. + * @param jobStatus Completed job. + * @param needsReschedule Whether the implementing class should reschedule this job. + */ + @Override + public void onJobCompletedLocked(JobStatus jobStatus, boolean needsReschedule) { + if (DEBUG) { + Slog.d(TAG, "Completed " + jobStatus + ", reschedule=" + needsReschedule); + } + + // If the job wants to be rescheduled, we first need to make the next upcoming + // job so we can transfer any appropriate state over from the previous job when + // we stop it. + final JobStatus rescheduledJob = needsReschedule + ? getRescheduleJobForFailureLocked(jobStatus) : null; + + // Do not write back immediately if this is a periodic job. The job may get lost if system + // shuts down before it is added back. + if (!stopTrackingJobLocked(jobStatus, rescheduledJob, !jobStatus.getJob().isPeriodic())) { + if (DEBUG) { + Slog.d(TAG, "Could not find job to remove. Was job removed while executing?"); + } + // We still want to check for jobs to execute, because this job may have + // scheduled a new job under the same job id, and now we can run it. + mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget(); + return; + } + + if (rescheduledJob != null) { + try { + rescheduledJob.prepareLocked(); + } catch (SecurityException e) { + Slog.w(TAG, "Unable to regrant job permissions for " + rescheduledJob); + } + startTrackingJobLocked(rescheduledJob, jobStatus); + } else if (jobStatus.getJob().isPeriodic()) { + JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus); + try { + rescheduledPeriodic.prepareLocked(); + } catch (SecurityException e) { + Slog.w(TAG, "Unable to regrant job permissions for " + rescheduledPeriodic); + } + startTrackingJobLocked(rescheduledPeriodic, jobStatus); + } + jobStatus.unprepareLocked(); + reportActiveLocked(); + mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget(); + } + + // StateChangedListener implementations. + + /** + * Posts a message to the {@link com.android.server.job.JobSchedulerService.JobHandler} that + * some controller's state has changed, so as to run through the list of jobs and start/stop + * any that are eligible. + */ + @Override + public void onControllerStateChanged() { + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + + @Override + public void onRunJobNow(JobStatus jobStatus) { + mHandler.obtainMessage(MSG_JOB_EXPIRED, jobStatus).sendToTarget(); + } + + final private class JobHandler extends Handler { + + public JobHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message message) { + synchronized (mLock) { + if (!mReadyToRock) { + return; + } + switch (message.what) { + case MSG_JOB_EXPIRED: { + JobStatus runNow = (JobStatus) message.obj; + // runNow can be null, which is a controller's way of indicating that its + // state is such that all ready jobs should be run immediately. + if (runNow != null && isReadyToBeExecutedLocked(runNow)) { + mJobPackageTracker.notePending(runNow); + addOrderedItem(mPendingJobs, runNow, sPendingJobComparator); + } else { + queueReadyJobsForExecutionLocked(); + } + } break; + case MSG_CHECK_JOB: + if (DEBUG) { + Slog.d(TAG, "MSG_CHECK_JOB"); + } + removeMessages(MSG_CHECK_JOB); + if (mReportedActive) { + // if jobs are currently being run, queue all ready jobs for execution. + queueReadyJobsForExecutionLocked(); + } else { + // Check the list of jobs and run some of them if we feel inclined. + maybeQueueReadyJobsForExecutionLocked(); + } + break; + case MSG_CHECK_JOB_GREEDY: + if (DEBUG) { + Slog.d(TAG, "MSG_CHECK_JOB_GREEDY"); + } + queueReadyJobsForExecutionLocked(); + break; + case MSG_STOP_JOB: + cancelJobImplLocked((JobStatus) message.obj, null, + "app no longer allowed to run"); + break; + + case MSG_UID_STATE_CHANGED: { + final int uid = message.arg1; + final int procState = message.arg2; + updateUidState(uid, procState); + break; + } + case MSG_UID_GONE: { + final int uid = message.arg1; + final boolean disabled = message.arg2 != 0; + updateUidState(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY); + if (disabled) { + cancelJobsForUid(uid, "uid gone"); + } + synchronized (mLock) { + mDeviceIdleJobsController.setUidActiveLocked(uid, false); + } + break; + } + case MSG_UID_ACTIVE: { + final int uid = message.arg1; + synchronized (mLock) { + mDeviceIdleJobsController.setUidActiveLocked(uid, true); + } + break; + } + case MSG_UID_IDLE: { + final int uid = message.arg1; + final boolean disabled = message.arg2 != 0; + if (disabled) { + cancelJobsForUid(uid, "app uid idle"); + } + synchronized (mLock) { + mDeviceIdleJobsController.setUidActiveLocked(uid, false); + } + break; + } + + } + maybeRunPendingJobsLocked(); + // Don't remove JOB_EXPIRED in case one came along while processing the queue. + } + } + } + + /** + * Check if a job is restricted by any of the declared {@link JobRestriction}s. + * Note, that the jobs with {@link JobInfo#PRIORITY_FOREGROUND_APP} priority or higher may not + * be restricted, thus we won't even perform the check, but simply return null early. + * + * @param job to be checked + * @return the first {@link JobRestriction} restricting the given job that has been found; null + * - if passes all the restrictions or has priority {@link JobInfo#PRIORITY_FOREGROUND_APP} + * or higher. + */ + private JobRestriction checkIfRestricted(JobStatus job) { + if (evaluateJobPriorityLocked(job) >= JobInfo.PRIORITY_FOREGROUND_APP) { + // Jobs with PRIORITY_FOREGROUND_APP or higher should not be restricted + return null; + } + for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { + final JobRestriction restriction = mJobRestrictions.get(i); + if (restriction.isJobRestricted(job)) { + return restriction; + } + } + return null; + } + + private void stopNonReadyActiveJobsLocked() { + for (int i=0; i<mActiveServices.size(); i++) { + JobServiceContext serviceContext = mActiveServices.get(i); + final JobStatus running = serviceContext.getRunningJobLocked(); + if (running == null) { + continue; + } + if (!running.isReady()) { + // If a restricted job doesn't have dynamic constraints satisfied, assume that's + // the reason the job is being stopped, instead of because of other constraints + // not being satisfied. + if (running.getEffectiveStandbyBucket() == RESTRICTED_INDEX + && !running.areDynamicConstraintsSatisfied()) { + serviceContext.cancelExecutingJobLocked( + JobParameters.REASON_RESTRICTED_BUCKET, + "cancelled due to restricted bucket"); + } else { + serviceContext.cancelExecutingJobLocked( + JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED, + "cancelled due to unsatisfied constraints"); + } + } else { + final JobRestriction restriction = checkIfRestricted(running); + if (restriction != null) { + final int reason = restriction.getReason(); + serviceContext.cancelExecutingJobLocked(reason, + "restricted due to " + JobParameters.getReasonCodeDescription(reason)); + } + } + } + } + + /** + * Run through list of jobs and execute all possible - at least one is expired so we do + * as many as we can. + */ + private void queueReadyJobsForExecutionLocked() { + if (DEBUG) { + Slog.d(TAG, "queuing all ready jobs for execution:"); + } + noteJobsNonpending(mPendingJobs); + mPendingJobs.clear(); + stopNonReadyActiveJobsLocked(); + mJobs.forEachJob(mReadyQueueFunctor); + mReadyQueueFunctor.postProcess(); + + if (DEBUG) { + final int queuedJobs = mPendingJobs.size(); + if (queuedJobs == 0) { + Slog.d(TAG, "No jobs pending."); + } else { + Slog.d(TAG, queuedJobs + " jobs queued."); + } + } + } + + final class ReadyJobQueueFunctor implements Consumer<JobStatus> { + final ArrayList<JobStatus> newReadyJobs = new ArrayList<>(); + + @Override + public void accept(JobStatus job) { + if (isReadyToBeExecutedLocked(job)) { + if (DEBUG) { + Slog.d(TAG, " queued " + job.toShortString()); + } + newReadyJobs.add(job); + } else { + evaluateControllerStatesLocked(job); + } + } + + public void postProcess() { + noteJobsPending(newReadyJobs); + mPendingJobs.addAll(newReadyJobs); + if (mPendingJobs.size() > 1) { + mPendingJobs.sort(sPendingJobComparator); + } + + newReadyJobs.clear(); + } + } + private final ReadyJobQueueFunctor mReadyQueueFunctor = new ReadyJobQueueFunctor(); + + /** + * The state of at least one job has changed. Here is where we could enforce various + * policies on when we want to execute jobs. + */ + final class MaybeReadyJobQueueFunctor implements Consumer<JobStatus> { + int forceBatchedCount; + int unbatchedCount; + final List<JobStatus> runnableJobs = new ArrayList<>(); + + public MaybeReadyJobQueueFunctor() { + reset(); + } + + // Functor method invoked for each job via JobStore.forEachJob() + @Override + public void accept(JobStatus job) { + if (isReadyToBeExecutedLocked(job)) { + try { + if (ActivityManager.getService().isAppStartModeDisabled(job.getUid(), + job.getJob().getService().getPackageName())) { + Slog.w(TAG, "Aborting job " + job.getUid() + ":" + + job.getJob().toString() + " -- package not allowed to start"); + mHandler.obtainMessage(MSG_STOP_JOB, job).sendToTarget(); + return; + } + } catch (RemoteException e) { + } + + final boolean shouldForceBatchJob; + // Restricted jobs must always be batched + if (job.getEffectiveStandbyBucket() == RESTRICTED_INDEX) { + shouldForceBatchJob = true; + } else if (job.getNumFailures() > 0) { + shouldForceBatchJob = false; + } else { + final long nowElapsed = sElapsedRealtimeClock.millis(); + final boolean batchDelayExpired = job.getFirstForceBatchedTimeElapsed() > 0 + && nowElapsed - job.getFirstForceBatchedTimeElapsed() + >= mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS; + shouldForceBatchJob = + mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT > 1 + && job.getEffectiveStandbyBucket() != ACTIVE_INDEX + && !batchDelayExpired; + } + + if (shouldForceBatchJob) { + // Force batching non-ACTIVE jobs. Don't include them in the other counts. + forceBatchedCount++; + if (job.getFirstForceBatchedTimeElapsed() == 0) { + job.setFirstForceBatchedTimeElapsed(sElapsedRealtimeClock.millis()); + } + } else { + unbatchedCount++; + } + runnableJobs.add(job); + } else { + evaluateControllerStatesLocked(job); + } + } + + public void postProcess() { + if (unbatchedCount > 0 + || forceBatchedCount >= mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT) { + if (DEBUG) { + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Running jobs."); + } + noteJobsPending(runnableJobs); + mPendingJobs.addAll(runnableJobs); + if (mPendingJobs.size() > 1) { + mPendingJobs.sort(sPendingJobComparator); + } + } else { + if (DEBUG) { + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Not running anything."); + } + } + + // Be ready for next time + reset(); + } + + @VisibleForTesting + void reset() { + forceBatchedCount = 0; + unbatchedCount = 0; + runnableJobs.clear(); + } + } + private final MaybeReadyJobQueueFunctor mMaybeQueueFunctor = new MaybeReadyJobQueueFunctor(); + + private void maybeQueueReadyJobsForExecutionLocked() { + if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs..."); + + noteJobsNonpending(mPendingJobs); + mPendingJobs.clear(); + stopNonReadyActiveJobsLocked(); + mJobs.forEachJob(mMaybeQueueFunctor); + mMaybeQueueFunctor.postProcess(); + } + + /** Returns true if both the calling and source users for the job are started. */ + private boolean areUsersStartedLocked(final JobStatus job) { + boolean sourceStarted = ArrayUtils.contains(mStartedUsers, job.getSourceUserId()); + if (job.getUserId() == job.getSourceUserId()) { + return sourceStarted; + } + return sourceStarted && ArrayUtils.contains(mStartedUsers, job.getUserId()); + } + + /** + * Criteria for moving a job into the pending queue: + * - It's ready. + * - It's not pending. + * - It's not already running on a JSC. + * - The user that requested the job is running. + * - The job's standby bucket has come due to be runnable. + * - The component is enabled and runnable. + */ + @VisibleForTesting + boolean isReadyToBeExecutedLocked(JobStatus job) { + final boolean jobReady = job.isReady(); + + if (DEBUG) { + Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + + " ready=" + jobReady); + } + + // This is a condition that is very likely to be false (most jobs that are + // scheduled are sitting there, not ready yet) and very cheap to check (just + // a few conditions on data in JobStatus). + if (!jobReady) { + if (job.getSourcePackageName().equals("android.jobscheduler.cts.jobtestapp")) { + Slog.v(TAG, " NOT READY: " + job); + } + return false; + } + + final boolean jobExists = mJobs.containsJob(job); + final boolean userStarted = areUsersStartedLocked(job); + final boolean backingUp = mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0; + + if (DEBUG) { + Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + + " exists=" + jobExists + " userStarted=" + userStarted + + " backingUp=" + backingUp); + } + + // These are also fairly cheap to check, though they typically will not + // be conditions we fail. + if (!jobExists || !userStarted || backingUp) { + return false; + } + + if (checkIfRestricted(job) != null) { + return false; + } + + final boolean jobPending = mPendingJobs.contains(job); + final boolean jobActive = isCurrentlyActiveLocked(job); + + if (DEBUG) { + Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + + " pending=" + jobPending + " active=" + jobActive); + } + + // These can be a little more expensive (especially jobActive, since we need to + // go through the array of all potentially active jobs), so we are doing them + // later... but still before checking with the package manager! + if (jobPending || jobActive) { + return false; + } + + // The expensive check: validate that the defined package+service is + // still present & viable. + return isComponentUsable(job); + } + + private boolean isComponentUsable(@NonNull JobStatus job) { + final ServiceInfo service; + try { + // TODO: cache result until we're notified that something in the package changed. + service = AppGlobals.getPackageManager().getServiceInfo( + job.getServiceComponent(), PackageManager.MATCH_DEBUG_TRIAGED_MISSING, + job.getUserId()); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + + if (service == null) { + if (DEBUG) { + Slog.v(TAG, "isComponentUsable: " + job.toShortString() + + " component not present"); + } + return false; + } + + // Everything else checked out so far, so this is the final yes/no check + final boolean appIsBad = mActivityManagerInternal.isAppBad(service.applicationInfo); + if (DEBUG && appIsBad) { + Slog.i(TAG, "App is bad for " + job.toShortString() + " so not runnable"); + } + return !appIsBad; + } + + @VisibleForTesting + void evaluateControllerStatesLocked(final JobStatus job) { + for (int c = mControllers.size() - 1; c >= 0; --c) { + final StateController sc = mControllers.get(c); + sc.evaluateStateLocked(job); + } + } + + /** + * Returns true if non-job constraint components are in place -- if job.isReady() returns true + * and this method returns true, then the job is ready to be executed. + */ + public boolean areComponentsInPlaceLocked(JobStatus job) { + // This code is very similar to the code in isReadyToBeExecutedLocked --- it uses the same + // conditions. + + final boolean jobExists = mJobs.containsJob(job); + final boolean userStarted = areUsersStartedLocked(job); + final boolean backingUp = mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0; + + if (DEBUG) { + Slog.v(TAG, "areComponentsInPlaceLocked: " + job.toShortString() + + " exists=" + jobExists + " userStarted=" + userStarted + + " backingUp=" + backingUp); + } + + // These are also fairly cheap to check, though they typically will not + // be conditions we fail. + if (!jobExists || !userStarted || backingUp) { + return false; + } + + if (checkIfRestricted(job) != null) { + return false; + } + + // Job pending/active doesn't affect the readiness of a job. + + // The expensive check: validate that the defined package+service is + // still present & viable. + return isComponentUsable(job); + } + + /** Returns the maximum amount of time this job could run for. */ + public long getMaxJobExecutionTimeMs(JobStatus job) { + synchronized (mLock) { + return Math.min(mQuotaController.getMaxJobExecutionTimeMsLocked(job), + JobServiceContext.EXECUTING_TIMESLICE_MILLIS); + } + } + + /** + * Reconcile jobs in the pending queue against available execution contexts. + * A controller can force a job into the pending queue even if it's already running, but + * here is where we decide whether to actually execute it. + */ + void maybeRunPendingJobsLocked() { + if (DEBUG) { + Slog.d(TAG, "pending queue: " + mPendingJobs.size() + " jobs."); + } + mConcurrencyManager.assignJobsToContextsLocked(); + reportActiveLocked(); + } + + private int adjustJobPriority(int curPriority, JobStatus job) { + if (curPriority < JobInfo.PRIORITY_TOP_APP) { + float factor = mJobPackageTracker.getLoadFactor(job); + if (factor >= mConstants.HEAVY_USE_FACTOR) { + curPriority += JobInfo.PRIORITY_ADJ_ALWAYS_RUNNING; + } else if (factor >= mConstants.MODERATE_USE_FACTOR) { + curPriority += JobInfo.PRIORITY_ADJ_OFTEN_RUNNING; + } + } + return curPriority; + } + + int evaluateJobPriorityLocked(JobStatus job) { + int priority = job.getPriority(); + if (priority >= JobInfo.PRIORITY_BOUND_FOREGROUND_SERVICE) { + return adjustJobPriority(priority, job); + } + int override = mUidPriorityOverride.get(job.getSourceUid(), 0); + if (override != 0) { + return adjustJobPriority(override, job); + } + return adjustJobPriority(priority, job); + } + + final class LocalService implements JobSchedulerInternal { + + /** + * Returns a list of all pending jobs. A running job is not considered pending. Periodic + * jobs are always considered pending. + */ + @Override + public List<JobInfo> getSystemScheduledPendingJobs() { + synchronized (mLock) { + final List<JobInfo> pendingJobs = new ArrayList<JobInfo>(); + mJobs.forEachJob(Process.SYSTEM_UID, (job) -> { + if (job.getJob().isPeriodic() || !isCurrentlyActiveLocked(job)) { + pendingJobs.add(job.getJob()); + } + }); + return pendingJobs; + } + } + + @Override + public void cancelJobsForUid(int uid, String reason) { + JobSchedulerService.this.cancelJobsForUid(uid, reason); + } + + @Override + public void addBackingUpUid(int uid) { + synchronized (mLock) { + // No need to actually do anything here, since for a full backup the + // activity manager will kill the process which will kill the job (and + // cause it to restart, but now it can't run). + mBackingUpUids.put(uid, uid); + } + } + + @Override + public void removeBackingUpUid(int uid) { + synchronized (mLock) { + mBackingUpUids.delete(uid); + // If there are any jobs for this uid, we need to rebuild the pending list + // in case they are now ready to run. + if (mJobs.countJobsForUid(uid) > 0) { + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + } + } + + @Override + public void clearAllBackingUpUids() { + synchronized (mLock) { + if (mBackingUpUids.size() > 0) { + mBackingUpUids.clear(); + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + } + } + + @Override + public String getMediaBackupPackage() { + return mSystemGalleryPackage; + } + + @Override + public void reportAppUsage(String packageName, int userId) { + JobSchedulerService.this.reportAppUsage(packageName, userId); + } + + @Override + public JobStorePersistStats getPersistStats() { + synchronized (mLock) { + return new JobStorePersistStats(mJobs.getPersistStats()); + } + } + } + + /** + * Tracking of app assignments to standby buckets + */ + final class StandbyTracker extends AppIdleStateChangeListener { + + // AppIdleStateChangeListener interface for live updates + + @Override + public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId, + boolean idle, int bucket, int reason) { + // QuotaController handles this now. + } + + @Override + public void onUserInteractionStarted(String packageName, int userId) { + final int uid = mLocalPM.getPackageUid(packageName, + PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); + if (uid < 0) { + // Quietly ignore; the case is already logged elsewhere + return; + } + + long sinceLast = mUsageStats.getTimeSinceLastJobRun(packageName, userId); + if (sinceLast > 2 * DateUtils.DAY_IN_MILLIS) { + // Too long ago, not worth logging + sinceLast = 0L; + } + final DeferredJobCounter counter = new DeferredJobCounter(); + synchronized (mLock) { + mJobs.forEachJobForSourceUid(uid, counter); + } + if (counter.numDeferred() > 0 || sinceLast > 0) { + BatteryStatsInternal mBatteryStatsInternal = LocalServices.getService + (BatteryStatsInternal.class); + mBatteryStatsInternal.noteJobsDeferred(uid, counter.numDeferred(), sinceLast); + FrameworkStatsLog.write_non_chained( + FrameworkStatsLog.DEFERRED_JOB_STATS_REPORTED, uid, null, + counter.numDeferred(), sinceLast); + } + } + } + + static class DeferredJobCounter implements Consumer<JobStatus> { + private int mDeferred = 0; + + public int numDeferred() { + return mDeferred; + } + + @Override + public void accept(JobStatus job) { + if (job.getWhenStandbyDeferred() > 0) { + mDeferred++; + } + } + } + + public static int standbyBucketToBucketIndex(int bucket) { + // Normalize AppStandby constants to indices into our bookkeeping + if (bucket == UsageStatsManager.STANDBY_BUCKET_NEVER) { + return NEVER_INDEX; + } else if (bucket > UsageStatsManager.STANDBY_BUCKET_RARE) { + return RESTRICTED_INDEX; + } else if (bucket > UsageStatsManager.STANDBY_BUCKET_FREQUENT) { + return RARE_INDEX; + } else if (bucket > UsageStatsManager.STANDBY_BUCKET_WORKING_SET) { + return FREQUENT_INDEX; + } else if (bucket > UsageStatsManager.STANDBY_BUCKET_ACTIVE) { + return WORKING_INDEX; + } else { + return ACTIVE_INDEX; + } + } + + // Static to support external callers + public static int standbyBucketForPackage(String packageName, int userId, long elapsedNow) { + UsageStatsManagerInternal usageStats = LocalServices.getService( + UsageStatsManagerInternal.class); + int bucket = usageStats != null + ? usageStats.getAppStandbyBucket(packageName, userId, elapsedNow) + : 0; + + bucket = standbyBucketToBucketIndex(bucket); + + if (DEBUG_STANDBY) { + Slog.v(TAG, packageName + "/" + userId + " standby bucket index: " + bucket); + } + return bucket; + } + + /** + * Binder stub trampoline implementation + */ + final class JobSchedulerStub extends IJobScheduler.Stub { + /** Cache determination of whether a given app can persist jobs + * key is uid of the calling app; value is undetermined/true/false + */ + private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>(); + + // Enforce that only the app itself (or shared uid participant) can schedule a + // job that runs one of the app's services, as well as verifying that the + // named service properly requires the BIND_JOB_SERVICE permission + private void enforceValidJobRequest(int uid, JobInfo job) { + final IPackageManager pm = AppGlobals.getPackageManager(); + final ComponentName service = job.getService(); + try { + ServiceInfo si = pm.getServiceInfo(service, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, + UserHandle.getUserId(uid)); + if (si == null) { + throw new IllegalArgumentException("No such service " + service); + } + if (si.applicationInfo.uid != uid) { + throw new IllegalArgumentException("uid " + uid + + " cannot schedule job in " + service.getPackageName()); + } + if (!JobService.PERMISSION_BIND.equals(si.permission)) { + throw new IllegalArgumentException("Scheduled service " + service + + " does not require android.permission.BIND_JOB_SERVICE permission"); + } + } catch (RemoteException e) { + // Can't happen; the Package Manager is in this same process + } + } + + private boolean canPersistJobs(int pid, int uid) { + // If we get this far we're good to go; all we need to do now is check + // whether the app is allowed to persist its scheduled work. + final boolean canPersist; + synchronized (mPersistCache) { + Boolean cached = mPersistCache.get(uid); + if (cached != null) { + canPersist = cached.booleanValue(); + } else { + // Persisting jobs is tantamount to running at boot, so we permit + // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED + // permission + int result = getContext().checkPermission( + android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid); + canPersist = (result == PackageManager.PERMISSION_GRANTED); + mPersistCache.put(uid, canPersist); + } + } + return canPersist; + } + + private void validateJobFlags(JobInfo job, int callingUid) { + if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG); + } + if ((job.getFlags() & JobInfo.FLAG_EXEMPT_FROM_APP_STANDBY) != 0) { + if (callingUid != Process.SYSTEM_UID) { + throw new SecurityException("Job has invalid flags"); + } + if (job.isPeriodic()) { + Slog.wtf(TAG, "Periodic jobs mustn't have" + + " FLAG_EXEMPT_FROM_APP_STANDBY. Job=" + job); + } + } + } + + // IJobScheduler implementation + @Override + public int schedule(JobInfo job) throws RemoteException { + if (DEBUG) { + Slog.d(TAG, "Scheduling job: " + job.toString()); + } + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(uid); + + enforceValidJobRequest(uid, job); + if (job.isPersisted()) { + if (!canPersistJobs(pid, uid)) { + throw new IllegalArgumentException("Error: requested job be persisted without" + + " holding RECEIVE_BOOT_COMPLETED permission."); + } + } + + validateJobFlags(job, uid); + + long ident = Binder.clearCallingIdentity(); + try { + return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, userId, + null); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + // IJobScheduler implementation + @Override + public int enqueue(JobInfo job, JobWorkItem work) throws RemoteException { + if (DEBUG) { + Slog.d(TAG, "Enqueueing job: " + job.toString() + " work: " + work); + } + final int uid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(uid); + + enforceValidJobRequest(uid, job); + if (job.isPersisted()) { + throw new IllegalArgumentException("Can't enqueue work for persisted jobs"); + } + if (work == null) { + throw new NullPointerException("work is null"); + } + + validateJobFlags(job, uid); + + long ident = Binder.clearCallingIdentity(); + try { + return JobSchedulerService.this.scheduleAsPackage(job, work, uid, null, userId, + null); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag) + throws RemoteException { + final int callerUid = Binder.getCallingUid(); + if (DEBUG) { + Slog.d(TAG, "Caller uid " + callerUid + " scheduling job: " + job.toString() + + " on behalf of " + packageName + "/"); + } + + if (packageName == null) { + throw new NullPointerException("Must specify a package for scheduleAsPackage()"); + } + + int mayScheduleForOthers = getContext().checkCallingOrSelfPermission( + android.Manifest.permission.UPDATE_DEVICE_STATS); + if (mayScheduleForOthers != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Caller uid " + callerUid + + " not permitted to schedule jobs for other apps"); + } + + validateJobFlags(job, callerUid); + + long ident = Binder.clearCallingIdentity(); + try { + return JobSchedulerService.this.scheduleAsPackage(job, null, callerUid, + packageName, userId, tag); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public ParceledListSlice<JobInfo> getAllPendingJobs() throws RemoteException { + final int uid = Binder.getCallingUid(); + + long ident = Binder.clearCallingIdentity(); + try { + return new ParceledListSlice<>(JobSchedulerService.this.getPendingJobs(uid)); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public JobInfo getPendingJob(int jobId) throws RemoteException { + final int uid = Binder.getCallingUid(); + + long ident = Binder.clearCallingIdentity(); + try { + return JobSchedulerService.this.getPendingJob(uid, jobId); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public void cancelAll() throws RemoteException { + final int uid = Binder.getCallingUid(); + long ident = Binder.clearCallingIdentity(); + try { + JobSchedulerService.this.cancelJobsForUid(uid, + "cancelAll() called by app, callingUid=" + uid); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public void cancel(int jobId) throws RemoteException { + final int uid = Binder.getCallingUid(); + + long ident = Binder.clearCallingIdentity(); + try { + JobSchedulerService.this.cancelJob(uid, jobId, uid); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + /** + * "dumpsys" infrastructure + */ + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, pw)) return; + + int filterUid = -1; + boolean proto = false; + if (!ArrayUtils.isEmpty(args)) { + int opti = 0; + while (opti < args.length) { + String arg = args[opti]; + if ("-h".equals(arg)) { + dumpHelp(pw); + return; + } else if ("-a".equals(arg)) { + // Ignore, we always dump all. + } else if ("--proto".equals(arg)) { + proto = true; + } else if (arg.length() > 0 && arg.charAt(0) == '-') { + pw.println("Unknown option: " + arg); + return; + } else { + break; + } + opti++; + } + if (opti < args.length) { + String pkg = args[opti]; + try { + filterUid = getContext().getPackageManager().getPackageUid(pkg, + PackageManager.MATCH_ANY_USER); + } catch (NameNotFoundException ignored) { + pw.println("Invalid package: " + pkg); + return; + } + } + } + + final long identityToken = Binder.clearCallingIdentity(); + try { + if (proto) { + JobSchedulerService.this.dumpInternalProto(fd, filterUid); + } else { + JobSchedulerService.this.dumpInternal(new IndentingPrintWriter(pw, " "), + filterUid); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + } + + @Override + public int handleShellCommand(@NonNull ParcelFileDescriptor in, + @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, + @NonNull String[] args) { + return (new JobSchedulerShellCommand(JobSchedulerService.this)).exec( + this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), + args); + } + + /** + * <b>For internal system user only!</b> + * Returns a list of all currently-executing jobs. + */ + @Override + public List<JobInfo> getStartedJobs() { + final int uid = Binder.getCallingUid(); + if (uid != Process.SYSTEM_UID) { + throw new SecurityException( + "getStartedJobs() is system internal use only."); + } + + final ArrayList<JobInfo> runningJobs; + + synchronized (mLock) { + runningJobs = new ArrayList<>(mActiveServices.size()); + for (JobServiceContext jsc : mActiveServices) { + final JobStatus job = jsc.getRunningJobLocked(); + if (job != null) { + runningJobs.add(job.getJob()); + } + } + } + + return runningJobs; + } + + /** + * <b>For internal system user only!</b> + * Returns a snapshot of the state of all jobs known to the system. + * + * <p class="note">This is a slow operation, so it should be called sparingly. + */ + @Override + public ParceledListSlice<JobSnapshot> getAllJobSnapshots() { + final int uid = Binder.getCallingUid(); + if (uid != Process.SYSTEM_UID) { + throw new SecurityException( + "getAllJobSnapshots() is system internal use only."); + } + synchronized (mLock) { + final ArrayList<JobSnapshot> snapshots = new ArrayList<>(mJobs.size()); + mJobs.forEachJob((job) -> snapshots.add( + new JobSnapshot(job.getJob(), job.getSatisfiedConstraintFlags(), + isReadyToBeExecutedLocked(job)))); + return new ParceledListSlice<>(snapshots); + } + } + } + + // Shell command infrastructure: run the given job immediately + int executeRunCommand(String pkgName, int userId, int jobId, boolean satisfied, boolean force) { + Slog.d(TAG, "executeRunCommand(): " + pkgName + "/" + userId + + " " + jobId + " s=" + satisfied + " f=" + force); + + try { + final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0, + userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM); + if (uid < 0) { + return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE; + } + + synchronized (mLock) { + final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId); + if (js == null) { + return JobSchedulerShellCommand.CMD_ERR_NO_JOB; + } + + js.overrideState = (force) ? JobStatus.OVERRIDE_FULL + : (satisfied ? JobStatus.OVERRIDE_SORTING : JobStatus.OVERRIDE_SOFT); + + // Re-evaluate constraints after the override is set in case one of the overridden + // constraints was preventing another constraint from thinking it needed to update. + for (int c = mControllers.size() - 1; c >= 0; --c) { + mControllers.get(c).reevaluateStateLocked(uid); + } + + if (!js.isConstraintsSatisfied()) { + js.overrideState = JobStatus.OVERRIDE_NONE; + return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS; + } + + queueReadyJobsForExecutionLocked(); + maybeRunPendingJobsLocked(); + } + } catch (RemoteException e) { + // can't happen + } + return 0; + } + + // Shell command infrastructure: immediately timeout currently executing jobs + int executeTimeoutCommand(PrintWriter pw, String pkgName, int userId, + boolean hasJobId, int jobId) { + if (DEBUG) { + Slog.v(TAG, "executeTimeoutCommand(): " + pkgName + "/" + userId + " " + jobId); + } + + synchronized (mLock) { + boolean foundSome = false; + for (int i=0; i<mActiveServices.size(); i++) { + final JobServiceContext jc = mActiveServices.get(i); + final JobStatus js = jc.getRunningJobLocked(); + if (jc.timeoutIfExecutingLocked(pkgName, userId, hasJobId, jobId, "shell")) { + foundSome = true; + pw.print("Timing out: "); + js.printUniqueId(pw); + pw.print(" "); + pw.println(js.getServiceComponent().flattenToShortString()); + } + } + if (!foundSome) { + pw.println("No matching executing jobs found."); + } + } + return 0; + } + + // Shell command infrastructure: cancel a scheduled job + int executeCancelCommand(PrintWriter pw, String pkgName, int userId, + boolean hasJobId, int jobId) { + if (DEBUG) { + Slog.v(TAG, "executeCancelCommand(): " + pkgName + "/" + userId + " " + jobId); + } + + int pkgUid = -1; + try { + IPackageManager pm = AppGlobals.getPackageManager(); + pkgUid = pm.getPackageUid(pkgName, 0, userId); + } catch (RemoteException e) { /* can't happen */ } + + if (pkgUid < 0) { + pw.println("Package " + pkgName + " not found."); + return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE; + } + + if (!hasJobId) { + pw.println("Canceling all jobs for " + pkgName + " in user " + userId); + if (!cancelJobsForUid(pkgUid, "cancel shell command for package")) { + pw.println("No matching jobs found."); + } + } else { + pw.println("Canceling job " + pkgName + "/#" + jobId + " in user " + userId); + if (!cancelJob(pkgUid, jobId, Process.SHELL_UID)) { + pw.println("No matching job found."); + } + } + + return 0; + } + + void setMonitorBattery(boolean enabled) { + synchronized (mLock) { + if (mBatteryController != null) { + mBatteryController.getTracker().setMonitorBatteryLocked(enabled); + } + } + } + + int getBatterySeq() { + synchronized (mLock) { + return mBatteryController != null ? mBatteryController.getTracker().getSeq() : -1; + } + } + + boolean getBatteryCharging() { + synchronized (mLock) { + return mBatteryController != null + ? mBatteryController.getTracker().isOnStablePower() : false; + } + } + + boolean getBatteryNotLow() { + synchronized (mLock) { + return mBatteryController != null + ? mBatteryController.getTracker().isBatteryNotLow() : false; + } + } + + int getStorageSeq() { + synchronized (mLock) { + return mStorageController != null ? mStorageController.getTracker().getSeq() : -1; + } + } + + boolean getStorageNotLow() { + synchronized (mLock) { + return mStorageController != null + ? mStorageController.getTracker().isStorageNotLow() : false; + } + } + + // Shell command infrastructure + int getJobState(PrintWriter pw, String pkgName, int userId, int jobId) { + try { + final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0, + userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM); + if (uid < 0) { + pw.print("unknown("); pw.print(pkgName); pw.println(")"); + return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE; + } + + synchronized (mLock) { + final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId); + if (DEBUG) Slog.d(TAG, "get-job-state " + uid + "/" + jobId + ": " + js); + if (js == null) { + pw.print("unknown("); UserHandle.formatUid(pw, uid); + pw.print("/jid"); pw.print(jobId); pw.println(")"); + return JobSchedulerShellCommand.CMD_ERR_NO_JOB; + } + + boolean printed = false; + if (mPendingJobs.contains(js)) { + pw.print("pending"); + printed = true; + } + if (isCurrentlyActiveLocked(js)) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("active"); + } + if (!ArrayUtils.contains(mStartedUsers, js.getUserId())) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("user-stopped"); + } + if (!ArrayUtils.contains(mStartedUsers, js.getSourceUserId())) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("source-user-stopped"); + } + if (mBackingUpUids.indexOfKey(js.getSourceUid()) >= 0) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("backing-up"); + } + boolean componentPresent = false; + try { + componentPresent = (AppGlobals.getPackageManager().getServiceInfo( + js.getServiceComponent(), + PackageManager.MATCH_DEBUG_TRIAGED_MISSING, + js.getUserId()) != null); + } catch (RemoteException e) { + } + if (!componentPresent) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("no-component"); + } + if (js.isReady()) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("ready"); + } + if (!printed) { + pw.print("waiting"); + } + pw.println(); + } + } catch (RemoteException e) { + // can't happen + } + return 0; + } + + void resetExecutionQuota(@NonNull String pkgName, int userId) { + mQuotaController.clearAppStats(userId, pkgName); + } + + void resetScheduleQuota() { + mQuotaTracker.clear(); + } + + void triggerDockState(boolean idleState) { + final Intent dockIntent; + if (idleState) { + dockIntent = new Intent(Intent.ACTION_DOCK_IDLE); + } else { + dockIntent = new Intent(Intent.ACTION_DOCK_ACTIVE); + } + dockIntent.setPackage("android"); + dockIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND); + getContext().sendBroadcastAsUser(dockIntent, UserHandle.ALL); + } + + static void dumpHelp(PrintWriter pw) { + pw.println("Job Scheduler (jobscheduler) dump options:"); + pw.println(" [-h] [package] ..."); + pw.println(" -h: print this help"); + pw.println(" [package] is an optional package name to limit the output to."); + } + + /** Sort jobs by caller UID, then by Job ID. */ + private static void sortJobs(List<JobStatus> jobs) { + Collections.sort(jobs, new Comparator<JobStatus>() { + @Override + public int compare(JobStatus o1, JobStatus o2) { + int uid1 = o1.getUid(); + int uid2 = o2.getUid(); + int id1 = o1.getJobId(); + int id2 = o2.getJobId(); + if (uid1 != uid2) { + return uid1 < uid2 ? -1 : 1; + } + return id1 < id2 ? -1 : (id1 > id2 ? 1 : 0); + } + }); + } + + void dumpInternal(final IndentingPrintWriter pw, int filterUid) { + final int filterUidFinal = UserHandle.getAppId(filterUid); + final long now = sSystemClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + final long nowUptime = sUptimeMillisClock.millis(); + + final Predicate<JobStatus> predicate = (js) -> { + return filterUidFinal == -1 || UserHandle.getAppId(js.getUid()) == filterUidFinal + || UserHandle.getAppId(js.getSourceUid()) == filterUidFinal; + }; + synchronized (mLock) { + mConstants.dump(pw); + for (StateController controller : mControllers) { + pw.increaseIndent(); + controller.dumpConstants(pw); + pw.decreaseIndent(); + } + pw.println(); + + for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { + pw.print(" "); + mJobRestrictions.get(i).dumpConstants(pw); + pw.println(); + } + pw.println(); + + mQuotaTracker.dump(pw); + pw.println(); + + pw.println("Started users: " + Arrays.toString(mStartedUsers)); + pw.print("Registered "); + pw.print(mJobs.size()); + pw.println(" jobs:"); + if (mJobs.size() > 0) { + final List<JobStatus> jobs = mJobs.mJobSet.getAllJobs(); + sortJobs(jobs); + for (JobStatus job : jobs) { + pw.print(" JOB #"); job.printUniqueId(pw); pw.print(": "); + pw.println(job.toShortStringExceptUniqueId()); + + // Skip printing details if the caller requested a filter + if (!predicate.test(job)) { + continue; + } + + job.dump(pw, " ", true, nowElapsed); + + + pw.print(" Restricted due to:"); + final boolean isRestricted = checkIfRestricted(job) != null; + if (isRestricted) { + for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { + final JobRestriction restriction = mJobRestrictions.get(i); + if (restriction.isJobRestricted(job)) { + final int reason = restriction.getReason(); + pw.print(" " + JobParameters.getReasonCodeDescription(reason)); + } + } + } else { + pw.print(" none"); + } + pw.println("."); + + pw.print(" Ready: "); + pw.print(isReadyToBeExecutedLocked(job)); + pw.print(" (job="); + pw.print(job.isReady()); + pw.print(" user="); + pw.print(areUsersStartedLocked(job)); + pw.print(" !restricted="); + pw.print(!isRestricted); + pw.print(" !pending="); + pw.print(!mPendingJobs.contains(job)); + pw.print(" !active="); + pw.print(!isCurrentlyActiveLocked(job)); + pw.print(" !backingup="); + pw.print(!(mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0)); + pw.print(" comp="); + pw.print(isComponentUsable(job)); + pw.println(")"); + } + } else { + pw.println(" None."); + } + for (int i=0; i<mControllers.size(); i++) { + pw.println(); + pw.println(mControllers.get(i).getClass().getSimpleName() + ":"); + pw.increaseIndent(); + mControllers.get(i).dumpControllerStateLocked(pw, predicate); + pw.decreaseIndent(); + } + pw.println(); + pw.println("Uid priority overrides:"); + for (int i=0; i< mUidPriorityOverride.size(); i++) { + int uid = mUidPriorityOverride.keyAt(i); + if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) { + pw.print(" "); pw.print(UserHandle.formatUid(uid)); + pw.print(": "); pw.println(mUidPriorityOverride.valueAt(i)); + } + } + if (mBackingUpUids.size() > 0) { + pw.println(); + pw.println("Backing up uids:"); + boolean first = true; + for (int i = 0; i < mBackingUpUids.size(); i++) { + int uid = mBackingUpUids.keyAt(i); + if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) { + if (first) { + pw.print(" "); + first = false; + } else { + pw.print(", "); + } + pw.print(UserHandle.formatUid(uid)); + } + } + pw.println(); + } + pw.println(); + mJobPackageTracker.dump(pw, "", filterUidFinal); + pw.println(); + if (mJobPackageTracker.dumpHistory(pw, "", filterUidFinal)) { + pw.println(); + } + pw.println("Pending queue:"); + for (int i=0; i<mPendingJobs.size(); i++) { + JobStatus job = mPendingJobs.get(i); + pw.print(" Pending #"); pw.print(i); pw.print(": "); + pw.println(job.toShortString()); + job.dump(pw, " ", false, nowElapsed); + int priority = evaluateJobPriorityLocked(job); + pw.print(" Evaluated priority: "); + pw.println(JobInfo.getPriorityString(priority)); + + pw.print(" Tag: "); pw.println(job.getTag()); + pw.print(" Enq: "); + TimeUtils.formatDuration(job.madePending - nowUptime, pw); + pw.println(); + } + pw.println(); + pw.println("Active jobs:"); + for (int i=0; i<mActiveServices.size(); i++) { + JobServiceContext jsc = mActiveServices.get(i); + pw.print(" Slot #"); pw.print(i); pw.print(": "); + final JobStatus job = jsc.getRunningJobLocked(); + if (job == null) { + if (jsc.mStoppedReason != null) { + pw.print("inactive since "); + TimeUtils.formatDuration(jsc.mStoppedTime, nowElapsed, pw); + pw.print(", stopped because: "); + pw.println(jsc.mStoppedReason); + } else { + pw.println("inactive"); + } + continue; + } else { + pw.println(job.toShortString()); + pw.print(" Running for: "); + TimeUtils.formatDuration(nowElapsed - jsc.getExecutionStartTimeElapsed(), pw); + pw.print(", timeout at: "); + TimeUtils.formatDuration(jsc.getTimeoutElapsed() - nowElapsed, pw); + pw.println(); + job.dump(pw, " ", false, nowElapsed); + int priority = evaluateJobPriorityLocked(jsc.getRunningJobLocked()); + pw.print(" Evaluated priority: "); + pw.println(JobInfo.getPriorityString(priority)); + + pw.print(" Active at "); + TimeUtils.formatDuration(job.madeActive - nowUptime, pw); + pw.print(", pending for "); + TimeUtils.formatDuration(job.madeActive - job.madePending, pw); + pw.println(); + } + } + if (filterUid == -1) { + pw.println(); + pw.print("mReadyToRock="); pw.println(mReadyToRock); + pw.print("mReportedActive="); pw.println(mReportedActive); + } + pw.println(); + + mConcurrencyManager.dumpLocked(pw, now, nowElapsed); + + pw.println(); + pw.print("PersistStats: "); + pw.println(mJobs.getPersistStats()); + } + pw.println(); + } + + void dumpInternalProto(final FileDescriptor fd, int filterUid) { + ProtoOutputStream proto = new ProtoOutputStream(fd); + final int filterUidFinal = UserHandle.getAppId(filterUid); + final long now = sSystemClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + final long nowUptime = sUptimeMillisClock.millis(); + final Predicate<JobStatus> predicate = (js) -> { + return filterUidFinal == -1 || UserHandle.getAppId(js.getUid()) == filterUidFinal + || UserHandle.getAppId(js.getSourceUid()) == filterUidFinal; + }; + + synchronized (mLock) { + final long settingsToken = proto.start(JobSchedulerServiceDumpProto.SETTINGS); + mConstants.dump(proto); + for (StateController controller : mControllers) { + controller.dumpConstants(proto); + } + proto.end(settingsToken); + + for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { + mJobRestrictions.get(i).dumpConstants(proto); + } + + for (int u : mStartedUsers) { + proto.write(JobSchedulerServiceDumpProto.STARTED_USERS, u); + } + + mQuotaTracker.dump(proto, JobSchedulerServiceDumpProto.QUOTA_TRACKER); + + if (mJobs.size() > 0) { + final List<JobStatus> jobs = mJobs.mJobSet.getAllJobs(); + sortJobs(jobs); + for (JobStatus job : jobs) { + final long rjToken = proto.start(JobSchedulerServiceDumpProto.REGISTERED_JOBS); + job.writeToShortProto(proto, JobSchedulerServiceDumpProto.RegisteredJob.INFO); + + // Skip printing details if the caller requested a filter + if (!predicate.test(job)) { + continue; + } + + job.dump(proto, JobSchedulerServiceDumpProto.RegisteredJob.DUMP, true, nowElapsed); + + proto.write( + JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_READY_TO_BE_EXECUTED, + isReadyToBeExecutedLocked(job)); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_READY, + job.isReady()); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.ARE_USERS_STARTED, + areUsersStartedLocked(job)); + proto.write( + JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_RESTRICTED, + checkIfRestricted(job) != null); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_PENDING, + mPendingJobs.contains(job)); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_CURRENTLY_ACTIVE, + isCurrentlyActiveLocked(job)); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_UID_BACKING_UP, + mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_COMPONENT_USABLE, + isComponentUsable(job)); + + for (JobRestriction restriction : mJobRestrictions) { + final long restrictionsToken = proto.start( + JobSchedulerServiceDumpProto.RegisteredJob.RESTRICTIONS); + proto.write(JobSchedulerServiceDumpProto.JobRestriction.REASON, + restriction.getReason()); + proto.write(JobSchedulerServiceDumpProto.JobRestriction.IS_RESTRICTING, + restriction.isJobRestricted(job)); + proto.end(restrictionsToken); + } + + proto.end(rjToken); + } + } + for (StateController controller : mControllers) { + controller.dumpControllerStateLocked( + proto, JobSchedulerServiceDumpProto.CONTROLLERS, predicate); + } + for (int i=0; i< mUidPriorityOverride.size(); i++) { + int uid = mUidPriorityOverride.keyAt(i); + if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) { + long pToken = proto.start(JobSchedulerServiceDumpProto.PRIORITY_OVERRIDES); + proto.write(JobSchedulerServiceDumpProto.PriorityOverride.UID, uid); + proto.write(JobSchedulerServiceDumpProto.PriorityOverride.OVERRIDE_VALUE, + mUidPriorityOverride.valueAt(i)); + proto.end(pToken); + } + } + for (int i = 0; i < mBackingUpUids.size(); i++) { + int uid = mBackingUpUids.keyAt(i); + if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) { + proto.write(JobSchedulerServiceDumpProto.BACKING_UP_UIDS, uid); + } + } + + mJobPackageTracker.dump(proto, JobSchedulerServiceDumpProto.PACKAGE_TRACKER, + filterUidFinal); + mJobPackageTracker.dumpHistory(proto, JobSchedulerServiceDumpProto.HISTORY, + filterUidFinal); + + for (JobStatus job : mPendingJobs) { + final long pjToken = proto.start(JobSchedulerServiceDumpProto.PENDING_JOBS); + + job.writeToShortProto(proto, PendingJob.INFO); + job.dump(proto, PendingJob.DUMP, false, nowElapsed); + proto.write(PendingJob.EVALUATED_PRIORITY, evaluateJobPriorityLocked(job)); + proto.write(PendingJob.PENDING_DURATION_MS, nowUptime - job.madePending); + + proto.end(pjToken); + } + for (JobServiceContext jsc : mActiveServices) { + final long ajToken = proto.start(JobSchedulerServiceDumpProto.ACTIVE_JOBS); + final JobStatus job = jsc.getRunningJobLocked(); + + if (job == null) { + final long ijToken = proto.start(ActiveJob.INACTIVE); + + proto.write(ActiveJob.InactiveJob.TIME_SINCE_STOPPED_MS, + nowElapsed - jsc.mStoppedTime); + if (jsc.mStoppedReason != null) { + proto.write(ActiveJob.InactiveJob.STOPPED_REASON, + jsc.mStoppedReason); + } + + proto.end(ijToken); + } else { + final long rjToken = proto.start(ActiveJob.RUNNING); + + job.writeToShortProto(proto, ActiveJob.RunningJob.INFO); + + proto.write(ActiveJob.RunningJob.RUNNING_DURATION_MS, + nowElapsed - jsc.getExecutionStartTimeElapsed()); + proto.write(ActiveJob.RunningJob.TIME_UNTIL_TIMEOUT_MS, + jsc.getTimeoutElapsed() - nowElapsed); + + job.dump(proto, ActiveJob.RunningJob.DUMP, false, nowElapsed); + + proto.write(ActiveJob.RunningJob.EVALUATED_PRIORITY, + evaluateJobPriorityLocked(jsc.getRunningJobLocked())); + + proto.write(ActiveJob.RunningJob.TIME_SINCE_MADE_ACTIVE_MS, + nowUptime - job.madeActive); + proto.write(ActiveJob.RunningJob.PENDING_DURATION_MS, + job.madeActive - job.madePending); + + proto.end(rjToken); + } + proto.end(ajToken); + } + if (filterUid == -1) { + proto.write(JobSchedulerServiceDumpProto.IS_READY_TO_ROCK, mReadyToRock); + proto.write(JobSchedulerServiceDumpProto.REPORTED_ACTIVE, mReportedActive); + } + mConcurrencyManager.dumpProtoLocked(proto, + JobSchedulerServiceDumpProto.CONCURRENCY_MANAGER, now, nowElapsed); + + mJobs.getPersistStats().dumpDebug(proto, JobSchedulerServiceDumpProto.PERSIST_STATS); + } + + proto.flush(); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java new file mode 100644 index 000000000000..1e7206287566 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java @@ -0,0 +1,496 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job; + +import android.app.ActivityManager; +import android.app.AppGlobals; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.os.BasicShellCommandHandler; +import android.os.Binder; +import android.os.UserHandle; + +import java.io.PrintWriter; + +public final class JobSchedulerShellCommand extends BasicShellCommandHandler { + public static final int CMD_ERR_NO_PACKAGE = -1000; + public static final int CMD_ERR_NO_JOB = -1001; + public static final int CMD_ERR_CONSTRAINTS = -1002; + + JobSchedulerService mInternal; + IPackageManager mPM; + + JobSchedulerShellCommand(JobSchedulerService service) { + mInternal = service; + mPM = AppGlobals.getPackageManager(); + } + + @Override + public int onCommand(String cmd) { + final PrintWriter pw = getOutPrintWriter(); + try { + switch (cmd != null ? cmd : "") { + case "run": + return runJob(pw); + case "timeout": + return timeout(pw); + case "cancel": + return cancelJob(pw); + case "monitor-battery": + return monitorBattery(pw); + case "get-battery-seq": + return getBatterySeq(pw); + case "get-battery-charging": + return getBatteryCharging(pw); + case "get-battery-not-low": + return getBatteryNotLow(pw); + case "get-storage-seq": + return getStorageSeq(pw); + case "get-storage-not-low": + return getStorageNotLow(pw); + case "get-job-state": + return getJobState(pw); + case "heartbeat": + return doHeartbeat(pw); + case "reset-execution-quota": + return resetExecutionQuota(pw); + case "reset-schedule-quota": + return resetScheduleQuota(pw); + case "trigger-dock-state": + return triggerDockState(pw); + default: + return handleDefaultCommands(cmd); + } + } catch (Exception e) { + pw.println("Exception: " + e); + } + return -1; + } + + private void checkPermission(String operation) throws Exception { + final int uid = Binder.getCallingUid(); + if (uid == 0) { + // Root can do anything. + return; + } + final int perm = mPM.checkUidPermission( + "android.permission.CHANGE_APP_IDLE_STATE", uid); + if (perm != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Uid " + uid + + " not permitted to " + operation); + } + } + + private boolean printError(int errCode, String pkgName, int userId, int jobId) { + PrintWriter pw; + switch (errCode) { + case CMD_ERR_NO_PACKAGE: + pw = getErrPrintWriter(); + pw.print("Package not found: "); + pw.print(pkgName); + pw.print(" / user "); + pw.println(userId); + return true; + + case CMD_ERR_NO_JOB: + pw = getErrPrintWriter(); + pw.print("Could not find job "); + pw.print(jobId); + pw.print(" in package "); + pw.print(pkgName); + pw.print(" / user "); + pw.println(userId); + return true; + + case CMD_ERR_CONSTRAINTS: + pw = getErrPrintWriter(); + pw.print("Job "); + pw.print(jobId); + pw.print(" in package "); + pw.print(pkgName); + pw.print(" / user "); + pw.print(userId); + pw.println(" has functional constraints but --force not specified"); + return true; + + default: + return false; + } + } + + private int runJob(PrintWriter pw) throws Exception { + checkPermission("force scheduled jobs"); + + boolean force = false; + boolean satisfied = false; + int userId = UserHandle.USER_SYSTEM; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-f": + case "--force": + force = true; + break; + + case "-s": + case "--satisfied": + satisfied = true; + break; + + case "-u": + case "--user": + userId = Integer.parseInt(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + if (force && satisfied) { + pw.println("Cannot specify both --force and --satisfied"); + return -1; + } + + final String pkgName = getNextArgRequired(); + final int jobId = Integer.parseInt(getNextArgRequired()); + + final long ident = Binder.clearCallingIdentity(); + try { + int ret = mInternal.executeRunCommand(pkgName, userId, jobId, satisfied, force); + if (printError(ret, pkgName, userId, jobId)) { + return ret; + } + + // success! + pw.print("Running job"); + if (force) { + pw.print(" [FORCED]"); + } + pw.println(); + + return ret; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private int timeout(PrintWriter pw) throws Exception { + checkPermission("force timeout jobs"); + + int userId = UserHandle.USER_ALL; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-u": + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + if (userId == UserHandle.USER_CURRENT) { + userId = ActivityManager.getCurrentUser(); + } + + final String pkgName = getNextArg(); + final String jobIdStr = getNextArg(); + final int jobId = jobIdStr != null ? Integer.parseInt(jobIdStr) : -1; + + final long ident = Binder.clearCallingIdentity(); + try { + return mInternal.executeTimeoutCommand(pw, pkgName, userId, jobIdStr != null, jobId); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private int cancelJob(PrintWriter pw) throws Exception { + checkPermission("cancel jobs"); + + int userId = UserHandle.USER_SYSTEM; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-u": + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + if (userId < 0) { + pw.println("Error: must specify a concrete user ID"); + return -1; + } + + final String pkgName = getNextArg(); + final String jobIdStr = getNextArg(); + final int jobId = jobIdStr != null ? Integer.parseInt(jobIdStr) : -1; + + final long ident = Binder.clearCallingIdentity(); + try { + return mInternal.executeCancelCommand(pw, pkgName, userId, jobIdStr != null, jobId); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private int monitorBattery(PrintWriter pw) throws Exception { + checkPermission("change battery monitoring"); + String opt = getNextArgRequired(); + boolean enabled; + if ("on".equals(opt)) { + enabled = true; + } else if ("off".equals(opt)) { + enabled = false; + } else { + getErrPrintWriter().println("Error: unknown option " + opt); + return 1; + } + final long ident = Binder.clearCallingIdentity(); + try { + mInternal.setMonitorBattery(enabled); + if (enabled) pw.println("Battery monitoring enabled"); + else pw.println("Battery monitoring disabled"); + } finally { + Binder.restoreCallingIdentity(ident); + } + return 0; + } + + private int getBatterySeq(PrintWriter pw) { + int seq = mInternal.getBatterySeq(); + pw.println(seq); + return 0; + } + + private int getBatteryCharging(PrintWriter pw) { + boolean val = mInternal.getBatteryCharging(); + pw.println(val); + return 0; + } + + private int getBatteryNotLow(PrintWriter pw) { + boolean val = mInternal.getBatteryNotLow(); + pw.println(val); + return 0; + } + + private int getStorageSeq(PrintWriter pw) { + int seq = mInternal.getStorageSeq(); + pw.println(seq); + return 0; + } + + private int getStorageNotLow(PrintWriter pw) { + boolean val = mInternal.getStorageNotLow(); + pw.println(val); + return 0; + } + + private int getJobState(PrintWriter pw) throws Exception { + checkPermission("force timeout jobs"); + + int userId = UserHandle.USER_SYSTEM; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-u": + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + if (userId == UserHandle.USER_CURRENT) { + userId = ActivityManager.getCurrentUser(); + } + + final String pkgName = getNextArgRequired(); + final String jobIdStr = getNextArgRequired(); + final int jobId = Integer.parseInt(jobIdStr); + + final long ident = Binder.clearCallingIdentity(); + try { + int ret = mInternal.getJobState(pw, pkgName, userId, jobId); + printError(ret, pkgName, userId, jobId); + return ret; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private int doHeartbeat(PrintWriter pw) throws Exception { + checkPermission("manipulate scheduler heartbeat"); + + pw.println("Heartbeat command is no longer supported"); + return -1; + } + + private int resetExecutionQuota(PrintWriter pw) throws Exception { + checkPermission("reset execution quota"); + + int userId = UserHandle.USER_SYSTEM; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-u": + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + if (userId == UserHandle.USER_CURRENT) { + userId = ActivityManager.getCurrentUser(); + } + + final String pkgName = getNextArgRequired(); + + final long ident = Binder.clearCallingIdentity(); + try { + mInternal.resetExecutionQuota(pkgName, userId); + } finally { + Binder.restoreCallingIdentity(ident); + } + return 0; + } + + private int resetScheduleQuota(PrintWriter pw) throws Exception { + checkPermission("reset schedule quota"); + + final long ident = Binder.clearCallingIdentity(); + try { + mInternal.resetScheduleQuota(); + } finally { + Binder.restoreCallingIdentity(ident); + } + return 0; + } + + private int triggerDockState(PrintWriter pw) throws Exception { + checkPermission("trigger wireless charging dock state"); + + final String opt = getNextArgRequired(); + boolean idleState; + if ("idle".equals(opt)) { + idleState = true; + } else if ("active".equals(opt)) { + idleState = false; + } else { + getErrPrintWriter().println("Error: unknown option " + opt); + return 1; + } + + final long ident = Binder.clearCallingIdentity(); + try { + mInternal.triggerDockState(idleState); + } finally { + Binder.restoreCallingIdentity(ident); + } + return 0; + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutPrintWriter(); + + pw.println("Job scheduler (jobscheduler) commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(" run [-f | --force] [-s | --satisfied] [-u | --user USER_ID] PACKAGE JOB_ID"); + pw.println(" Trigger immediate execution of a specific scheduled job. For historical"); + pw.println(" reasons, some constraints, such as battery, are ignored when this"); + pw.println(" command is called. If you don't want any constraints to be ignored,"); + pw.println(" include the -s flag."); + pw.println(" Options:"); + pw.println(" -f or --force: run the job even if technical constraints such as"); + pw.println(" connectivity are not currently met. This is incompatible with -f "); + pw.println(" and so an error will be reported if both are given."); + pw.println(" -s or --satisfied: run the job only if all constraints are met."); + pw.println(" This is incompatible with -f and so an error will be reported"); + pw.println(" if both are given."); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" the primary or system user"); + pw.println(" timeout [-u | --user USER_ID] [PACKAGE] [JOB_ID]"); + pw.println(" Trigger immediate timeout of currently executing jobs, as if their."); + pw.println(" execution timeout had expired."); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" all users"); + pw.println(" cancel [-u | --user USER_ID] PACKAGE [JOB_ID]"); + pw.println(" Cancel a scheduled job. If a job ID is not supplied, all jobs scheduled"); + pw.println(" by that package will be canceled. USE WITH CAUTION."); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" the primary or system user"); + pw.println(" heartbeat [num]"); + pw.println(" No longer used."); + pw.println(" monitor-battery [on|off]"); + pw.println(" Control monitoring of all battery changes. Off by default. Turning"); + pw.println(" on makes get-battery-seq useful."); + pw.println(" get-battery-seq"); + pw.println(" Return the last battery update sequence number that was received."); + pw.println(" get-battery-charging"); + pw.println(" Return whether the battery is currently considered to be charging."); + pw.println(" get-battery-not-low"); + pw.println(" Return whether the battery is currently considered to not be low."); + pw.println(" get-storage-seq"); + pw.println(" Return the last storage update sequence number that was received."); + pw.println(" get-storage-not-low"); + pw.println(" Return whether storage is currently considered to not be low."); + pw.println(" get-job-state [-u | --user USER_ID] PACKAGE JOB_ID"); + pw.println(" Return the current state of a job, may be any combination of:"); + pw.println(" pending: currently on the pending list, waiting to be active"); + pw.println(" active: job is actively running"); + pw.println(" user-stopped: job can't run because its user is stopped"); + pw.println(" backing-up: job can't run because app is currently backing up its data"); + pw.println(" no-component: job can't run because its component is not available"); + pw.println(" ready: job is ready to run (all constraints satisfied or bypassed)"); + pw.println(" waiting: if nothing else above is printed, job not ready to run"); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" the primary or system user"); + pw.println(" trigger-dock-state [idle|active]"); + pw.println(" Trigger wireless charging dock state. Active by default."); + pw.println(); + } + +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java new file mode 100644 index 000000000000..565ed959aeb4 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -0,0 +1,876 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.app.job.IJobCallback; +import android.app.job.IJobService; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobProtoEnums; +import android.app.job.JobWorkItem; +import android.app.usage.UsageStatsManagerInternal; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.WorkSource; +import android.util.EventLog; +import android.util.Slog; +import android.util.TimeUtils; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IBatteryStats; +import com.android.internal.util.FrameworkStatsLog; +import com.android.server.EventLogTags; +import com.android.server.LocalServices; +import com.android.server.job.controllers.JobStatus; + +/** + * Handles client binding and lifecycle of a job. Jobs execute one at a time on an instance of this + * class. + * + * There are two important interactions into this class from the + * {@link com.android.server.job.JobSchedulerService}. To execute a job and to cancel a job. + * - Execution of a new job is handled by the {@link #mAvailable}. This bit is flipped once when a + * job lands, and again when it is complete. + * - Cancelling is trickier, because there are also interactions from the client. It's possible + * the {@link com.android.server.job.JobServiceContext.JobServiceHandler} tries to process a + * {@link #doCancelLocked} after the client has already finished. This is handled by having + * {@link com.android.server.job.JobServiceContext.JobServiceHandler#handleCancelLocked} check whether + * the context is still valid. + * To mitigate this, we avoid sending duplicate onStopJob() + * calls to the client after they've specified jobFinished(). + */ +public final class JobServiceContext implements ServiceConnection { + private static final boolean DEBUG = JobSchedulerService.DEBUG; + private static final boolean DEBUG_STANDBY = JobSchedulerService.DEBUG_STANDBY; + + private static final String TAG = "JobServiceContext"; + /** Amount of time a job is allowed to execute for before being considered timed-out. */ + public static final long EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000; // 10mins. + /** Amount of time the JobScheduler waits for the initial service launch+bind. */ + private static final long OP_BIND_TIMEOUT_MILLIS = 18 * 1000; + /** Amount of time the JobScheduler will wait for a response from an app for a message. */ + private static final long OP_TIMEOUT_MILLIS = 8 * 1000; + + private static final String[] VERB_STRINGS = { + "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED" + }; + + // States that a job occupies while interacting with the client. + static final int VERB_BINDING = 0; + static final int VERB_STARTING = 1; + static final int VERB_EXECUTING = 2; + static final int VERB_STOPPING = 3; + static final int VERB_FINISHED = 4; + + // Messages that result from interactions with the client service. + /** System timed out waiting for a response. */ + private static final int MSG_TIMEOUT = 0; + + public static final int NO_PREFERRED_UID = -1; + + private final Handler mCallbackHandler; + /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */ + private final JobCompletedListener mCompletedListener; + /** Used for service binding, etc. */ + private final Context mContext; + private final Object mLock; + private final IBatteryStats mBatteryStats; + private final JobPackageTracker mJobPackageTracker; + private PowerManager.WakeLock mWakeLock; + + // Execution state. + private JobParameters mParams; + @VisibleForTesting + int mVerb; + private boolean mCancelled; + + /** + * All the information maintained about the job currently being executed. + * + * Any reads (dereferences) not done from the handler thread must be synchronized on + * {@link #mLock}. + * Writes can only be done from the handler thread, or {@link #executeRunnableJob(JobStatus)}. + */ + private JobStatus mRunningJob; + private JobCallback mRunningCallback; + /** Used to store next job to run when current job is to be preempted. */ + private int mPreferredUid; + IJobService service; + + /** + * Whether this context is free. This is set to false at the start of execution, and reset to + * true when execution is complete. + */ + @GuardedBy("mLock") + private boolean mAvailable; + /** Track start time. */ + private long mExecutionStartTimeElapsed; + /** Track when job will timeout. */ + private long mTimeoutElapsed; + + // Debugging: reason this job was last stopped. + public String mStoppedReason; + + // Debugging: time this job was last stopped. + public long mStoppedTime; + + final class JobCallback extends IJobCallback.Stub { + public String mStoppedReason; + public long mStoppedTime; + + @Override + public void acknowledgeStartMessage(int jobId, boolean ongoing) { + doAcknowledgeStartMessage(this, jobId, ongoing); + } + + @Override + public void acknowledgeStopMessage(int jobId, boolean reschedule) { + doAcknowledgeStopMessage(this, jobId, reschedule); + } + + @Override + public JobWorkItem dequeueWork(int jobId) { + return doDequeueWork(this, jobId); + } + + @Override + public boolean completeWork(int jobId, int workId) { + return doCompleteWork(this, jobId, workId); + } + + @Override + public void jobFinished(int jobId, boolean reschedule) { + doJobFinished(this, jobId, reschedule); + } + } + + JobServiceContext(JobSchedulerService service, IBatteryStats batteryStats, + JobPackageTracker tracker, Looper looper) { + this(service.getContext(), service.getLock(), batteryStats, tracker, service, looper); + } + + @VisibleForTesting + JobServiceContext(Context context, Object lock, IBatteryStats batteryStats, + JobPackageTracker tracker, JobCompletedListener completedListener, Looper looper) { + mContext = context; + mLock = lock; + mBatteryStats = batteryStats; + mJobPackageTracker = tracker; + mCallbackHandler = new JobServiceHandler(looper); + mCompletedListener = completedListener; + mAvailable = true; + mVerb = VERB_FINISHED; + mPreferredUid = NO_PREFERRED_UID; + } + + /** + * Give a job to this context for execution. Callers must first check {@link #getRunningJobLocked()} + * and ensure it is null to make sure this is a valid context. + * @param job The status of the job that we are going to run. + * @return True if the job is valid and is running. False if the job cannot be executed. + */ + boolean executeRunnableJob(JobStatus job) { + synchronized (mLock) { + if (!mAvailable) { + Slog.e(TAG, "Starting new runnable but context is unavailable > Error."); + return false; + } + + mPreferredUid = NO_PREFERRED_UID; + + mRunningJob = job; + mRunningCallback = new JobCallback(); + final boolean isDeadlineExpired = + job.hasDeadlineConstraint() && + (job.getLatestRunTimeElapsed() < sElapsedRealtimeClock.millis()); + Uri[] triggeredUris = null; + if (job.changedUris != null) { + triggeredUris = new Uri[job.changedUris.size()]; + job.changedUris.toArray(triggeredUris); + } + String[] triggeredAuthorities = null; + if (job.changedAuthorities != null) { + triggeredAuthorities = new String[job.changedAuthorities.size()]; + job.changedAuthorities.toArray(triggeredAuthorities); + } + final JobInfo ji = job.getJob(); + mParams = new JobParameters(mRunningCallback, job.getJobId(), ji.getExtras(), + ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(), + isDeadlineExpired, triggeredUris, triggeredAuthorities, job.network); + mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis(); + + final long whenDeferred = job.getWhenStandbyDeferred(); + if (whenDeferred > 0) { + final long deferral = mExecutionStartTimeElapsed - whenDeferred; + EventLog.writeEvent(EventLogTags.JOB_DEFERRED_EXECUTION, deferral); + if (DEBUG_STANDBY) { + StringBuilder sb = new StringBuilder(128); + sb.append("Starting job deferred for standby by "); + TimeUtils.formatDuration(deferral, sb); + sb.append(" ms : "); + sb.append(job.toShortString()); + Slog.v(TAG, sb.toString()); + } + } + + // Once we'e begun executing a job, we by definition no longer care whether + // it was inflated from disk with not-yet-coherent delay/deadline bounds. + job.clearPersistedUtcTimes(); + + mVerb = VERB_BINDING; + scheduleOpTimeOutLocked(); + final Intent intent = new Intent().setComponent(job.getServiceComponent()); + boolean binding = false; + try { + binding = mContext.bindServiceAsUser(intent, this, + Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND + | Context.BIND_NOT_PERCEPTIBLE, + UserHandle.of(job.getUserId())); + } catch (SecurityException e) { + // Some permission policy, for example INTERACT_ACROSS_USERS and + // android:singleUser, can result in a SecurityException being thrown from + // bindServiceAsUser(). If this happens, catch it and fail gracefully. + Slog.w(TAG, "Job service " + job.getServiceComponent().getShortClassName() + + " cannot be executed: " + e.getMessage()); + binding = false; + } + if (!binding) { + if (DEBUG) { + Slog.d(TAG, job.getServiceComponent().getShortClassName() + " unavailable."); + } + mRunningJob = null; + mRunningCallback = null; + mParams = null; + mExecutionStartTimeElapsed = 0L; + mVerb = VERB_FINISHED; + removeOpTimeOutLocked(); + return false; + } + mJobPackageTracker.noteActive(job); + FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED, + job.getSourceUid(), null, job.getBatteryName(), + FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__STARTED, + JobProtoEnums.STOP_REASON_UNKNOWN, job.getStandbyBucket(), job.getJobId(), + job.hasChargingConstraint(), + job.hasBatteryNotLowConstraint(), + job.hasStorageNotLowConstraint(), + job.hasTimingDelayConstraint(), + job.hasDeadlineConstraint(), + job.hasIdleConstraint(), + job.hasConnectivityConstraint(), + job.hasContentTriggerConstraint()); + try { + mBatteryStats.noteJobStart(job.getBatteryName(), job.getSourceUid()); + } catch (RemoteException e) { + // Whatever. + } + final String jobPackage = job.getSourcePackageName(); + final int jobUserId = job.getSourceUserId(); + UsageStatsManagerInternal usageStats = + LocalServices.getService(UsageStatsManagerInternal.class); + usageStats.setLastJobRunTime(jobPackage, jobUserId, mExecutionStartTimeElapsed); + mAvailable = false; + mStoppedReason = null; + mStoppedTime = 0; + return true; + } + } + + /** + * Used externally to query the running job. Will return null if there is no job running. + */ + JobStatus getRunningJobLocked() { + return mRunningJob; + } + + /** + * Used only for debugging. Will return <code>"<null>"</code> if there is no job running. + */ + private String getRunningJobNameLocked() { + return mRunningJob != null ? mRunningJob.toShortString() : "<null>"; + } + + /** Called externally when a job that was scheduled for execution should be cancelled. */ + @GuardedBy("mLock") + void cancelExecutingJobLocked(int reason, String debugReason) { + doCancelLocked(reason, debugReason); + } + + @GuardedBy("mLock") + void preemptExecutingJobLocked() { + doCancelLocked(JobParameters.REASON_PREEMPT, "cancelled due to preemption"); + } + + int getPreferredUid() { + return mPreferredUid; + } + + void clearPreferredUid() { + mPreferredUid = NO_PREFERRED_UID; + } + + long getExecutionStartTimeElapsed() { + return mExecutionStartTimeElapsed; + } + + long getTimeoutElapsed() { + return mTimeoutElapsed; + } + + @GuardedBy("mLock") + boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId, + String reason) { + final JobStatus executing = getRunningJobLocked(); + if (executing != null && (userId == UserHandle.USER_ALL || userId == executing.getUserId()) + && (pkgName == null || pkgName.equals(executing.getSourcePackageName())) + && (!matchJobId || jobId == executing.getJobId())) { + if (mVerb == VERB_EXECUTING) { + mParams.setStopReason(JobParameters.REASON_TIMEOUT, reason); + sendStopMessageLocked("force timeout from shell"); + return true; + } + } + return false; + } + + void doJobFinished(JobCallback cb, int jobId, boolean reschedule) { + doCallback(cb, reschedule, "app called jobFinished"); + } + + void doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule) { + doCallback(cb, reschedule, null); + } + + void doAcknowledgeStartMessage(JobCallback cb, int jobId, boolean ongoing) { + doCallback(cb, ongoing, "finished start"); + } + + JobWorkItem doDequeueWork(JobCallback cb, int jobId) { + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + assertCallerLocked(cb); + if (mVerb == VERB_STOPPING || mVerb == VERB_FINISHED) { + // This job is either all done, or on its way out. Either way, it + // should not dispatch any more work. We will pick up any remaining + // work the next time we start the job again. + return null; + } + final JobWorkItem work = mRunningJob.dequeueWorkLocked(); + if (work == null && !mRunningJob.hasExecutingWorkLocked()) { + // This will finish the job. + doCallbackLocked(false, "last work dequeued"); + } + return work; + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + boolean doCompleteWork(JobCallback cb, int jobId, int workId) { + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + assertCallerLocked(cb); + return mRunningJob.completeWorkLocked(workId); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + /** + * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work + * we intend to send to the client - we stop sending work when the service is unbound so until + * then we keep the wakelock. + * @param name The concrete component name of the service that has been connected. + * @param service The IBinder of the Service's communication channel, + */ + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + JobStatus runningJob; + synchronized (mLock) { + // This isn't strictly necessary b/c the JobServiceHandler is running on the main + // looper and at this point we can't get any binder callbacks from the client. Better + // safe than sorry. + runningJob = mRunningJob; + + if (runningJob == null || !name.equals(runningJob.getServiceComponent())) { + closeAndCleanupJobLocked(true /* needsReschedule */, + "connected for different component"); + return; + } + this.service = IJobService.Stub.asInterface(service); + final PowerManager pm = + (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + runningJob.getTag()); + wl.setWorkSource(deriveWorkSource(runningJob)); + wl.setReferenceCounted(false); + wl.acquire(); + + // We use a new wakelock instance per job. In rare cases there is a race between + // teardown following job completion/cancellation and new job service spin-up + // such that if we simply assign mWakeLock to be the new instance, we orphan + // the currently-live lock instead of cleanly replacing it. Watch for this and + // explicitly fast-forward the release if we're in that situation. + if (mWakeLock != null) { + Slog.w(TAG, "Bound new job " + runningJob + " but live wakelock " + mWakeLock + + " tag=" + mWakeLock.getTag()); + mWakeLock.release(); + } + mWakeLock = wl; + doServiceBoundLocked(); + } + } + + private WorkSource deriveWorkSource(JobStatus runningJob) { + final int jobUid = runningJob.getSourceUid(); + if (WorkSource.isChainedBatteryAttributionEnabled(mContext)) { + WorkSource workSource = new WorkSource(); + workSource.createWorkChain() + .addNode(jobUid, null) + .addNode(android.os.Process.SYSTEM_UID, "JobScheduler"); + return workSource; + } else { + return new WorkSource(jobUid); + } + } + + /** If the client service crashes we reschedule this job and clean up. */ + @Override + public void onServiceDisconnected(ComponentName name) { + synchronized (mLock) { + closeAndCleanupJobLocked(true /* needsReschedule */, "unexpectedly disconnected"); + } + } + + /** + * This class is reused across different clients, and passes itself in as a callback. Check + * whether the client exercising the callback is the client we expect. + * @return True if the binder calling is coming from the client we expect. + */ + private boolean verifyCallerLocked(JobCallback cb) { + if (mRunningCallback != cb) { + if (DEBUG) { + Slog.d(TAG, "Stale callback received, ignoring."); + } + return false; + } + return true; + } + + private void assertCallerLocked(JobCallback cb) { + if (!verifyCallerLocked(cb)) { + StringBuilder sb = new StringBuilder(128); + sb.append("Caller no longer running"); + if (cb.mStoppedReason != null) { + sb.append(", last stopped "); + TimeUtils.formatDuration(sElapsedRealtimeClock.millis() - cb.mStoppedTime, sb); + sb.append(" because: "); + sb.append(cb.mStoppedReason); + } + throw new SecurityException(sb.toString()); + } + } + + /** + * Scheduling of async messages (basically timeouts at this point). + */ + private class JobServiceHandler extends Handler { + JobServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_TIMEOUT: + synchronized (mLock) { + if (message.obj == mRunningCallback) { + handleOpTimeoutLocked(); + } else { + JobCallback jc = (JobCallback)message.obj; + StringBuilder sb = new StringBuilder(128); + sb.append("Ignoring timeout of no longer active job"); + if (jc.mStoppedReason != null) { + sb.append(", stopped "); + TimeUtils.formatDuration(sElapsedRealtimeClock.millis() + - jc.mStoppedTime, sb); + sb.append(" because: "); + sb.append(jc.mStoppedReason); + } + Slog.w(TAG, sb.toString()); + } + } + break; + default: + Slog.e(TAG, "Unrecognised message: " + message); + } + } + } + + @GuardedBy("mLock") + void doServiceBoundLocked() { + removeOpTimeOutLocked(); + handleServiceBoundLocked(); + } + + void doCallback(JobCallback cb, boolean reschedule, String reason) { + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + if (!verifyCallerLocked(cb)) { + return; + } + doCallbackLocked(reschedule, reason); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @GuardedBy("mLock") + void doCallbackLocked(boolean reschedule, String reason) { + if (DEBUG) { + Slog.d(TAG, "doCallback of : " + mRunningJob + + " v:" + VERB_STRINGS[mVerb]); + } + removeOpTimeOutLocked(); + + if (mVerb == VERB_STARTING) { + handleStartedLocked(reschedule); + } else if (mVerb == VERB_EXECUTING || + mVerb == VERB_STOPPING) { + handleFinishedLocked(reschedule, reason); + } else { + if (DEBUG) { + Slog.d(TAG, "Unrecognised callback: " + mRunningJob); + } + } + } + + @GuardedBy("mLock") + void doCancelLocked(int arg1, String debugReason) { + if (mVerb == VERB_FINISHED) { + if (DEBUG) { + Slog.d(TAG, + "Trying to process cancel for torn-down context, ignoring."); + } + return; + } + mParams.setStopReason(arg1, debugReason); + if (arg1 == JobParameters.REASON_PREEMPT) { + mPreferredUid = mRunningJob != null ? mRunningJob.getUid() : + NO_PREFERRED_UID; + } + handleCancelLocked(debugReason); + } + + /** Start the job on the service. */ + @GuardedBy("mLock") + private void handleServiceBoundLocked() { + if (DEBUG) { + Slog.d(TAG, "handleServiceBound for " + getRunningJobNameLocked()); + } + if (mVerb != VERB_BINDING) { + Slog.e(TAG, "Sending onStartJob for a job that isn't pending. " + + VERB_STRINGS[mVerb]); + closeAndCleanupJobLocked(false /* reschedule */, "started job not pending"); + return; + } + if (mCancelled) { + if (DEBUG) { + Slog.d(TAG, "Job cancelled while waiting for bind to complete. " + + mRunningJob); + } + closeAndCleanupJobLocked(true /* reschedule */, "cancelled while waiting for bind"); + return; + } + try { + mVerb = VERB_STARTING; + scheduleOpTimeOutLocked(); + service.startJob(mParams); + } catch (Exception e) { + // We catch 'Exception' because client-app malice or bugs might induce a wide + // range of possible exception-throw outcomes from startJob() and its handling + // of the client's ParcelableBundle extras. + Slog.e(TAG, "Error sending onStart message to '" + + mRunningJob.getServiceComponent().getShortClassName() + "' ", e); + } + } + + /** + * State behaviours. + * VERB_STARTING -> Successful start, change job to VERB_EXECUTING and post timeout. + * _PENDING -> Error + * _EXECUTING -> Error + * _STOPPING -> Error + */ + @GuardedBy("mLock") + private void handleStartedLocked(boolean workOngoing) { + switch (mVerb) { + case VERB_STARTING: + mVerb = VERB_EXECUTING; + if (!workOngoing) { + // Job is finished already so fast-forward to handleFinished. + handleFinishedLocked(false, "onStartJob returned false"); + return; + } + if (mCancelled) { + if (DEBUG) { + Slog.d(TAG, "Job cancelled while waiting for onStartJob to complete."); + } + // Cancelled *while* waiting for acknowledgeStartMessage from client. + handleCancelLocked(null); + return; + } + scheduleOpTimeOutLocked(); + break; + default: + Slog.e(TAG, "Handling started job but job wasn't starting! Was " + + VERB_STRINGS[mVerb] + "."); + return; + } + } + + /** + * VERB_EXECUTING -> Client called jobFinished(), clean up and notify done. + * _STOPPING -> Successful finish, clean up and notify done. + * _STARTING -> Error + * _PENDING -> Error + */ + @GuardedBy("mLock") + private void handleFinishedLocked(boolean reschedule, String reason) { + switch (mVerb) { + case VERB_EXECUTING: + case VERB_STOPPING: + closeAndCleanupJobLocked(reschedule, reason); + break; + default: + Slog.e(TAG, "Got an execution complete message for a job that wasn't being" + + "executed. Was " + VERB_STRINGS[mVerb] + "."); + } + } + + /** + * A job can be in various states when a cancel request comes in: + * VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for + * {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)} + * _STARTING -> Mark as cancelled and wait for + * {@link JobServiceContext#doAcknowledgeStartMessage} + * _EXECUTING -> call {@link #sendStopMessageLocked}}, but only if there are no callbacks + * in the message queue. + * _ENDING -> No point in doing anything here, so we ignore. + */ + @GuardedBy("mLock") + private void handleCancelLocked(String reason) { + if (JobSchedulerService.DEBUG) { + Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " " + + VERB_STRINGS[mVerb]); + } + switch (mVerb) { + case VERB_BINDING: + case VERB_STARTING: + mCancelled = true; + applyStoppedReasonLocked(reason); + break; + case VERB_EXECUTING: + sendStopMessageLocked(reason); + break; + case VERB_STOPPING: + // Nada. + break; + default: + Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb); + break; + } + } + + /** Process MSG_TIMEOUT here. */ + @GuardedBy("mLock") + private void handleOpTimeoutLocked() { + switch (mVerb) { + case VERB_BINDING: + Slog.w(TAG, "Time-out while trying to bind " + getRunningJobNameLocked() + + ", dropping."); + closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while binding"); + break; + case VERB_STARTING: + // Client unresponsive - wedged or failed to respond in time. We don't really + // know what happened so let's log it and notify the JobScheduler + // FINISHED/NO-RETRY. + Slog.w(TAG, "No response from client for onStartJob " + + getRunningJobNameLocked()); + closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while starting"); + break; + case VERB_STOPPING: + // At least we got somewhere, so fail but ask the JobScheduler to reschedule. + Slog.w(TAG, "No response from client for onStopJob " + + getRunningJobNameLocked()); + closeAndCleanupJobLocked(true /* needsReschedule */, "timed out while stopping"); + break; + case VERB_EXECUTING: + // Not an error - client ran out of time. + Slog.i(TAG, "Client timed out while executing (no jobFinished received), " + + "sending onStop: " + getRunningJobNameLocked()); + mParams.setStopReason(JobParameters.REASON_TIMEOUT, "client timed out"); + sendStopMessageLocked("timeout while executing"); + break; + default: + Slog.e(TAG, "Handling timeout for an invalid job state: " + + getRunningJobNameLocked() + ", dropping."); + closeAndCleanupJobLocked(false /* needsReschedule */, "invalid timeout"); + } + } + + /** + * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> + * VERB_STOPPING. + */ + @GuardedBy("mLock") + private void sendStopMessageLocked(String reason) { + removeOpTimeOutLocked(); + if (mVerb != VERB_EXECUTING) { + Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob); + closeAndCleanupJobLocked(false /* reschedule */, reason); + return; + } + try { + applyStoppedReasonLocked(reason); + mVerb = VERB_STOPPING; + scheduleOpTimeOutLocked(); + service.stopJob(mParams); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending onStopJob to client.", e); + // The job's host app apparently crashed during the job, so we should reschedule. + closeAndCleanupJobLocked(true /* reschedule */, "host crashed when trying to stop"); + } + } + + /** + * The provided job has finished, either by calling + * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)} + * or from acknowledging the stop message we sent. Either way, we're done tracking it and + * we want to clean up internally. + */ + @GuardedBy("mLock") + private void closeAndCleanupJobLocked(boolean reschedule, String reason) { + final JobStatus completedJob; + if (mVerb == VERB_FINISHED) { + return; + } + applyStoppedReasonLocked(reason); + completedJob = mRunningJob; + mJobPackageTracker.noteInactive(completedJob, mParams.getStopReason(), reason); + FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED, + completedJob.getSourceUid(), null, completedJob.getBatteryName(), + FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__FINISHED, + mParams.getStopReason(), completedJob.getStandbyBucket(), completedJob.getJobId(), + completedJob.hasChargingConstraint(), + completedJob.hasBatteryNotLowConstraint(), + completedJob.hasStorageNotLowConstraint(), + completedJob.hasTimingDelayConstraint(), + completedJob.hasDeadlineConstraint(), + completedJob.hasIdleConstraint(), + completedJob.hasConnectivityConstraint(), + completedJob.hasContentTriggerConstraint()); + try { + mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), mRunningJob.getSourceUid(), + mParams.getStopReason()); + } catch (RemoteException e) { + // Whatever. + } + if (mWakeLock != null) { + mWakeLock.release(); + } + mContext.unbindService(JobServiceContext.this); + mWakeLock = null; + mRunningJob = null; + mRunningCallback = null; + mParams = null; + mVerb = VERB_FINISHED; + mCancelled = false; + service = null; + mAvailable = true; + removeOpTimeOutLocked(); + mCompletedListener.onJobCompletedLocked(completedJob, reschedule); + } + + private void applyStoppedReasonLocked(String reason) { + if (reason != null && mStoppedReason == null) { + mStoppedReason = reason; + mStoppedTime = sElapsedRealtimeClock.millis(); + if (mRunningCallback != null) { + mRunningCallback.mStoppedReason = mStoppedReason; + mRunningCallback.mStoppedTime = mStoppedTime; + } + } + } + + /** + * Called when sending a message to the client, over whose execution we have no control. If + * we haven't received a response in a certain amount of time, we want to give up and carry + * on with life. + */ + private void scheduleOpTimeOutLocked() { + removeOpTimeOutLocked(); + + final long timeoutMillis; + switch (mVerb) { + case VERB_EXECUTING: + timeoutMillis = EXECUTING_TIMESLICE_MILLIS; + break; + + case VERB_BINDING: + timeoutMillis = OP_BIND_TIMEOUT_MILLIS; + break; + + default: + timeoutMillis = OP_TIMEOUT_MILLIS; + break; + } + if (DEBUG) { + Slog.d(TAG, "Scheduling time out for '" + + mRunningJob.getServiceComponent().getShortClassName() + "' jId: " + + mParams.getJobId() + ", in " + (timeoutMillis / 1000) + " s"); + } + Message m = mCallbackHandler.obtainMessage(MSG_TIMEOUT, mRunningCallback); + mCallbackHandler.sendMessageDelayed(m, timeoutMillis); + mTimeoutElapsed = sElapsedRealtimeClock.millis() + timeoutMillis; + } + + + private void removeOpTimeOutLocked() { + mCallbackHandler.removeMessages(MSG_TIMEOUT); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java new file mode 100644 index 000000000000..2f5f555817ec --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -0,0 +1,1272 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import static com.android.server.job.JobSchedulerService.sSystemClock; + +import android.annotation.Nullable; +import android.app.job.JobInfo; +import android.content.ComponentName; +import android.content.Context; +import android.net.NetworkRequest; +import android.os.Environment; +import android.os.Handler; +import android.os.PersistableBundle; +import android.os.Process; +import android.os.SystemClock; +import android.os.UserHandle; +import android.text.format.DateUtils; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseArray; +import android.util.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.BitUtils; +import com.android.internal.util.FastXmlSerializer; +import com.android.server.IoThread; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerInternal.JobStorePersistStats; +import com.android.server.job.controllers.JobStatus; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by + * reference, so none of the functions in this class should make a copy. + * Also handles read/write of persisted jobs. + * + * Note on locking: + * All callers to this class must <strong>lock on the class object they are calling</strong>. + * This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable} + * and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that + * object. + * + * Test: + * atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java + */ +public final class JobStore { + private static final String TAG = "JobStore"; + private static final boolean DEBUG = JobSchedulerService.DEBUG; + + /** Threshold to adjust how often we want to write to the db. */ + private static final long JOB_PERSIST_DELAY = 2000L; + + final Object mLock; + final Object mWriteScheduleLock; // used solely for invariants around write scheduling + final JobSet mJobSet; // per-caller-uid and per-source-uid tracking + final Context mContext; + + // Bookkeeping around incorrect boot-time system clock + private final long mXmlTimestamp; + private boolean mRtcGood; + + @GuardedBy("mWriteScheduleLock") + private boolean mWriteScheduled; + + @GuardedBy("mWriteScheduleLock") + private boolean mWriteInProgress; + + private static final Object sSingletonLock = new Object(); + private final AtomicFile mJobsFile; + /** Handler backed by IoThread for writing to disk. */ + private final Handler mIoHandler = IoThread.getHandler(); + private static JobStore sSingleton; + + private JobStorePersistStats mPersistInfo = new JobStorePersistStats(); + + /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */ + static JobStore initAndGet(JobSchedulerService jobManagerService) { + synchronized (sSingletonLock) { + if (sSingleton == null) { + sSingleton = new JobStore(jobManagerService.getContext(), + jobManagerService.getLock(), Environment.getDataDirectory()); + } + return sSingleton; + } + } + + /** + * @return A freshly initialized job store object, with no loaded jobs. + */ + @VisibleForTesting + public static JobStore initAndGetForTesting(Context context, File dataDir) { + JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir); + jobStoreUnderTest.clear(); + return jobStoreUnderTest; + } + + /** + * Construct the instance of the job store. This results in a blocking read from disk. + */ + private JobStore(Context context, Object lock, File dataDir) { + mLock = lock; + mWriteScheduleLock = new Object(); + mContext = context; + + File systemDir = new File(dataDir, "system"); + File jobDir = new File(systemDir, "job"); + jobDir.mkdirs(); + mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), "jobs"); + + mJobSet = new JobSet(); + + // If the current RTC is earlier than the timestamp on our persisted jobs file, + // we suspect that the RTC is uninitialized and so we cannot draw conclusions + // about persisted job scheduling. + // + // Note that if the persisted jobs file does not exist, we proceed with the + // assumption that the RTC is good. This is less work and is safe: if the + // clock updates to sanity then we'll be saving the persisted jobs file in that + // correct state, which is normal; or we'll wind up writing the jobs file with + // an incorrect historical timestamp. That's fine; at worst we'll reboot with + // a *correct* timestamp, see a bunch of overdue jobs, and run them; then + // settle into normal operation. + mXmlTimestamp = mJobsFile.getLastModifiedTime(); + mRtcGood = (sSystemClock.millis() > mXmlTimestamp); + + readJobMapFromDisk(mJobSet, mRtcGood); + } + + public boolean jobTimesInflatedValid() { + return mRtcGood; + } + + public boolean clockNowValidToInflate(long now) { + return now >= mXmlTimestamp; + } + + /** + * Find all the jobs that were affected by RTC clock uncertainty at boot time. Returns + * parallel lists of the existing JobStatus objects and of new, equivalent JobStatus instances + * with now-corrected time bounds. + */ + public void getRtcCorrectedJobsLocked(final ArrayList<JobStatus> toAdd, + final ArrayList<JobStatus> toRemove) { + final long elapsedNow = sElapsedRealtimeClock.millis(); + + // Find the jobs that need to be fixed up, collecting them for post-iteration + // replacement with their new versions + forEachJob(job -> { + final Pair<Long, Long> utcTimes = job.getPersistedUtcTimes(); + if (utcTimes != null) { + Pair<Long, Long> elapsedRuntimes = + convertRtcBoundsToElapsed(utcTimes, elapsedNow); + JobStatus newJob = new JobStatus(job, + elapsedRuntimes.first, elapsedRuntimes.second, + 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime()); + newJob.prepareLocked(); + toAdd.add(newJob); + toRemove.add(job); + } + }); + } + + /** + * Add a job to the master list, persisting it if necessary. If the JobStatus already exists, + * it will be replaced. + * @param jobStatus Job to add. + * @return Whether or not an equivalent JobStatus was replaced by this operation. + */ + public boolean add(JobStatus jobStatus) { + boolean replaced = mJobSet.remove(jobStatus); + mJobSet.add(jobStatus); + if (jobStatus.isPersisted()) { + maybeWriteStatusToDiskAsync(); + } + if (DEBUG) { + Slog.d(TAG, "Added job status to store: " + jobStatus); + } + return replaced; + } + + boolean containsJob(JobStatus jobStatus) { + return mJobSet.contains(jobStatus); + } + + public int size() { + return mJobSet.size(); + } + + public JobStorePersistStats getPersistStats() { + return mPersistInfo; + } + + public int countJobsForUid(int uid) { + return mJobSet.countJobsForUid(uid); + } + + /** + * Remove the provided job. Will also delete the job if it was persisted. + * @param removeFromPersisted If true, the job will be removed from the persisted job list + * immediately (if it was persisted). + * @return Whether or not the job existed to be removed. + */ + public boolean remove(JobStatus jobStatus, boolean removeFromPersisted) { + boolean removed = mJobSet.remove(jobStatus); + if (!removed) { + if (DEBUG) { + Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus); + } + return false; + } + if (removeFromPersisted && jobStatus.isPersisted()) { + maybeWriteStatusToDiskAsync(); + } + return removed; + } + + /** + * Remove the jobs of users not specified in the whitelist. + * @param whitelist Array of User IDs whose jobs are not to be removed. + */ + public void removeJobsOfNonUsers(int[] whitelist) { + mJobSet.removeJobsOfNonUsers(whitelist); + } + + @VisibleForTesting + public void clear() { + mJobSet.clear(); + maybeWriteStatusToDiskAsync(); + } + + /** + * @param userHandle User for whom we are querying the list of jobs. + * @return A list of all the jobs scheduled for the provided user. Never null. + */ + public List<JobStatus> getJobsByUser(int userHandle) { + return mJobSet.getJobsByUser(userHandle); + } + + /** + * @param uid Uid of the requesting app. + * @return All JobStatus objects for a given uid from the master list. Never null. + */ + public List<JobStatus> getJobsByUid(int uid) { + return mJobSet.getJobsByUid(uid); + } + + /** + * @param uid Uid of the requesting app. + * @param jobId Job id, specified at schedule-time. + * @return the JobStatus that matches the provided uId and jobId, or null if none found. + */ + public JobStatus getJobByUidAndJobId(int uid, int jobId) { + return mJobSet.get(uid, jobId); + } + + /** + * Iterate over the set of all jobs, invoking the supplied functor on each. This is for + * customers who need to examine each job; we'd much rather not have to generate + * transient unified collections for them to iterate over and then discard, or creating + * iterators every time a client needs to perform a sweep. + */ + public void forEachJob(Consumer<JobStatus> functor) { + mJobSet.forEachJob(null, functor); + } + + public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate, + Consumer<JobStatus> functor) { + mJobSet.forEachJob(filterPredicate, functor); + } + + public void forEachJob(int uid, Consumer<JobStatus> functor) { + mJobSet.forEachJob(uid, functor); + } + + public void forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor) { + mJobSet.forEachJobForSourceUid(sourceUid, functor); + } + + /** Version of the db schema. */ + private static final int JOBS_FILE_VERSION = 0; + /** Tag corresponds to constraints this job needs. */ + private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints"; + /** Tag corresponds to execution parameters. */ + private static final String XML_TAG_PERIODIC = "periodic"; + private static final String XML_TAG_ONEOFF = "one-off"; + private static final String XML_TAG_EXTRAS = "extras"; + + /** + * Every time the state changes we write all the jobs in one swath, instead of trying to + * track incremental changes. + */ + private void maybeWriteStatusToDiskAsync() { + synchronized (mWriteScheduleLock) { + if (!mWriteScheduled) { + if (DEBUG) { + Slog.v(TAG, "Scheduling persist of jobs to disk."); + } + mIoHandler.postDelayed(mWriteRunnable, JOB_PERSIST_DELAY); + mWriteScheduled = mWriteInProgress = true; + } + } + } + + @VisibleForTesting + public void readJobMapFromDisk(JobSet jobSet, boolean rtcGood) { + new ReadJobMapFromDiskRunnable(jobSet, rtcGood).run(); + } + + /** Write persisted JobStore state to disk synchronously. Should only be used for testing. */ + @VisibleForTesting + public void writeStatusToDiskForTesting() { + synchronized (mWriteScheduleLock) { + if (mWriteScheduled) { + throw new IllegalStateException("An asynchronous write is already scheduled."); + } + + mWriteScheduled = mWriteInProgress = true; + mWriteRunnable.run(); + } + } + + /** + * Wait for any pending write to the persistent store to clear + * @param maxWaitMillis Maximum time from present to wait + * @return {@code true} if I/O cleared as expected, {@code false} if the wait + * timed out before the pending write completed. + */ + @VisibleForTesting + public boolean waitForWriteToCompleteForTesting(long maxWaitMillis) { + final long start = SystemClock.uptimeMillis(); + final long end = start + maxWaitMillis; + synchronized (mWriteScheduleLock) { + while (mWriteInProgress) { + final long now = SystemClock.uptimeMillis(); + if (now >= end) { + // still not done and we've hit the end; failure + return false; + } + try { + mWriteScheduleLock.wait(now - start + maxWaitMillis); + } catch (InterruptedException e) { + // Spurious; keep waiting + break; + } + } + } + return true; + } + + /** + * Runnable that writes {@link #mJobSet} out to xml. + * NOTE: This Runnable locks on mLock + */ + private final Runnable mWriteRunnable = new Runnable() { + @Override + public void run() { + final long startElapsed = sElapsedRealtimeClock.millis(); + final List<JobStatus> storeCopy = new ArrayList<JobStatus>(); + // Intentionally allow new scheduling of a write operation *before* we clone + // the job set. If we reset it to false after cloning, there's a window in + // which no new write will be scheduled but mLock is not held, i.e. a new + // job might appear and fail to be recognized as needing a persist. The + // potential cost is one redundant write of an identical set of jobs in the + // rare case of that specific race, but by doing it this way we avoid quite + // a bit of lock contention. + synchronized (mWriteScheduleLock) { + mWriteScheduled = false; + } + synchronized (mLock) { + // Clone the jobs so we can release the lock before writing. + mJobSet.forEachJob(null, (job) -> { + if (job.isPersisted()) { + storeCopy.add(new JobStatus(job)); + } + }); + } + writeJobsMapImpl(storeCopy); + if (DEBUG) { + Slog.v(TAG, "Finished writing, took " + (sElapsedRealtimeClock.millis() + - startElapsed) + "ms"); + } + synchronized (mWriteScheduleLock) { + mWriteInProgress = false; + mWriteScheduleLock.notifyAll(); + } + } + + private void writeJobsMapImpl(List<JobStatus> jobList) { + int numJobs = 0; + int numSystemJobs = 0; + int numSyncJobs = 0; + try { + final long startTime = SystemClock.uptimeMillis(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(baos, StandardCharsets.UTF_8.name()); + out.startDocument(null, true); + out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + + out.startTag(null, "job-info"); + out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION)); + for (int i=0; i<jobList.size(); i++) { + JobStatus jobStatus = jobList.get(i); + if (DEBUG) { + Slog.d(TAG, "Saving job " + jobStatus.getJobId()); + } + out.startTag(null, "job"); + addAttributesToJobTag(out, jobStatus); + writeConstraintsToXml(out, jobStatus); + writeExecutionCriteriaToXml(out, jobStatus); + writeBundleToXml(jobStatus.getJob().getExtras(), out); + out.endTag(null, "job"); + + numJobs++; + if (jobStatus.getUid() == Process.SYSTEM_UID) { + numSystemJobs++; + if (isSyncJob(jobStatus)) { + numSyncJobs++; + } + } + } + out.endTag(null, "job-info"); + out.endDocument(); + + // Write out to disk in one fell swoop. + FileOutputStream fos = mJobsFile.startWrite(startTime); + fos.write(baos.toByteArray()); + mJobsFile.finishWrite(fos); + } catch (IOException e) { + if (DEBUG) { + Slog.v(TAG, "Error writing out job data.", e); + } + } catch (XmlPullParserException e) { + if (DEBUG) { + Slog.d(TAG, "Error persisting bundle.", e); + } + } finally { + mPersistInfo.countAllJobsSaved = numJobs; + mPersistInfo.countSystemServerJobsSaved = numSystemJobs; + mPersistInfo.countSystemSyncManagerJobsSaved = numSyncJobs; + } + } + + /** Write out a tag with data comprising the required fields and priority of this job and + * its client. + */ + private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus) + throws IOException { + out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId())); + out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName()); + out.attribute(null, "class", jobStatus.getServiceComponent().getClassName()); + if (jobStatus.getSourcePackageName() != null) { + out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName()); + } + if (jobStatus.getSourceTag() != null) { + out.attribute(null, "sourceTag", jobStatus.getSourceTag()); + } + out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId())); + out.attribute(null, "uid", Integer.toString(jobStatus.getUid())); + out.attribute(null, "priority", String.valueOf(jobStatus.getPriority())); + out.attribute(null, "flags", String.valueOf(jobStatus.getFlags())); + if (jobStatus.getInternalFlags() != 0) { + out.attribute(null, "internalFlags", String.valueOf(jobStatus.getInternalFlags())); + } + + out.attribute(null, "lastSuccessfulRunTime", + String.valueOf(jobStatus.getLastSuccessfulRunTime())); + out.attribute(null, "lastFailedRunTime", + String.valueOf(jobStatus.getLastFailedRunTime())); + } + + private void writeBundleToXml(PersistableBundle extras, XmlSerializer out) + throws IOException, XmlPullParserException { + out.startTag(null, XML_TAG_EXTRAS); + PersistableBundle extrasCopy = deepCopyBundle(extras, 10); + extrasCopy.saveToXml(out); + out.endTag(null, XML_TAG_EXTRAS); + } + + private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) { + if (maxDepth <= 0) { + return null; + } + PersistableBundle copy = (PersistableBundle) bundle.clone(); + Set<String> keySet = bundle.keySet(); + for (String key: keySet) { + Object o = copy.get(key); + if (o instanceof PersistableBundle) { + PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1); + copy.putPersistableBundle(key, bCopy); + } + } + return copy; + } + + /** + * Write out a tag with data identifying this job's constraints. If the constraint isn't here + * it doesn't apply. + */ + private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException { + out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS); + if (jobStatus.hasConnectivityConstraint()) { + final NetworkRequest network = jobStatus.getJob().getRequiredNetwork(); + out.attribute(null, "net-capabilities", Long.toString( + BitUtils.packBits(network.networkCapabilities.getCapabilities()))); + out.attribute(null, "net-unwanted-capabilities", Long.toString( + BitUtils.packBits(network.networkCapabilities.getUnwantedCapabilities()))); + + out.attribute(null, "net-transport-types", Long.toString( + BitUtils.packBits(network.networkCapabilities.getTransportTypes()))); + } + if (jobStatus.hasIdleConstraint()) { + out.attribute(null, "idle", Boolean.toString(true)); + } + if (jobStatus.hasChargingConstraint()) { + out.attribute(null, "charging", Boolean.toString(true)); + } + if (jobStatus.hasBatteryNotLowConstraint()) { + out.attribute(null, "battery-not-low", Boolean.toString(true)); + } + if (jobStatus.hasStorageNotLowConstraint()) { + out.attribute(null, "storage-not-low", Boolean.toString(true)); + } + out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS); + } + + private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus) + throws IOException { + final JobInfo job = jobStatus.getJob(); + if (jobStatus.getJob().isPeriodic()) { + out.startTag(null, XML_TAG_PERIODIC); + out.attribute(null, "period", Long.toString(job.getIntervalMillis())); + out.attribute(null, "flex", Long.toString(job.getFlexMillis())); + } else { + out.startTag(null, XML_TAG_ONEOFF); + } + + // If we still have the persisted times, we need to record those directly because + // we haven't yet been able to calculate the usual elapsed-timebase bounds + // correctly due to wall-clock uncertainty. + Pair <Long, Long> utcJobTimes = jobStatus.getPersistedUtcTimes(); + if (DEBUG && utcJobTimes != null) { + Slog.i(TAG, "storing original UTC timestamps for " + jobStatus); + } + + final long nowRTC = sSystemClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + if (jobStatus.hasDeadlineConstraint()) { + // Wall clock deadline. + final long deadlineWallclock = (utcJobTimes == null) + ? nowRTC + (jobStatus.getLatestRunTimeElapsed() - nowElapsed) + : utcJobTimes.second; + out.attribute(null, "deadline", Long.toString(deadlineWallclock)); + } + if (jobStatus.hasTimingDelayConstraint()) { + final long delayWallclock = (utcJobTimes == null) + ? nowRTC + (jobStatus.getEarliestRunTime() - nowElapsed) + : utcJobTimes.first; + out.attribute(null, "delay", Long.toString(delayWallclock)); + } + + // Only write out back-off policy if it differs from the default. + // This also helps the case where the job is idle -> these aren't allowed to specify + // back-off. + if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS + || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) { + out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy())); + out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis())); + } + if (job.isPeriodic()) { + out.endTag(null, XML_TAG_PERIODIC); + } else { + out.endTag(null, XML_TAG_ONEOFF); + } + } + }; + + /** + * Translate the supplied RTC times to the elapsed timebase, with clamping appropriate + * to interpreting them as a job's delay + deadline times for alarm-setting purposes. + * @param rtcTimes a Pair<Long, Long> in which {@code first} is the "delay" earliest + * allowable runtime for the job, and {@code second} is the "deadline" time at which + * the job becomes overdue. + */ + private static Pair<Long, Long> convertRtcBoundsToElapsed(Pair<Long, Long> rtcTimes, + long nowElapsed) { + final long nowWallclock = sSystemClock.millis(); + final long earliest = (rtcTimes.first > JobStatus.NO_EARLIEST_RUNTIME) + ? nowElapsed + Math.max(rtcTimes.first - nowWallclock, 0) + : JobStatus.NO_EARLIEST_RUNTIME; + final long latest = (rtcTimes.second < JobStatus.NO_LATEST_RUNTIME) + ? nowElapsed + Math.max(rtcTimes.second - nowWallclock, 0) + : JobStatus.NO_LATEST_RUNTIME; + return Pair.create(earliest, latest); + } + + private static boolean isSyncJob(JobStatus status) { + return com.android.server.content.SyncJobService.class.getName() + .equals(status.getServiceComponent().getClassName()); + } + + /** + * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't + * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}. + */ + private final class ReadJobMapFromDiskRunnable implements Runnable { + private final JobSet jobSet; + private final boolean rtcGood; + + /** + * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore, + * so that after disk read we can populate it directly. + */ + ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood) { + this.jobSet = jobSet; + this.rtcGood = rtcIsGood; + } + + @Override + public void run() { + int numJobs = 0; + int numSystemJobs = 0; + int numSyncJobs = 0; + try { + List<JobStatus> jobs; + FileInputStream fis = mJobsFile.openRead(); + synchronized (mLock) { + jobs = readJobMapImpl(fis, rtcGood); + if (jobs != null) { + long now = sElapsedRealtimeClock.millis(); + for (int i=0; i<jobs.size(); i++) { + JobStatus js = jobs.get(i); + js.prepareLocked(); + js.enqueueTime = now; + this.jobSet.add(js); + + numJobs++; + if (js.getUid() == Process.SYSTEM_UID) { + numSystemJobs++; + if (isSyncJob(js)) { + numSyncJobs++; + } + } + } + } + } + fis.close(); + } catch (FileNotFoundException e) { + if (DEBUG) { + Slog.d(TAG, "Could not find jobs file, probably there was nothing to load."); + } + } catch (XmlPullParserException | IOException e) { + Slog.wtf(TAG, "Error jobstore xml.", e); + } finally { + if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once. + mPersistInfo.countAllJobsLoaded = numJobs; + mPersistInfo.countSystemServerJobsLoaded = numSystemJobs; + mPersistInfo.countSystemSyncManagerJobsLoaded = numSyncJobs; + } + } + Slog.i(TAG, "Read " + numJobs + " jobs"); + } + + private List<JobStatus> readJobMapImpl(FileInputStream fis, boolean rtcIsGood) + throws XmlPullParserException, IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(fis, StandardCharsets.UTF_8.name()); + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG && + eventType != XmlPullParser.END_DOCUMENT) { + eventType = parser.next(); + Slog.d(TAG, "Start tag: " + parser.getName()); + } + if (eventType == XmlPullParser.END_DOCUMENT) { + if (DEBUG) { + Slog.d(TAG, "No persisted jobs."); + } + return null; + } + + String tagName = parser.getName(); + if ("job-info".equals(tagName)) { + final List<JobStatus> jobs = new ArrayList<JobStatus>(); + // Read in version info. + try { + int version = Integer.parseInt(parser.getAttributeValue(null, "version")); + if (version != JOBS_FILE_VERSION) { + Slog.d(TAG, "Invalid version number, aborting jobs file read."); + return null; + } + } catch (NumberFormatException e) { + Slog.e(TAG, "Invalid version number, aborting jobs file read."); + return null; + } + eventType = parser.next(); + do { + // Read each <job/> + if (eventType == XmlPullParser.START_TAG) { + tagName = parser.getName(); + // Start reading job. + if ("job".equals(tagName)) { + JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser); + if (persistedJob != null) { + if (DEBUG) { + Slog.d(TAG, "Read out " + persistedJob); + } + jobs.add(persistedJob); + } else { + Slog.d(TAG, "Error reading job from file."); + } + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + return jobs; + } + return null; + } + + /** + * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call + * will take the parser into the body of the job tag. + * @return Newly instantiated job holding all the information we just read out of the xml tag. + */ + private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser) + throws XmlPullParserException, IOException { + JobInfo.Builder jobBuilder; + int uid, sourceUserId; + long lastSuccessfulRunTime; + long lastFailedRunTime; + int internalFlags = 0; + + // Read out job identifier attributes and priority. + try { + jobBuilder = buildBuilderFromXml(parser); + jobBuilder.setPersisted(true); + uid = Integer.parseInt(parser.getAttributeValue(null, "uid")); + + String val = parser.getAttributeValue(null, "priority"); + if (val != null) { + jobBuilder.setPriority(Integer.parseInt(val)); + } + val = parser.getAttributeValue(null, "flags"); + if (val != null) { + jobBuilder.setFlags(Integer.parseInt(val)); + } + val = parser.getAttributeValue(null, "internalFlags"); + if (val != null) { + internalFlags = Integer.parseInt(val); + } + val = parser.getAttributeValue(null, "sourceUserId"); + sourceUserId = val == null ? -1 : Integer.parseInt(val); + + val = parser.getAttributeValue(null, "lastSuccessfulRunTime"); + lastSuccessfulRunTime = val == null ? 0 : Long.parseLong(val); + + val = parser.getAttributeValue(null, "lastFailedRunTime"); + lastFailedRunTime = val == null ? 0 : Long.parseLong(val); + } catch (NumberFormatException e) { + Slog.e(TAG, "Error parsing job's required fields, skipping"); + return null; + } + + String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName"); + final String sourceTag = parser.getAttributeValue(null, "sourceTag"); + + int eventType; + // Read out constraints tag. + do { + eventType = parser.next(); + } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG. + + if (!(eventType == XmlPullParser.START_TAG && + XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) { + // Expecting a <constraints> start tag. + return null; + } + try { + buildConstraintsFromXml(jobBuilder, parser); + } catch (NumberFormatException e) { + Slog.d(TAG, "Error reading constraints, skipping."); + return null; + } + parser.next(); // Consume </constraints> + + // Read out execution parameters tag. + do { + eventType = parser.next(); + } while (eventType == XmlPullParser.TEXT); + if (eventType != XmlPullParser.START_TAG) { + return null; + } + + // Tuple of (earliest runtime, latest runtime) in UTC. + final Pair<Long, Long> rtcRuntimes; + try { + rtcRuntimes = buildRtcExecutionTimesFromXml(parser); + } catch (NumberFormatException e) { + if (DEBUG) { + Slog.d(TAG, "Error parsing execution time parameters, skipping."); + } + return null; + } + + final long elapsedNow = sElapsedRealtimeClock.millis(); + Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, elapsedNow); + + if (XML_TAG_PERIODIC.equals(parser.getName())) { + try { + String val = parser.getAttributeValue(null, "period"); + final long periodMillis = Long.parseLong(val); + val = parser.getAttributeValue(null, "flex"); + final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis; + jobBuilder.setPeriodic(periodMillis, flexMillis); + // As a sanity check, cap the recreated run time to be no later than flex+period + // from now. This is the latest the periodic could be pushed out. This could + // happen if the periodic ran early (at flex time before period), and then the + // device rebooted. + if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) { + final long clampedLateRuntimeElapsed = elapsedNow + flexMillis + + periodMillis; + final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed + - flexMillis; + Slog.w(TAG, + String.format("Periodic job for uid='%d' persisted run-time is" + + " too big [%s, %s]. Clamping to [%s,%s]", + uid, + DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000), + DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000), + DateUtils.formatElapsedTime( + clampedEarlyRuntimeElapsed / 1000), + DateUtils.formatElapsedTime( + clampedLateRuntimeElapsed / 1000)) + ); + elapsedRuntimes = + Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed); + } + } catch (NumberFormatException e) { + Slog.d(TAG, "Error reading periodic execution criteria, skipping."); + return null; + } + } else if (XML_TAG_ONEOFF.equals(parser.getName())) { + try { + if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) { + jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow); + } + if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) { + jobBuilder.setOverrideDeadline( + elapsedRuntimes.second - elapsedNow); + } + } catch (NumberFormatException e) { + Slog.d(TAG, "Error reading job execution criteria, skipping."); + return null; + } + } else { + if (DEBUG) { + Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName()); + } + // Expecting a parameters start tag. + return null; + } + maybeBuildBackoffPolicyFromXml(jobBuilder, parser); + + parser.nextTag(); // Consume parameters end tag. + + // Read out extras Bundle. + do { + eventType = parser.next(); + } while (eventType == XmlPullParser.TEXT); + if (!(eventType == XmlPullParser.START_TAG + && XML_TAG_EXTRAS.equals(parser.getName()))) { + if (DEBUG) { + Slog.d(TAG, "Error reading extras, skipping."); + } + return null; + } + + PersistableBundle extras = PersistableBundle.restoreFromXml(parser); + jobBuilder.setExtras(extras); + parser.nextTag(); // Consume </extras> + + final JobInfo builtJob; + try { + builtJob = jobBuilder.build(); + } catch (Exception e) { + Slog.w(TAG, "Unable to build job from XML, ignoring: " + + jobBuilder.summarize()); + return null; + } + + // Migrate sync jobs forward from earlier, incomplete representation + if ("android".equals(sourcePackageName) + && extras != null + && extras.getBoolean("SyncManagerJob", false)) { + sourcePackageName = extras.getString("owningPackage", sourcePackageName); + if (DEBUG) { + Slog.i(TAG, "Fixing up sync job source package name from 'android' to '" + + sourcePackageName + "'"); + } + } + + // And now we're done + JobSchedulerInternal service = LocalServices.getService(JobSchedulerInternal.class); + final int appBucket = JobSchedulerService.standbyBucketForPackage(sourcePackageName, + sourceUserId, elapsedNow); + JobStatus js = new JobStatus( + jobBuilder.build(), uid, sourcePackageName, sourceUserId, + appBucket, sourceTag, + elapsedRuntimes.first, elapsedRuntimes.second, + lastSuccessfulRunTime, lastFailedRunTime, + (rtcIsGood) ? null : rtcRuntimes, internalFlags); + return js; + } + + private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException { + // Pull out required fields from <job> attributes. + int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid")); + String packageName = parser.getAttributeValue(null, "package"); + String className = parser.getAttributeValue(null, "class"); + ComponentName cname = new ComponentName(packageName, className); + + return new JobInfo.Builder(jobId, cname); + } + + private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { + String val; + + final String netCapabilities = parser.getAttributeValue(null, "net-capabilities"); + final String netUnwantedCapabilities = parser.getAttributeValue( + null, "net-unwanted-capabilities"); + final String netTransportTypes = parser.getAttributeValue(null, "net-transport-types"); + if (netCapabilities != null && netTransportTypes != null) { + final NetworkRequest request = new NetworkRequest.Builder().build(); + final long unwantedCapabilities = netUnwantedCapabilities != null + ? Long.parseLong(netUnwantedCapabilities) + : BitUtils.packBits(request.networkCapabilities.getUnwantedCapabilities()); + + // We're okay throwing NFE here; caught by caller + request.networkCapabilities.setCapabilities( + BitUtils.unpackBits(Long.parseLong(netCapabilities)), + BitUtils.unpackBits(unwantedCapabilities)); + request.networkCapabilities.setTransportTypes( + BitUtils.unpackBits(Long.parseLong(netTransportTypes))); + jobBuilder.setRequiredNetwork(request); + } else { + // Read legacy values + val = parser.getAttributeValue(null, "connectivity"); + if (val != null) { + jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + } + val = parser.getAttributeValue(null, "metered"); + if (val != null) { + jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED); + } + val = parser.getAttributeValue(null, "unmetered"); + if (val != null) { + jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); + } + val = parser.getAttributeValue(null, "not-roaming"); + if (val != null) { + jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING); + } + } + + val = parser.getAttributeValue(null, "idle"); + if (val != null) { + jobBuilder.setRequiresDeviceIdle(true); + } + val = parser.getAttributeValue(null, "charging"); + if (val != null) { + jobBuilder.setRequiresCharging(true); + } + val = parser.getAttributeValue(null, "battery-not-low"); + if (val != null) { + jobBuilder.setRequiresBatteryNotLow(true); + } + val = parser.getAttributeValue(null, "storage-not-low"); + if (val != null) { + jobBuilder.setRequiresStorageNotLow(true); + } + } + + /** + * Builds the back-off policy out of the params tag. These attributes may not exist, depending + * on whether the back-off was set when the job was first scheduled. + */ + private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { + String val = parser.getAttributeValue(null, "initial-backoff"); + if (val != null) { + long initialBackoff = Long.parseLong(val); + val = parser.getAttributeValue(null, "backoff-policy"); + int backoffPolicy = Integer.parseInt(val); // Will throw NFE which we catch higher up. + jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy); + } + } + + /** + * Extract a job's earliest/latest run time data from XML. These are returned in + * unadjusted UTC wall clock time, because we do not yet know whether the system + * clock is reliable for purposes of calculating deltas from 'now'. + * + * @param parser + * @return A Pair of timestamps in UTC wall-clock time. The first is the earliest + * time at which the job is to become runnable, and the second is the deadline at + * which it becomes overdue to execute. + * @throws NumberFormatException + */ + private Pair<Long, Long> buildRtcExecutionTimesFromXml(XmlPullParser parser) + throws NumberFormatException { + String val; + // Pull out execution time data. + val = parser.getAttributeValue(null, "delay"); + final long earliestRunTimeRtc = (val != null) + ? Long.parseLong(val) + : JobStatus.NO_EARLIEST_RUNTIME; + val = parser.getAttributeValue(null, "deadline"); + final long latestRunTimeRtc = (val != null) + ? Long.parseLong(val) + : JobStatus.NO_LATEST_RUNTIME; + return Pair.create(earliestRunTimeRtc, latestRunTimeRtc); + } + } + + /** Set of all tracked jobs. */ + @VisibleForTesting + public static final class JobSet { + @VisibleForTesting // Key is the getUid() originator of the jobs in each sheaf + final SparseArray<ArraySet<JobStatus>> mJobs; + + @VisibleForTesting // Same data but with the key as getSourceUid() of the jobs in each sheaf + final SparseArray<ArraySet<JobStatus>> mJobsPerSourceUid; + + public JobSet() { + mJobs = new SparseArray<ArraySet<JobStatus>>(); + mJobsPerSourceUid = new SparseArray<>(); + } + + public List<JobStatus> getJobsByUid(int uid) { + ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>(); + ArraySet<JobStatus> jobs = mJobs.get(uid); + if (jobs != null) { + matchingJobs.addAll(jobs); + } + return matchingJobs; + } + + // By user, not by uid, so we need to traverse by key and check + public List<JobStatus> getJobsByUser(int userId) { + final ArrayList<JobStatus> result = new ArrayList<JobStatus>(); + for (int i = mJobsPerSourceUid.size() - 1; i >= 0; i--) { + if (UserHandle.getUserId(mJobsPerSourceUid.keyAt(i)) == userId) { + final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(i); + if (jobs != null) { + result.addAll(jobs); + } + } + } + return result; + } + + public boolean add(JobStatus job) { + final int uid = job.getUid(); + final int sourceUid = job.getSourceUid(); + ArraySet<JobStatus> jobs = mJobs.get(uid); + if (jobs == null) { + jobs = new ArraySet<JobStatus>(); + mJobs.put(uid, jobs); + } + ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid); + if (jobsForSourceUid == null) { + jobsForSourceUid = new ArraySet<>(); + mJobsPerSourceUid.put(sourceUid, jobsForSourceUid); + } + final boolean added = jobs.add(job); + final boolean addedInSource = jobsForSourceUid.add(job); + if (added != addedInSource) { + Slog.wtf(TAG, "mJobs and mJobsPerSourceUid mismatch; caller= " + added + + " source= " + addedInSource); + } + return added || addedInSource; + } + + public boolean remove(JobStatus job) { + final int uid = job.getUid(); + final ArraySet<JobStatus> jobs = mJobs.get(uid); + final int sourceUid = job.getSourceUid(); + final ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid); + final boolean didRemove = jobs != null && jobs.remove(job); + final boolean sourceRemove = jobsForSourceUid != null && jobsForSourceUid.remove(job); + if (didRemove != sourceRemove) { + Slog.wtf(TAG, "Job presence mismatch; caller=" + didRemove + + " source=" + sourceRemove); + } + if (didRemove || sourceRemove) { + // no more jobs for this uid? let the now-empty set objects be GC'd. + if (jobs != null && jobs.size() == 0) { + mJobs.remove(uid); + } + if (jobsForSourceUid != null && jobsForSourceUid.size() == 0) { + mJobsPerSourceUid.remove(sourceUid); + } + return true; + } + return false; + } + + /** + * Removes the jobs of all users not specified by the whitelist of user ids. + * This will remove jobs scheduled *by* non-existent users as well as jobs scheduled *for* + * non-existent users + */ + public void removeJobsOfNonUsers(final int[] whitelist) { + final Predicate<JobStatus> noSourceUser = + job -> !ArrayUtils.contains(whitelist, job.getSourceUserId()); + final Predicate<JobStatus> noCallingUser = + job -> !ArrayUtils.contains(whitelist, job.getUserId()); + removeAll(noSourceUser.or(noCallingUser)); + } + + private void removeAll(Predicate<JobStatus> predicate) { + for (int jobSetIndex = mJobs.size() - 1; jobSetIndex >= 0; jobSetIndex--) { + final ArraySet<JobStatus> jobs = mJobs.valueAt(jobSetIndex); + jobs.removeIf(predicate); + if (jobs.size() == 0) { + mJobs.removeAt(jobSetIndex); + } + } + for (int jobSetIndex = mJobsPerSourceUid.size() - 1; jobSetIndex >= 0; jobSetIndex--) { + final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(jobSetIndex); + jobs.removeIf(predicate); + if (jobs.size() == 0) { + mJobsPerSourceUid.removeAt(jobSetIndex); + } + } + } + + public boolean contains(JobStatus job) { + final int uid = job.getUid(); + ArraySet<JobStatus> jobs = mJobs.get(uid); + return jobs != null && jobs.contains(job); + } + + public JobStatus get(int uid, int jobId) { + ArraySet<JobStatus> jobs = mJobs.get(uid); + if (jobs != null) { + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.valueAt(i); + if (job.getJobId() == jobId) { + return job; + } + } + } + return null; + } + + // Inefficient; use only for testing + public List<JobStatus> getAllJobs() { + ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size()); + for (int i = mJobs.size() - 1; i >= 0; i--) { + ArraySet<JobStatus> jobs = mJobs.valueAt(i); + if (jobs != null) { + // Use a for loop over the ArraySet, so we don't need to make its + // optional collection class iterator implementation or have to go + // through a temporary array from toArray(). + for (int j = jobs.size() - 1; j >= 0; j--) { + allJobs.add(jobs.valueAt(j)); + } + } + } + return allJobs; + } + + public void clear() { + mJobs.clear(); + mJobsPerSourceUid.clear(); + } + + public int size() { + int total = 0; + for (int i = mJobs.size() - 1; i >= 0; i--) { + total += mJobs.valueAt(i).size(); + } + return total; + } + + // We only want to count the jobs that this uid has scheduled on its own + // behalf, not those that the app has scheduled on someone else's behalf. + public int countJobsForUid(int uid) { + int total = 0; + ArraySet<JobStatus> jobs = mJobs.get(uid); + if (jobs != null) { + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.valueAt(i); + if (job.getUid() == job.getSourceUid()) { + total++; + } + } + } + return total; + } + + public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate, + Consumer<JobStatus> functor) { + for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) { + ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex); + if (jobs != null) { + for (int i = jobs.size() - 1; i >= 0; i--) { + final JobStatus jobStatus = jobs.valueAt(i); + if ((filterPredicate == null) || filterPredicate.test(jobStatus)) { + functor.accept(jobStatus); + } + } + } + } + } + + public void forEachJob(int callingUid, Consumer<JobStatus> functor) { + ArraySet<JobStatus> jobs = mJobs.get(callingUid); + if (jobs != null) { + for (int i = jobs.size() - 1; i >= 0; i--) { + functor.accept(jobs.valueAt(i)); + } + } + } + + public void forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor) { + final ArraySet<JobStatus> jobs = mJobsPerSourceUid.get(sourceUid); + if (jobs != null) { + for (int i = jobs.size() - 1; i >= 0; i--) { + functor.accept(jobs.valueAt(i)); + } + } + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java new file mode 100644 index 000000000000..cb3c43714111 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job; + +import android.annotation.NonNull; + +import com.android.server.job.controllers.JobStatus; + +import java.util.List; + +/** + * Interface through which a {@link com.android.server.job.controllers.StateController} informs + * the {@link com.android.server.job.JobSchedulerService} that there are some tasks potentially + * ready to be run. + */ +public interface StateChangedListener { + /** + * Called by the controller to notify the JobManager that it should check on the state of a + * task. + */ + public void onControllerStateChanged(); + + /** + * Called by the controller to notify the JobManager that regardless of the state of the task, + * it must be run immediately. + * @param jobStatus The state of the task which is to be run immediately. <strong>null + * indicates to the scheduler that any ready jobs should be flushed.</strong> + */ + public void onRunJobNow(JobStatus jobStatus); + + public void onDeviceIdleStateChanged(boolean deviceIdle); + + /** + * Called when these jobs are added or removed from the + * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket. + */ + void onRestrictedBucketChanged(@NonNull List<JobStatus> jobs); +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING new file mode 100644 index 000000000000..484fec31e594 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING @@ -0,0 +1,45 @@ +{ + "presubmit": [ + { + "name": "CtsJobSchedulerTestCases", + "options": [ + {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.LargeTest"} + ] + }, + { + "name": "FrameworksMockingServicesTests", + "options": [ + {"include-filter": "com.android.server.job"}, + {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.FlakyTest"} + ] + }, + { + "name": "FrameworksServicesTests", + "options": [ + {"include-filter": "com.android.server.job"}, + {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.FlakyTest"} + ] + } + ], + "postsubmit": [ + { + "name": "CtsJobSchedulerTestCases" + }, + { + "name": "FrameworksMockingServicesTests", + "options": [ + {"include-filter": "com.android.server.job"} + ] + }, + { + "name": "FrameworksServicesTests", + "options": [ + {"include-filter": "com.android.server.job"} + ] + } + ] +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java new file mode 100644 index 000000000000..1645bcb928c1 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers; + +import static com.android.server.job.JobSchedulerService.NEVER_INDEX; + +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.Log; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.AppStateTracker; +import com.android.server.AppStateTracker.Listener; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobStore; +import com.android.server.job.StateControllerProto; +import com.android.server.job.StateControllerProto.BackgroundJobsController.TrackedJob; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Tracks the following pieces of JobStatus state: + * + * - the CONSTRAINT_BACKGROUND_NOT_RESTRICTED general constraint bit, which + * is used to selectively permit battery-saver exempted jobs to run; and + * + * - the uid-active boolean state expressed by the AppStateTracker. Jobs in 'active' + * uids are inherently eligible to run jobs regardless of the uid's standby bucket. + */ +public final class BackgroundJobsController extends StateController { + private static final String TAG = "JobScheduler.Background"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + // Tri-state about possible "is this uid 'active'?" knowledge + static final int UNKNOWN = 0; + static final int KNOWN_ACTIVE = 1; + static final int KNOWN_INACTIVE = 2; + + private final AppStateTracker mAppStateTracker; + + public BackgroundJobsController(JobSchedulerService service) { + super(service); + + mAppStateTracker = Objects.requireNonNull( + LocalServices.getService(AppStateTracker.class)); + mAppStateTracker.addListener(mForceAppStandbyListener); + } + + @Override + public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { + updateSingleJobRestrictionLocked(jobStatus, UNKNOWN); + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate) { + } + + @Override + public void dumpControllerStateLocked(final IndentingPrintWriter pw, + final Predicate<JobStatus> predicate) { + mAppStateTracker.dump(pw); + pw.println(); + + mService.getJobStore().forEachJob(predicate, (jobStatus) -> { + final int uid = jobStatus.getSourceUid(); + final String sourcePkg = jobStatus.getSourcePackageName(); + pw.print("#"); + jobStatus.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, uid); + pw.print(mAppStateTracker.isUidActive(uid) ? " active" : " idle"); + if (mAppStateTracker.isUidPowerSaveWhitelisted(uid) || + mAppStateTracker.isUidTempPowerSaveWhitelisted(uid)) { + pw.print(", whitelisted"); + } + pw.print(": "); + pw.print(sourcePkg); + + pw.print(" [RUN_ANY_IN_BACKGROUND "); + pw.print(mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(uid, sourcePkg) + ? "allowed]" : "disallowed]"); + + if ((jobStatus.satisfiedConstraints + & JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) { + pw.println(" RUNNABLE"); + } else { + pw.println(" WAITING"); + } + }); + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.BACKGROUND); + + mAppStateTracker.dumpProto(proto, + StateControllerProto.BackgroundJobsController.APP_STATE_TRACKER); + + mService.getJobStore().forEachJob(predicate, (jobStatus) -> { + final long jsToken = + proto.start(StateControllerProto.BackgroundJobsController.TRACKED_JOBS); + + jobStatus.writeToShortProto(proto, TrackedJob.INFO); + final int sourceUid = jobStatus.getSourceUid(); + proto.write(TrackedJob.SOURCE_UID, sourceUid); + final String sourcePkg = jobStatus.getSourcePackageName(); + proto.write(TrackedJob.SOURCE_PACKAGE_NAME, sourcePkg); + + proto.write(TrackedJob.IS_IN_FOREGROUND, mAppStateTracker.isUidActive(sourceUid)); + proto.write(TrackedJob.IS_WHITELISTED, + mAppStateTracker.isUidPowerSaveWhitelisted(sourceUid) || + mAppStateTracker.isUidTempPowerSaveWhitelisted(sourceUid)); + + proto.write(TrackedJob.CAN_RUN_ANY_IN_BACKGROUND, + mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(sourceUid, sourcePkg)); + + proto.write(TrackedJob.ARE_CONSTRAINTS_SATISFIED, + (jobStatus.satisfiedConstraints & + JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0); + + proto.end(jsToken); + }); + + proto.end(mToken); + proto.end(token); + } + + private void updateAllJobRestrictionsLocked() { + updateJobRestrictionsLocked(/*filterUid=*/ -1, UNKNOWN); + } + + private void updateJobRestrictionsForUidLocked(int uid, boolean isActive) { + updateJobRestrictionsLocked(uid, (isActive) ? KNOWN_ACTIVE : KNOWN_INACTIVE); + } + + private void updateJobRestrictionsLocked(int filterUid, int newActiveState) { + final UpdateJobFunctor updateTrackedJobs = new UpdateJobFunctor(newActiveState); + + final long start = DEBUG ? SystemClock.elapsedRealtimeNanos() : 0; + + final JobStore store = mService.getJobStore(); + if (filterUid > 0) { + store.forEachJobForSourceUid(filterUid, updateTrackedJobs); + } else { + store.forEachJob(updateTrackedJobs); + } + + final long time = DEBUG ? (SystemClock.elapsedRealtimeNanos() - start) : 0; + if (DEBUG) { + Slog.d(TAG, String.format( + "Job status updated: %d/%d checked/total jobs, %d us", + updateTrackedJobs.mCheckedCount, + updateTrackedJobs.mTotalCount, + (time / 1000) + )); + } + + if (updateTrackedJobs.mChanged) { + mStateChangedListener.onControllerStateChanged(); + } + } + + boolean updateSingleJobRestrictionLocked(JobStatus jobStatus, int activeState) { + final int uid = jobStatus.getSourceUid(); + final String packageName = jobStatus.getSourcePackageName(); + + final boolean canRun = !mAppStateTracker.areJobsRestricted(uid, packageName, + (jobStatus.getInternalFlags() & JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) + != 0); + + final boolean isActive; + if (activeState == UNKNOWN) { + isActive = mAppStateTracker.isUidActive(uid); + } else { + isActive = (activeState == KNOWN_ACTIVE); + } + if (isActive && jobStatus.getStandbyBucket() == NEVER_INDEX) { + Slog.wtf(TAG, "App " + packageName + " became active but still in NEVER bucket"); + } + boolean didChange = jobStatus.setBackgroundNotRestrictedConstraintSatisfied(canRun); + didChange |= jobStatus.setUidActive(isActive); + return didChange; + } + + private final class UpdateJobFunctor implements Consumer<JobStatus> { + final int activeState; + boolean mChanged = false; + int mTotalCount = 0; + int mCheckedCount = 0; + + public UpdateJobFunctor(int newActiveState) { + activeState = newActiveState; + } + + @Override + public void accept(JobStatus jobStatus) { + mTotalCount++; + mCheckedCount++; + if (updateSingleJobRestrictionLocked(jobStatus, activeState)) { + mChanged = true; + } + } + } + + private final Listener mForceAppStandbyListener = new Listener() { + @Override + public void updateAllJobs() { + synchronized (mLock) { + updateAllJobRestrictionsLocked(); + } + } + + @Override + public void updateJobsForUid(int uid, boolean isActive) { + synchronized (mLock) { + updateJobRestrictionsForUidLocked(uid, isActive); + } + } + + @Override + public void updateJobsForUidPackage(int uid, String packageName, boolean isActive) { + synchronized (mLock) { + updateJobRestrictionsForUidLocked(uid, isActive); + } + } + }; +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java new file mode 100644 index 000000000000..461ef21af7ee --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.BatteryManagerInternal; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; + +import java.util.function.Predicate; + +/** + * Simple controller that tracks whether the phone is charging or not. The phone is considered to + * be charging when it's been plugged in for more than two minutes, and the system has broadcast + * ACTION_BATTERY_OK. + */ +public final class BatteryController extends RestrictingController { + private static final String TAG = "JobScheduler.Battery"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>(); + private ChargingTracker mChargeTracker; + + @VisibleForTesting + public ChargingTracker getTracker() { + return mChargeTracker; + } + + public BatteryController(JobSchedulerService service) { + super(service); + mChargeTracker = new ChargingTracker(); + mChargeTracker.startTracking(); + } + + @Override + public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { + if (taskStatus.hasPowerConstraint()) { + mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY); + taskStatus.setChargingConstraintSatisfied(mChargeTracker.isOnStablePower()); + taskStatus.setBatteryNotLowConstraintSatisfied(mChargeTracker.isBatteryNotLow()); + } + } + + @Override + public void startTrackingRestrictedJobLocked(JobStatus jobStatus) { + maybeStartTrackingJobLocked(jobStatus, null); + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) { + mTrackedTasks.remove(taskStatus); + } + } + + @Override + public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) { + if (!jobStatus.hasPowerConstraint()) { + maybeStopTrackingJobLocked(jobStatus, null, false); + } + } + + private void maybeReportNewChargingStateLocked() { + final boolean stablePower = mChargeTracker.isOnStablePower(); + final boolean batteryNotLow = mChargeTracker.isBatteryNotLow(); + if (DEBUG) { + Slog.d(TAG, "maybeReportNewChargingStateLocked: " + stablePower); + } + boolean reportChange = false; + for (int i = mTrackedTasks.size() - 1; i >= 0; i--) { + final JobStatus ts = mTrackedTasks.valueAt(i); + boolean previous = ts.setChargingConstraintSatisfied(stablePower); + if (previous != stablePower) { + reportChange = true; + } + previous = ts.setBatteryNotLowConstraintSatisfied(batteryNotLow); + if (previous != batteryNotLow) { + reportChange = true; + } + } + if (stablePower || batteryNotLow) { + // If one of our conditions has been satisfied, always schedule any newly ready jobs. + mStateChangedListener.onRunJobNow(null); + } else if (reportChange) { + // Otherwise, just let the job scheduler know the state has changed and take care of it + // as it thinks is best. + mStateChangedListener.onControllerStateChanged(); + } + } + + public final class ChargingTracker extends BroadcastReceiver { + /** + * Track whether we're "charging", where charging means that we're ready to commit to + * doing work. + */ + private boolean mCharging; + /** Keep track of whether the battery is charged enough that we want to do work. */ + private boolean mBatteryHealthy; + /** Sequence number of last broadcast. */ + private int mLastBatterySeq = -1; + + private BroadcastReceiver mMonitor; + + public ChargingTracker() { + } + + public void startTracking() { + IntentFilter filter = new IntentFilter(); + + // Battery health. + filter.addAction(Intent.ACTION_BATTERY_LOW); + filter.addAction(Intent.ACTION_BATTERY_OKAY); + // Charging/not charging. + filter.addAction(BatteryManager.ACTION_CHARGING); + filter.addAction(BatteryManager.ACTION_DISCHARGING); + mContext.registerReceiver(this, filter); + + // Initialise tracker state. + BatteryManagerInternal batteryManagerInternal = + LocalServices.getService(BatteryManagerInternal.class); + mBatteryHealthy = !batteryManagerInternal.getBatteryLevelLow(); + mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY); + } + + public void setMonitorBatteryLocked(boolean enabled) { + if (enabled) { + if (mMonitor == null) { + mMonitor = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + ChargingTracker.this.onReceive(context, intent); + } + }; + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_BATTERY_CHANGED); + mContext.registerReceiver(mMonitor, filter); + } + } else { + if (mMonitor != null) { + mContext.unregisterReceiver(mMonitor); + mMonitor = null; + } + } + } + + public boolean isOnStablePower() { + return mCharging && mBatteryHealthy; + } + + public boolean isBatteryNotLow() { + return mBatteryHealthy; + } + + public boolean isMonitoring() { + return mMonitor != null; + } + + public int getSeq() { + return mLastBatterySeq; + } + + @Override + public void onReceive(Context context, Intent intent) { + onReceiveInternal(intent); + } + + @VisibleForTesting + public void onReceiveInternal(Intent intent) { + synchronized (mLock) { + final String action = intent.getAction(); + if (Intent.ACTION_BATTERY_LOW.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Battery life too low to do work. @ " + + sElapsedRealtimeClock.millis()); + } + // If we get this action, the battery is discharging => it isn't plugged in so + // there's no work to cancel. We track this variable for the case where it is + // charging, but hasn't been for long enough to be healthy. + mBatteryHealthy = false; + maybeReportNewChargingStateLocked(); + } else if (Intent.ACTION_BATTERY_OKAY.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Battery life healthy enough to do work. @ " + + sElapsedRealtimeClock.millis()); + } + mBatteryHealthy = true; + maybeReportNewChargingStateLocked(); + } else if (BatteryManager.ACTION_CHARGING.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Received charging intent, fired @ " + + sElapsedRealtimeClock.millis()); + } + mCharging = true; + maybeReportNewChargingStateLocked(); + } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Disconnected from power."); + } + mCharging = false; + maybeReportNewChargingStateLocked(); + } + mLastBatterySeq = intent.getIntExtra(BatteryManager.EXTRA_SEQUENCE, + mLastBatterySeq); + } + } + } + + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + pw.println("Stable power: " + mChargeTracker.isOnStablePower()); + pw.println("Not low: " + mChargeTracker.isBatteryNotLow()); + + if (mChargeTracker.isMonitoring()) { + pw.print("MONITORING: seq="); + pw.println(mChargeTracker.getSeq()); + } + pw.println(); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + pw.println(); + } + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.BATTERY); + + proto.write(StateControllerProto.BatteryController.IS_ON_STABLE_POWER, + mChargeTracker.isOnStablePower()); + proto.write(StateControllerProto.BatteryController.IS_BATTERY_NOT_LOW, + mChargeTracker.isBatteryNotLow()); + + proto.write(StateControllerProto.BatteryController.IS_MONITORING, + mChargeTracker.isMonitoring()); + proto.write(StateControllerProto.BatteryController.LAST_BROADCAST_SEQUENCE_NUMBER, + mChargeTracker.getSeq()); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + final long jsToken = proto.start(StateControllerProto.BatteryController.TRACKED_JOBS); + js.writeToShortProto(proto, StateControllerProto.BatteryController.TrackedJob.INFO); + proto.write(StateControllerProto.BatteryController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + proto.end(jsToken); + } + + proto.end(mToken); + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java new file mode 100644 index 000000000000..bb94275fc409 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java @@ -0,0 +1,702 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers; + +import static android.net.NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; + +import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; + +import android.app.job.JobInfo; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.INetworkPolicyListener; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkPolicyManager; +import android.net.NetworkRequest; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.UserHandle; +import android.text.format.DateUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.DataUnit; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobSchedulerService.Constants; +import com.android.server.job.StateControllerProto; +import com.android.server.net.NetworkPolicyManagerInternal; + +import java.util.Objects; +import java.util.function.Predicate; + +/** + * Handles changes in connectivity. + * <p> + * Each app can have a different default networks or different connectivity + * status due to user-requested network policies, so we need to check + * constraints on a per-UID basis. + * + * Test: atest com.android.server.job.controllers.ConnectivityControllerTest + */ +public final class ConnectivityController extends RestrictingController implements + ConnectivityManager.OnNetworkActiveListener { + private static final String TAG = "JobScheduler.Connectivity"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private final ConnectivityManager mConnManager; + private final NetworkPolicyManager mNetPolicyManager; + private final NetworkPolicyManagerInternal mNetPolicyManagerInternal; + + /** List of tracked jobs keyed by source UID. */ + @GuardedBy("mLock") + private final SparseArray<ArraySet<JobStatus>> mTrackedJobs = new SparseArray<>(); + + /** + * Keep track of all the UID's jobs that the controller has requested that NetworkPolicyManager + * grant an exception to in the app standby chain. + */ + @GuardedBy("mLock") + private final SparseArray<ArraySet<JobStatus>> mRequestedWhitelistJobs = new SparseArray<>(); + + /** List of currently available networks. */ + @GuardedBy("mLock") + private final ArraySet<Network> mAvailableNetworks = new ArraySet<>(); + + private static final int MSG_DATA_SAVER_TOGGLED = 0; + private static final int MSG_UID_RULES_CHANGES = 1; + private static final int MSG_REEVALUATE_JOBS = 2; + + private final Handler mHandler; + + public ConnectivityController(JobSchedulerService service) { + super(service); + mHandler = new CcHandler(mContext.getMainLooper()); + + mConnManager = mContext.getSystemService(ConnectivityManager.class); + mNetPolicyManager = mContext.getSystemService(NetworkPolicyManager.class); + mNetPolicyManagerInternal = LocalServices.getService(NetworkPolicyManagerInternal.class); + + // We're interested in all network changes; internally we match these + // network changes against the active network for each UID with jobs. + final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); + mConnManager.registerNetworkCallback(request, mNetworkCallback); + + mNetPolicyManager.registerListener(mNetPolicyListener); + } + + @GuardedBy("mLock") + @Override + public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { + if (jobStatus.hasConnectivityConstraint()) { + updateConstraintsSatisfied(jobStatus); + ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUid()); + if (jobs == null) { + jobs = new ArraySet<>(); + mTrackedJobs.put(jobStatus.getSourceUid(), jobs); + } + jobs.add(jobStatus); + jobStatus.setTrackingController(JobStatus.TRACKING_CONNECTIVITY); + } + } + + @GuardedBy("mLock") + @Override + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate) { + if (jobStatus.clearTrackingController(JobStatus.TRACKING_CONNECTIVITY)) { + ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUid()); + if (jobs != null) { + jobs.remove(jobStatus); + } + maybeRevokeStandbyExceptionLocked(jobStatus); + } + } + + @Override + public void startTrackingRestrictedJobLocked(JobStatus jobStatus) { + // Don't need to start tracking the job. If the job needed network, it would already be + // tracked. + if (jobStatus.hasConnectivityConstraint()) { + updateConstraintsSatisfied(jobStatus); + } + } + + @Override + public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) { + // Shouldn't stop tracking the job here. If the job was tracked, it still needs network, + // even after being unrestricted. + if (jobStatus.hasConnectivityConstraint()) { + updateConstraintsSatisfied(jobStatus); + } + } + + /** + * Returns true if the job's requested network is available. This DOES NOT necessarily mean + * that the UID has been granted access to the network. + */ + public boolean isNetworkAvailable(JobStatus job) { + synchronized (mLock) { + for (int i = 0; i < mAvailableNetworks.size(); ++i) { + final Network network = mAvailableNetworks.valueAt(i); + final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities( + network); + final boolean satisfied = isSatisfied(job, network, capabilities, mConstants); + if (DEBUG) { + Slog.v(TAG, "isNetworkAvailable(" + job + ") with network " + network + + " and capabilities " + capabilities + ". Satisfied=" + satisfied); + } + if (satisfied) { + return true; + } + } + return false; + } + } + + /** + * Request that NetworkPolicyManager grant an exception to the uid from its standby policy + * chain. + */ + @VisibleForTesting + @GuardedBy("mLock") + void requestStandbyExceptionLocked(JobStatus job) { + final int uid = job.getSourceUid(); + // Need to call this before adding the job. + final boolean isExceptionRequested = isStandbyExceptionRequestedLocked(uid); + ArraySet<JobStatus> jobs = mRequestedWhitelistJobs.get(uid); + if (jobs == null) { + jobs = new ArraySet<JobStatus>(); + mRequestedWhitelistJobs.put(uid, jobs); + } + if (!jobs.add(job) || isExceptionRequested) { + if (DEBUG) { + Slog.i(TAG, "requestStandbyExceptionLocked found exception already requested."); + } + return; + } + if (DEBUG) Slog.i(TAG, "Requesting standby exception for UID: " + uid); + mNetPolicyManagerInternal.setAppIdleWhitelist(uid, true); + } + + /** Returns whether a standby exception has been requested for the UID. */ + @VisibleForTesting + @GuardedBy("mLock") + boolean isStandbyExceptionRequestedLocked(final int uid) { + ArraySet jobs = mRequestedWhitelistJobs.get(uid); + return jobs != null && jobs.size() > 0; + } + + @VisibleForTesting + @GuardedBy("mLock") + boolean wouldBeReadyWithConnectivityLocked(JobStatus jobStatus) { + final boolean networkAvailable = isNetworkAvailable(jobStatus); + if (DEBUG) { + Slog.v(TAG, "wouldBeReadyWithConnectivityLocked: " + jobStatus.toShortString() + + " networkAvailable=" + networkAvailable); + } + // If the network isn't available, then requesting an exception won't help. + + return networkAvailable && wouldBeReadyWithConstraintLocked(jobStatus, + JobStatus.CONSTRAINT_CONNECTIVITY); + } + + /** + * Tell NetworkPolicyManager not to block a UID's network connection if that's the only + * thing stopping a job from running. + */ + @GuardedBy("mLock") + @Override + public void evaluateStateLocked(JobStatus jobStatus) { + if (!jobStatus.hasConnectivityConstraint()) { + return; + } + + // Always check the full job readiness stat in case the component has been disabled. + if (wouldBeReadyWithConnectivityLocked(jobStatus)) { + if (DEBUG) { + Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would be ready."); + } + requestStandbyExceptionLocked(jobStatus); + } else { + if (DEBUG) { + Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would not be ready."); + } + maybeRevokeStandbyExceptionLocked(jobStatus); + } + } + + @GuardedBy("mLock") + @Override + public void reevaluateStateLocked(final int uid) { + // Check if we still need a connectivity exception in case the JobService was disabled. + ArraySet<JobStatus> jobs = mTrackedJobs.get(uid); + if (jobs == null) { + return; + } + for (int i = jobs.size() - 1; i >= 0; i--) { + evaluateStateLocked(jobs.valueAt(i)); + } + } + + /** Cancel the requested standby exception if none of the jobs would be ready to run anyway. */ + @VisibleForTesting + @GuardedBy("mLock") + void maybeRevokeStandbyExceptionLocked(final JobStatus job) { + final int uid = job.getSourceUid(); + if (!isStandbyExceptionRequestedLocked(uid)) { + return; + } + ArraySet<JobStatus> jobs = mRequestedWhitelistJobs.get(uid); + if (jobs == null) { + Slog.wtf(TAG, + "maybeRevokeStandbyExceptionLocked found null jobs array even though a " + + "standby exception has been requested."); + return; + } + if (!jobs.remove(job) || jobs.size() > 0) { + if (DEBUG) { + Slog.i(TAG, + "maybeRevokeStandbyExceptionLocked not revoking because there are still " + + jobs.size() + " jobs left."); + } + return; + } + // No more jobs that need an exception. + revokeStandbyExceptionLocked(uid); + } + + /** + * Tell NetworkPolicyManager to revoke any exception it granted from its standby policy chain + * for the uid. + */ + @GuardedBy("mLock") + private void revokeStandbyExceptionLocked(final int uid) { + if (DEBUG) Slog.i(TAG, "Revoking standby exception for UID: " + uid); + mNetPolicyManagerInternal.setAppIdleWhitelist(uid, false); + mRequestedWhitelistJobs.remove(uid); + } + + @GuardedBy("mLock") + @Override + public void onAppRemovedLocked(String pkgName, int uid) { + mTrackedJobs.delete(uid); + } + + /** + * Test to see if running the given job on the given network is insane. + * <p> + * For example, if a job is trying to send 10MB over a 128Kbps EDGE + * connection, it would take 10.4 minutes, and has no chance of succeeding + * before the job times out, so we'd be insane to try running it. + */ + private boolean isInsane(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities, Constants constants) { + final long maxJobExecutionTimeMs = mService.getMaxJobExecutionTimeMs(jobStatus); + + final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes(); + if (downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + final long bandwidth = capabilities.getLinkDownstreamBandwidthKbps(); + // If we don't know the bandwidth, all we can do is hope the job finishes in time. + if (bandwidth != LINK_BANDWIDTH_UNSPECIFIED) { + // Divide by 8 to convert bits to bytes. + final long estimatedMillis = ((downloadBytes * DateUtils.SECOND_IN_MILLIS) + / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8)); + if (estimatedMillis > maxJobExecutionTimeMs) { + // If we'd never finish before the timeout, we'd be insane! + Slog.w(TAG, "Estimated " + downloadBytes + " download bytes over " + bandwidth + + " kbps network would take " + estimatedMillis + "ms and job has " + + maxJobExecutionTimeMs + "ms to run; that's insane!"); + return true; + } + } + } + + final long uploadBytes = jobStatus.getEstimatedNetworkUploadBytes(); + if (uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + final long bandwidth = capabilities.getLinkUpstreamBandwidthKbps(); + // If we don't know the bandwidth, all we can do is hope the job finishes in time. + if (bandwidth != LINK_BANDWIDTH_UNSPECIFIED) { + // Divide by 8 to convert bits to bytes. + final long estimatedMillis = ((uploadBytes * DateUtils.SECOND_IN_MILLIS) + / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8)); + if (estimatedMillis > maxJobExecutionTimeMs) { + // If we'd never finish before the timeout, we'd be insane! + Slog.w(TAG, "Estimated " + uploadBytes + " upload bytes over " + bandwidth + + " kbps network would take " + estimatedMillis + "ms and job has " + + maxJobExecutionTimeMs + "ms to run; that's insane!"); + return true; + } + } + } + + return false; + } + + private static boolean isCongestionDelayed(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities, Constants constants) { + // If network is congested, and job is less than 50% through the + // developer-requested window, then we're okay delaying the job. + if (!capabilities.hasCapability(NET_CAPABILITY_NOT_CONGESTED)) { + return jobStatus.getFractionRunTime() < constants.CONN_CONGESTION_DELAY_FRAC; + } else { + return false; + } + } + + private static boolean isStrictSatisfied(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities, Constants constants) { + final NetworkCapabilities required; + // A restricted job that's out of quota MUST use an unmetered network. + if (jobStatus.getEffectiveStandbyBucket() == RESTRICTED_INDEX + && !jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) { + required = new NetworkCapabilities( + jobStatus.getJob().getRequiredNetwork().networkCapabilities) + .addCapability(NET_CAPABILITY_NOT_METERED); + } else { + required = jobStatus.getJob().getRequiredNetwork().networkCapabilities; + } + + return required.satisfiedByNetworkCapabilities(capabilities); + } + + private static boolean isRelaxedSatisfied(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities, Constants constants) { + // Only consider doing this for unrestricted prefetching jobs + if (!jobStatus.getJob().isPrefetch() || jobStatus.getStandbyBucket() == RESTRICTED_INDEX) { + return false; + } + + // See if we match after relaxing any unmetered request + final NetworkCapabilities relaxed = new NetworkCapabilities( + jobStatus.getJob().getRequiredNetwork().networkCapabilities) + .removeCapability(NET_CAPABILITY_NOT_METERED); + if (relaxed.satisfiedByNetworkCapabilities(capabilities)) { + // TODO: treat this as "maybe" response; need to check quotas + return jobStatus.getFractionRunTime() > constants.CONN_PREFETCH_RELAX_FRAC; + } else { + return false; + } + } + + @VisibleForTesting + boolean isSatisfied(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities, Constants constants) { + // Zeroth, we gotta have a network to think about being satisfied + if (network == null || capabilities == null) return false; + + // First, are we insane? + if (isInsane(jobStatus, network, capabilities, constants)) return false; + + // Second, is the network congested? + if (isCongestionDelayed(jobStatus, network, capabilities, constants)) return false; + + // Third, is the network a strict match? + if (isStrictSatisfied(jobStatus, network, capabilities, constants)) return true; + + // Third, is the network a relaxed match? + if (isRelaxedSatisfied(jobStatus, network, capabilities, constants)) return true; + + return false; + } + + private boolean updateConstraintsSatisfied(JobStatus jobStatus) { + final Network network = mConnManager.getActiveNetworkForUid(jobStatus.getSourceUid()); + final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(network); + return updateConstraintsSatisfied(jobStatus, network, capabilities); + } + + private boolean updateConstraintsSatisfied(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities) { + // TODO: consider matching against non-active networks + + final boolean ignoreBlocked = (jobStatus.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0; + final NetworkInfo info = mConnManager.getNetworkInfoForUid(network, + jobStatus.getSourceUid(), ignoreBlocked); + + final boolean connected = (info != null) && info.isConnected(); + final boolean satisfied = isSatisfied(jobStatus, network, capabilities, mConstants); + + final boolean changed = jobStatus + .setConnectivityConstraintSatisfied(connected && satisfied); + + // Pass along the evaluated network for job to use; prevents race + // conditions as default routes change over time, and opens the door to + // using non-default routes. + jobStatus.network = network; + + if (DEBUG) { + Slog.i(TAG, "Connectivity " + (changed ? "CHANGED" : "unchanged") + + " for " + jobStatus + ": connected=" + connected + + " satisfied=" + satisfied); + } + return changed; + } + + /** + * Update any jobs tracked by this controller that match given filters. + * + * @param filterUid only update jobs belonging to this UID, or {@code -1} to + * update all tracked jobs. + * @param filterNetwork only update jobs that would use this + * {@link Network}, or {@code null} to update all tracked jobs. + */ + private void updateTrackedJobs(int filterUid, Network filterNetwork) { + synchronized (mLock) { + // Since this is a really hot codepath, temporarily cache any + // answers that we get from ConnectivityManager. + final ArrayMap<Network, NetworkCapabilities> networkToCapabilities = new ArrayMap<>(); + + boolean changed = false; + if (filterUid == -1) { + for (int i = mTrackedJobs.size() - 1; i >= 0; i--) { + changed |= updateTrackedJobsLocked(mTrackedJobs.valueAt(i), + filterNetwork, networkToCapabilities); + } + } else { + changed = updateTrackedJobsLocked(mTrackedJobs.get(filterUid), + filterNetwork, networkToCapabilities); + } + if (changed) { + mStateChangedListener.onControllerStateChanged(); + } + } + } + + private boolean updateTrackedJobsLocked(ArraySet<JobStatus> jobs, Network filterNetwork, + ArrayMap<Network, NetworkCapabilities> networkToCapabilities) { + if (jobs == null || jobs.size() == 0) { + return false; + } + + final Network network = mConnManager.getActiveNetworkForUid(jobs.valueAt(0).getSourceUid()); + NetworkCapabilities capabilities = networkToCapabilities.get(network); + if (capabilities == null) { + capabilities = mConnManager.getNetworkCapabilities(network); + networkToCapabilities.put(network, capabilities); + } + final boolean networkMatch = (filterNetwork == null + || Objects.equals(filterNetwork, network)); + + boolean changed = false; + for (int i = jobs.size() - 1; i >= 0; i--) { + final JobStatus js = jobs.valueAt(i); + + // Update either when we have a network match, or when the + // job hasn't yet been evaluated against the currently + // active network; typically when we just lost a network. + if (networkMatch || !Objects.equals(js.network, network)) { + changed |= updateConstraintsSatisfied(js, network, capabilities); + } + } + return changed; + } + + /** + * We know the network has just come up. We want to run any jobs that are ready. + */ + @Override + public void onNetworkActive() { + synchronized (mLock) { + for (int i = mTrackedJobs.size()-1; i >= 0; i--) { + final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i); + for (int j = jobs.size() - 1; j >= 0; j--) { + final JobStatus js = jobs.valueAt(j); + if (js.isReady()) { + if (DEBUG) { + Slog.d(TAG, "Running " + js + " due to network activity."); + } + mStateChangedListener.onRunJobNow(js); + } + } + } + } + } + + private final NetworkCallback mNetworkCallback = new NetworkCallback() { + @Override + public void onAvailable(Network network) { + if (DEBUG) Slog.v(TAG, "onAvailable: " + network); + synchronized (mLock) { + mAvailableNetworks.add(network); + } + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) { + if (DEBUG) { + Slog.v(TAG, "onCapabilitiesChanged: " + network); + } + updateTrackedJobs(-1, network); + } + + @Override + public void onLost(Network network) { + if (DEBUG) { + Slog.v(TAG, "onLost: " + network); + } + synchronized (mLock) { + mAvailableNetworks.remove(network); + } + updateTrackedJobs(-1, network); + } + }; + + private final INetworkPolicyListener mNetPolicyListener = new NetworkPolicyManager.Listener() { + @Override + public void onRestrictBackgroundChanged(boolean restrictBackground) { + if (DEBUG) { + Slog.v(TAG, "onRestrictBackgroundChanged: " + restrictBackground); + } + mHandler.obtainMessage(MSG_DATA_SAVER_TOGGLED).sendToTarget(); + } + + @Override + public void onUidRulesChanged(int uid, int uidRules) { + if (DEBUG) { + Slog.v(TAG, "onUidRulesChanged: " + uid); + } + mHandler.obtainMessage(MSG_UID_RULES_CHANGES, uid, 0).sendToTarget(); + } + }; + + private class CcHandler extends Handler { + CcHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + synchronized (mLock) { + switch (msg.what) { + case MSG_DATA_SAVER_TOGGLED: + updateTrackedJobs(-1, null); + break; + case MSG_UID_RULES_CHANGES: + updateTrackedJobs(msg.arg1, null); + break; + case MSG_REEVALUATE_JOBS: + updateTrackedJobs(-1, null); + break; + } + } + } + }; + + @GuardedBy("mLock") + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + + if (mRequestedWhitelistJobs.size() > 0) { + pw.print("Requested standby exceptions:"); + for (int i = 0; i < mRequestedWhitelistJobs.size(); i++) { + pw.print(" "); + pw.print(mRequestedWhitelistJobs.keyAt(i)); + pw.print(" ("); + pw.print(mRequestedWhitelistJobs.valueAt(i).size()); + pw.print(" jobs)"); + } + pw.println(); + } + if (mAvailableNetworks.size() > 0) { + pw.println("Available networks:"); + pw.increaseIndent(); + for (int i = 0; i < mAvailableNetworks.size(); i++) { + pw.println(mAvailableNetworks.valueAt(i)); + } + pw.decreaseIndent(); + } else { + pw.println("No available networks"); + } + for (int i = 0; i < mTrackedJobs.size(); i++) { + final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i); + for (int j = 0; j < jobs.size(); j++) { + final JobStatus js = jobs.valueAt(j); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + pw.print(": "); + pw.print(js.getJob().getRequiredNetwork()); + pw.println(); + } + } + } + + @GuardedBy("mLock") + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.CONNECTIVITY); + + for (int i = 0; i < mRequestedWhitelistJobs.size(); i++) { + proto.write( + StateControllerProto.ConnectivityController.REQUESTED_STANDBY_EXCEPTION_UIDS, + mRequestedWhitelistJobs.keyAt(i)); + } + for (int i = 0; i < mAvailableNetworks.size(); i++) { + Network network = mAvailableNetworks.valueAt(i); + if (network != null) { + network.dumpDebug(proto, + StateControllerProto.ConnectivityController.AVAILABLE_NETWORKS); + } + } + for (int i = 0; i < mTrackedJobs.size(); i++) { + final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i); + for (int j = 0; j < jobs.size(); j++) { + final JobStatus js = jobs.valueAt(j); + if (!predicate.test(js)) { + continue; + } + final long jsToken = proto.start( + StateControllerProto.ConnectivityController.TRACKED_JOBS); + js.writeToShortProto(proto, + StateControllerProto.ConnectivityController.TrackedJob.INFO); + proto.write(StateControllerProto.ConnectivityController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + NetworkRequest rn = js.getJob().getRequiredNetwork(); + if (rn != null) { + rn.dumpDebug(proto, + StateControllerProto.ConnectivityController.TrackedJob + .REQUIRED_NETWORK); + } + proto.end(jsToken); + } + } + + proto.end(mToken); + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java new file mode 100644 index 000000000000..5fcd774189ac --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java @@ -0,0 +1,544 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers; + +import android.annotation.UserIdInt; +import android.app.job.JobInfo; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; +import com.android.server.job.StateControllerProto.ContentObserverController.Observer.TriggerContentData; + +import java.util.ArrayList; +import java.util.function.Predicate; + +/** + * Controller for monitoring changes to content URIs through a ContentObserver. + */ +public final class ContentObserverController extends StateController { + private static final String TAG = "JobScheduler.ContentObserver"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + /** + * Maximum number of changing URIs we will batch together to report. + * XXX Should be smarter about this, restricting it by the maximum number + * of characters we will retain. + */ + private static final int MAX_URIS_REPORTED = 50; + + /** + * At this point we consider it urgent to schedule the job ASAP. + */ + private static final int URIS_URGENT_THRESHOLD = 40; + + final private ArraySet<JobStatus> mTrackedTasks = new ArraySet<>(); + /** + * Per-userid {@link JobInfo.TriggerContentUri} keyed ContentObserver cache. + */ + final SparseArray<ArrayMap<JobInfo.TriggerContentUri, ObserverInstance>> mObservers = + new SparseArray<>(); + final Handler mHandler; + + public ContentObserverController(JobSchedulerService service) { + super(service); + mHandler = new Handler(mContext.getMainLooper()); + } + + @Override + public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { + if (taskStatus.hasContentTriggerConstraint()) { + if (taskStatus.contentObserverJobInstance == null) { + taskStatus.contentObserverJobInstance = new JobInstance(taskStatus); + } + if (DEBUG) { + Slog.i(TAG, "Tracking content-trigger job " + taskStatus); + } + mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_CONTENT); + boolean havePendingUris = false; + // If there is a previous job associated with the new job, propagate over + // any pending content URI trigger reports. + if (taskStatus.contentObserverJobInstance.mChangedAuthorities != null) { + havePendingUris = true; + } + // If we have previously reported changed authorities/uris, then we failed + // to complete the job with them so will re-record them to report again. + if (taskStatus.changedAuthorities != null) { + havePendingUris = true; + if (taskStatus.contentObserverJobInstance.mChangedAuthorities == null) { + taskStatus.contentObserverJobInstance.mChangedAuthorities + = new ArraySet<>(); + } + for (String auth : taskStatus.changedAuthorities) { + taskStatus.contentObserverJobInstance.mChangedAuthorities.add(auth); + } + if (taskStatus.changedUris != null) { + if (taskStatus.contentObserverJobInstance.mChangedUris == null) { + taskStatus.contentObserverJobInstance.mChangedUris = new ArraySet<>(); + } + for (Uri uri : taskStatus.changedUris) { + taskStatus.contentObserverJobInstance.mChangedUris.add(uri); + } + } + taskStatus.changedAuthorities = null; + taskStatus.changedUris = null; + } + taskStatus.changedAuthorities = null; + taskStatus.changedUris = null; + taskStatus.setContentTriggerConstraintSatisfied(havePendingUris); + } + if (lastJob != null && lastJob.contentObserverJobInstance != null) { + // And now we can detach the instance state from the last job. + lastJob.contentObserverJobInstance.detachLocked(); + lastJob.contentObserverJobInstance = null; + } + } + + @Override + public void prepareForExecutionLocked(JobStatus taskStatus) { + if (taskStatus.hasContentTriggerConstraint()) { + if (taskStatus.contentObserverJobInstance != null) { + taskStatus.changedUris = taskStatus.contentObserverJobInstance.mChangedUris; + taskStatus.changedAuthorities + = taskStatus.contentObserverJobInstance.mChangedAuthorities; + taskStatus.contentObserverJobInstance.mChangedUris = null; + taskStatus.contentObserverJobInstance.mChangedAuthorities = null; + } + } + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, + boolean forUpdate) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_CONTENT)) { + mTrackedTasks.remove(taskStatus); + if (taskStatus.contentObserverJobInstance != null) { + taskStatus.contentObserverJobInstance.unscheduleLocked(); + if (incomingJob != null) { + if (taskStatus.contentObserverJobInstance != null + && taskStatus.contentObserverJobInstance.mChangedAuthorities != null) { + // We are stopping this job, but it is going to be replaced by this given + // incoming job. We want to propagate our state over to it, so we don't + // lose any content changes that had happened since the last one started. + // If there is a previous job associated with the new job, propagate over + // any pending content URI trigger reports. + if (incomingJob.contentObserverJobInstance == null) { + incomingJob.contentObserverJobInstance = new JobInstance(incomingJob); + } + incomingJob.contentObserverJobInstance.mChangedAuthorities + = taskStatus.contentObserverJobInstance.mChangedAuthorities; + incomingJob.contentObserverJobInstance.mChangedUris + = taskStatus.contentObserverJobInstance.mChangedUris; + taskStatus.contentObserverJobInstance.mChangedAuthorities = null; + taskStatus.contentObserverJobInstance.mChangedUris = null; + } + // We won't detach the content observers here, because we want to + // allow them to continue monitoring so we don't miss anything... and + // since we are giving an incomingJob here, we know this will be + // immediately followed by a start tracking of that job. + } else { + // But here there is no incomingJob, so nothing coming up, so time to detach. + taskStatus.contentObserverJobInstance.detachLocked(); + taskStatus.contentObserverJobInstance = null; + } + } + if (DEBUG) { + Slog.i(TAG, "No longer tracking job " + taskStatus); + } + } + } + + @Override + public void rescheduleForFailureLocked(JobStatus newJob, JobStatus failureToReschedule) { + if (failureToReschedule.hasContentTriggerConstraint() + && newJob.hasContentTriggerConstraint()) { + // Our job has failed, and we are scheduling a new job for it. + // Copy the last reported content changes in to the new job, so when + // we schedule the new one we will pick them up and report them again. + newJob.changedAuthorities = failureToReschedule.changedAuthorities; + newJob.changedUris = failureToReschedule.changedUris; + } + } + + final class ObserverInstance extends ContentObserver { + final JobInfo.TriggerContentUri mUri; + final @UserIdInt int mUserId; + final ArraySet<JobInstance> mJobs = new ArraySet<>(); + + public ObserverInstance(Handler handler, JobInfo.TriggerContentUri uri, + @UserIdInt int userId) { + super(handler); + mUri = uri; + mUserId = userId; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (DEBUG) { + Slog.i(TAG, "onChange(self=" + selfChange + ") for " + uri + + " when mUri=" + mUri + " mUserId=" + mUserId); + } + synchronized (mLock) { + final int N = mJobs.size(); + for (int i=0; i<N; i++) { + JobInstance inst = mJobs.valueAt(i); + if (inst.mChangedUris == null) { + inst.mChangedUris = new ArraySet<>(); + } + if (inst.mChangedUris.size() < MAX_URIS_REPORTED) { + inst.mChangedUris.add(uri); + } + if (inst.mChangedAuthorities == null) { + inst.mChangedAuthorities = new ArraySet<>(); + } + inst.mChangedAuthorities.add(uri.getAuthority()); + inst.scheduleLocked(); + } + } + } + } + + static final class TriggerRunnable implements Runnable { + final JobInstance mInstance; + + TriggerRunnable(JobInstance instance) { + mInstance = instance; + } + + @Override public void run() { + mInstance.trigger(); + } + } + + final class JobInstance { + final ArrayList<ObserverInstance> mMyObservers = new ArrayList<>(); + final JobStatus mJobStatus; + final Runnable mExecuteRunner; + final Runnable mTimeoutRunner; + ArraySet<Uri> mChangedUris; + ArraySet<String> mChangedAuthorities; + + boolean mTriggerPending; + + // This constructor must be called with the master job scheduler lock held. + JobInstance(JobStatus jobStatus) { + mJobStatus = jobStatus; + mExecuteRunner = new TriggerRunnable(this); + mTimeoutRunner = new TriggerRunnable(this); + final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris(); + final int sourceUserId = jobStatus.getSourceUserId(); + ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = + mObservers.get(sourceUserId); + if (observersOfUser == null) { + observersOfUser = new ArrayMap<>(); + mObservers.put(sourceUserId, observersOfUser); + } + if (uris != null) { + for (JobInfo.TriggerContentUri uri : uris) { + ObserverInstance obs = observersOfUser.get(uri); + if (obs == null) { + obs = new ObserverInstance(mHandler, uri, jobStatus.getSourceUserId()); + observersOfUser.put(uri, obs); + final boolean andDescendants = (uri.getFlags() & + JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0; + if (DEBUG) { + Slog.v(TAG, "New observer " + obs + " for " + uri.getUri() + + " andDescendants=" + andDescendants + + " sourceUserId=" + sourceUserId); + } + mContext.getContentResolver().registerContentObserver( + uri.getUri(), + andDescendants, + obs, + sourceUserId + ); + } else { + if (DEBUG) { + final boolean andDescendants = (uri.getFlags() & + JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0; + Slog.v(TAG, "Reusing existing observer " + obs + " for " + uri.getUri() + + " andDescendants=" + andDescendants); + } + } + obs.mJobs.add(this); + mMyObservers.add(obs); + } + } + } + + void trigger() { + boolean reportChange = false; + synchronized (mLock) { + if (mTriggerPending) { + if (mJobStatus.setContentTriggerConstraintSatisfied(true)) { + reportChange = true; + } + unscheduleLocked(); + } + } + // Let the scheduler know that state has changed. This may or may not result in an + // execution. + if (reportChange) { + mStateChangedListener.onControllerStateChanged(); + } + } + + void scheduleLocked() { + if (!mTriggerPending) { + mTriggerPending = true; + mHandler.postDelayed(mTimeoutRunner, mJobStatus.getTriggerContentMaxDelay()); + } + mHandler.removeCallbacks(mExecuteRunner); + if (mChangedUris.size() >= URIS_URGENT_THRESHOLD) { + // If we start getting near the limit, GO NOW! + mHandler.post(mExecuteRunner); + } else { + mHandler.postDelayed(mExecuteRunner, mJobStatus.getTriggerContentUpdateDelay()); + } + } + + void unscheduleLocked() { + if (mTriggerPending) { + mHandler.removeCallbacks(mExecuteRunner); + mHandler.removeCallbacks(mTimeoutRunner); + mTriggerPending = false; + } + } + + void detachLocked() { + final int N = mMyObservers.size(); + for (int i=0; i<N; i++) { + final ObserverInstance obs = mMyObservers.get(i); + obs.mJobs.remove(this); + if (obs.mJobs.size() == 0) { + if (DEBUG) { + Slog.i(TAG, "Unregistering observer " + obs + " for " + obs.mUri.getUri()); + } + mContext.getContentResolver().unregisterContentObserver(obs); + ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observerOfUser = + mObservers.get(obs.mUserId); + if (observerOfUser != null) { + observerOfUser.remove(obs.mUri); + } + } + } + } + } + + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + for (int i = 0; i < mTrackedTasks.size(); i++) { + JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + pw.println(); + } + pw.println(); + + int N = mObservers.size(); + if (N > 0) { + pw.println("Observers:"); + pw.increaseIndent(); + for (int userIdx = 0; userIdx < N; userIdx++) { + final int userId = mObservers.keyAt(userIdx); + ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = + mObservers.get(userId); + int numbOfObserversPerUser = observersOfUser.size(); + for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) { + ObserverInstance obs = observersOfUser.valueAt(observerIdx); + int M = obs.mJobs.size(); + boolean shouldDump = false; + for (int j = 0; j < M; j++) { + JobInstance inst = obs.mJobs.valueAt(j); + if (predicate.test(inst.mJobStatus)) { + shouldDump = true; + break; + } + } + if (!shouldDump) { + continue; + } + JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx); + pw.print(trigger.getUri()); + pw.print(" 0x"); + pw.print(Integer.toHexString(trigger.getFlags())); + pw.print(" ("); + pw.print(System.identityHashCode(obs)); + pw.println("):"); + pw.increaseIndent(); + pw.println("Jobs:"); + pw.increaseIndent(); + for (int j = 0; j < M; j++) { + JobInstance inst = obs.mJobs.valueAt(j); + pw.print("#"); + inst.mJobStatus.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, inst.mJobStatus.getSourceUid()); + if (inst.mChangedAuthorities != null) { + pw.println(":"); + pw.increaseIndent(); + if (inst.mTriggerPending) { + pw.print("Trigger pending: update="); + TimeUtils.formatDuration( + inst.mJobStatus.getTriggerContentUpdateDelay(), pw); + pw.print(", max="); + TimeUtils.formatDuration( + inst.mJobStatus.getTriggerContentMaxDelay(), pw); + pw.println(); + } + pw.println("Changed Authorities:"); + for (int k = 0; k < inst.mChangedAuthorities.size(); k++) { + pw.println(inst.mChangedAuthorities.valueAt(k)); + } + if (inst.mChangedUris != null) { + pw.println(" Changed URIs:"); + for (int k = 0; k < inst.mChangedUris.size(); k++) { + pw.println(inst.mChangedUris.valueAt(k)); + } + } + pw.decreaseIndent(); + } else { + pw.println(); + } + } + pw.decreaseIndent(); + pw.decreaseIndent(); + } + } + pw.decreaseIndent(); + } + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.CONTENT_OBSERVER); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + final long jsToken = + proto.start(StateControllerProto.ContentObserverController.TRACKED_JOBS); + js.writeToShortProto(proto, + StateControllerProto.ContentObserverController.TrackedJob.INFO); + proto.write(StateControllerProto.ContentObserverController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + proto.end(jsToken); + } + + final int n = mObservers.size(); + for (int userIdx = 0; userIdx < n; userIdx++) { + final long oToken = + proto.start(StateControllerProto.ContentObserverController.OBSERVERS); + final int userId = mObservers.keyAt(userIdx); + + proto.write(StateControllerProto.ContentObserverController.Observer.USER_ID, userId); + + ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = + mObservers.get(userId); + int numbOfObserversPerUser = observersOfUser.size(); + for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) { + ObserverInstance obs = observersOfUser.valueAt(observerIdx); + int m = obs.mJobs.size(); + boolean shouldDump = false; + for (int j = 0; j < m; j++) { + JobInstance inst = obs.mJobs.valueAt(j); + if (predicate.test(inst.mJobStatus)) { + shouldDump = true; + break; + } + } + if (!shouldDump) { + continue; + } + final long tToken = proto.start( + StateControllerProto.ContentObserverController.Observer.TRIGGERS); + + JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx); + Uri u = trigger.getUri(); + if (u != null) { + proto.write(TriggerContentData.URI, u.toString()); + } + proto.write(TriggerContentData.FLAGS, trigger.getFlags()); + + for (int j = 0; j < m; j++) { + final long jToken = proto.start(TriggerContentData.JOBS); + JobInstance inst = obs.mJobs.valueAt(j); + + inst.mJobStatus.writeToShortProto(proto, TriggerContentData.JobInstance.INFO); + proto.write(TriggerContentData.JobInstance.SOURCE_UID, + inst.mJobStatus.getSourceUid()); + + if (inst.mChangedAuthorities == null) { + proto.end(jToken); + continue; + } + if (inst.mTriggerPending) { + proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_UPDATE_DELAY_MS, + inst.mJobStatus.getTriggerContentUpdateDelay()); + proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_MAX_DELAY_MS, + inst.mJobStatus.getTriggerContentMaxDelay()); + } + for (int k = 0; k < inst.mChangedAuthorities.size(); k++) { + proto.write(TriggerContentData.JobInstance.CHANGED_AUTHORITIES, + inst.mChangedAuthorities.valueAt(k)); + } + if (inst.mChangedUris != null) { + for (int k = 0; k < inst.mChangedUris.size(); k++) { + u = inst.mChangedUris.valueAt(k); + if (u != null) { + proto.write(TriggerContentData.JobInstance.CHANGED_URIS, + u.toString()); + } + } + } + + proto.end(jToken); + } + + proto.end(tToken); + } + + proto.end(oToken); + } + + proto.end(mToken); + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java new file mode 100644 index 000000000000..01f5fa62f889 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers; + +import android.app.job.JobInfo; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; +import android.util.SparseBooleanArray; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.DeviceIdleInternal; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; +import com.android.server.job.StateControllerProto.DeviceIdleJobsController.TrackedJob; + +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * When device is dozing, set constraint for all jobs, except whitelisted apps, as not satisfied. + * When device is not dozing, set constraint for all jobs as satisfied. + */ +public final class DeviceIdleJobsController extends StateController { + private static final String TAG = "JobScheduler.DeviceIdle"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private static final long BACKGROUND_JOBS_DELAY = 3000; + + static final int PROCESS_BACKGROUND_JOBS = 1; + + /** + * These are jobs added with a special flag to indicate that they should be exempted from doze + * when the app is temp whitelisted or in the foreground. + */ + private final ArraySet<JobStatus> mAllowInIdleJobs; + private final SparseBooleanArray mForegroundUids; + private final DeviceIdleUpdateFunctor mDeviceIdleUpdateFunctor; + private final DeviceIdleJobsDelayHandler mHandler; + private final PowerManager mPowerManager; + private final DeviceIdleInternal mLocalDeviceIdleController; + + /** + * True when in device idle mode, so we don't want to schedule any jobs. + */ + private boolean mDeviceIdleMode; + private int[] mDeviceIdleWhitelistAppIds; + private int[] mPowerSaveTempWhitelistAppIds; + + // onReceive + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED: + case PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED: + updateIdleMode(mPowerManager != null && (mPowerManager.isDeviceIdleMode() + || mPowerManager.isLightDeviceIdleMode())); + break; + case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED: + synchronized (mLock) { + mDeviceIdleWhitelistAppIds = + mLocalDeviceIdleController.getPowerSaveWhitelistUserAppIds(); + if (DEBUG) { + Slog.d(TAG, "Got whitelist " + + Arrays.toString(mDeviceIdleWhitelistAppIds)); + } + } + break; + case PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED: + synchronized (mLock) { + mPowerSaveTempWhitelistAppIds = + mLocalDeviceIdleController.getPowerSaveTempWhitelistAppIds(); + if (DEBUG) { + Slog.d(TAG, "Got temp whitelist " + + Arrays.toString(mPowerSaveTempWhitelistAppIds)); + } + boolean changed = false; + for (int i = 0; i < mAllowInIdleJobs.size(); i++) { + changed |= updateTaskStateLocked(mAllowInIdleJobs.valueAt(i)); + } + if (changed) { + mStateChangedListener.onControllerStateChanged(); + } + } + break; + } + } + }; + + public DeviceIdleJobsController(JobSchedulerService service) { + super(service); + + mHandler = new DeviceIdleJobsDelayHandler(mContext.getMainLooper()); + // Register for device idle mode changes + mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + mLocalDeviceIdleController = + LocalServices.getService(DeviceIdleInternal.class); + mDeviceIdleWhitelistAppIds = mLocalDeviceIdleController.getPowerSaveWhitelistUserAppIds(); + mPowerSaveTempWhitelistAppIds = + mLocalDeviceIdleController.getPowerSaveTempWhitelistAppIds(); + mDeviceIdleUpdateFunctor = new DeviceIdleUpdateFunctor(); + mAllowInIdleJobs = new ArraySet<>(); + mForegroundUids = new SparseBooleanArray(); + final IntentFilter filter = new IntentFilter(); + filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + filter.addAction(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED); + filter.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); + filter.addAction(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED); + mContext.registerReceiverAsUser( + mBroadcastReceiver, UserHandle.ALL, filter, null, null); + } + + void updateIdleMode(boolean enabled) { + boolean changed = false; + synchronized (mLock) { + if (mDeviceIdleMode != enabled) { + changed = true; + } + mDeviceIdleMode = enabled; + if (DEBUG) Slog.d(TAG, "mDeviceIdleMode=" + mDeviceIdleMode); + if (enabled) { + mHandler.removeMessages(PROCESS_BACKGROUND_JOBS); + mService.getJobStore().forEachJob(mDeviceIdleUpdateFunctor); + } else { + // When coming out of doze, process all foreground uids immediately, while others + // will be processed after a delay of 3 seconds. + for (int i = 0; i < mForegroundUids.size(); i++) { + if (mForegroundUids.valueAt(i)) { + mService.getJobStore().forEachJobForSourceUid( + mForegroundUids.keyAt(i), mDeviceIdleUpdateFunctor); + } + } + mHandler.sendEmptyMessageDelayed(PROCESS_BACKGROUND_JOBS, BACKGROUND_JOBS_DELAY); + } + } + // Inform the job scheduler service about idle mode changes + if (changed) { + mStateChangedListener.onDeviceIdleStateChanged(enabled); + } + } + + /** + * Called by jobscheduler service to report uid state changes between active and idle + */ + public void setUidActiveLocked(int uid, boolean active) { + final boolean changed = (active != mForegroundUids.get(uid)); + if (!changed) { + return; + } + if (DEBUG) { + Slog.d(TAG, "uid " + uid + " going " + (active ? "active" : "inactive")); + } + mForegroundUids.put(uid, active); + mDeviceIdleUpdateFunctor.mChanged = false; + mService.getJobStore().forEachJobForSourceUid(uid, mDeviceIdleUpdateFunctor); + if (mDeviceIdleUpdateFunctor.mChanged) { + mStateChangedListener.onControllerStateChanged(); + } + } + + /** + * Checks if the given job's scheduling app id exists in the device idle user whitelist. + */ + boolean isWhitelistedLocked(JobStatus job) { + return Arrays.binarySearch(mDeviceIdleWhitelistAppIds, + UserHandle.getAppId(job.getSourceUid())) >= 0; + } + + /** + * Checks if the given job's scheduling app id exists in the device idle temp whitelist. + */ + boolean isTempWhitelistedLocked(JobStatus job) { + return ArrayUtils.contains(mPowerSaveTempWhitelistAppIds, + UserHandle.getAppId(job.getSourceUid())); + } + + private boolean updateTaskStateLocked(JobStatus task) { + final boolean allowInIdle = ((task.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) + && (mForegroundUids.get(task.getSourceUid()) || isTempWhitelistedLocked(task)); + final boolean whitelisted = isWhitelistedLocked(task); + final boolean enableTask = !mDeviceIdleMode || whitelisted || allowInIdle; + return task.setDeviceNotDozingConstraintSatisfied(enableTask, whitelisted); + } + + @Override + public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { + if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) { + mAllowInIdleJobs.add(jobStatus); + } + updateTaskStateLocked(jobStatus); + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate) { + if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) { + mAllowInIdleJobs.remove(jobStatus); + } + } + + @Override + public void dumpControllerStateLocked(final IndentingPrintWriter pw, + final Predicate<JobStatus> predicate) { + pw.println("Idle mode: " + mDeviceIdleMode); + pw.println(); + + mService.getJobStore().forEachJob(predicate, (jobStatus) -> { + pw.print("#"); + jobStatus.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, jobStatus.getSourceUid()); + pw.print(": "); + pw.print(jobStatus.getSourcePackageName()); + pw.print((jobStatus.satisfiedConstraints + & JobStatus.CONSTRAINT_DEVICE_NOT_DOZING) != 0 + ? " RUNNABLE" : " WAITING"); + if (jobStatus.dozeWhitelisted) { + pw.print(" WHITELISTED"); + } + if (mAllowInIdleJobs.contains(jobStatus)) { + pw.print(" ALLOWED_IN_DOZE"); + } + pw.println(); + }); + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.DEVICE_IDLE); + + proto.write(StateControllerProto.DeviceIdleJobsController.IS_DEVICE_IDLE_MODE, + mDeviceIdleMode); + mService.getJobStore().forEachJob(predicate, (jobStatus) -> { + final long jsToken = + proto.start(StateControllerProto.DeviceIdleJobsController.TRACKED_JOBS); + + jobStatus.writeToShortProto(proto, TrackedJob.INFO); + proto.write(TrackedJob.SOURCE_UID, jobStatus.getSourceUid()); + proto.write(TrackedJob.SOURCE_PACKAGE_NAME, jobStatus.getSourcePackageName()); + proto.write(TrackedJob.ARE_CONSTRAINTS_SATISFIED, + (jobStatus.satisfiedConstraints & JobStatus.CONSTRAINT_DEVICE_NOT_DOZING) != 0); + proto.write(TrackedJob.IS_DOZE_WHITELISTED, jobStatus.dozeWhitelisted); + proto.write(TrackedJob.IS_ALLOWED_IN_DOZE, mAllowInIdleJobs.contains(jobStatus)); + + proto.end(jsToken); + }); + + proto.end(mToken); + proto.end(token); + } + + final class DeviceIdleUpdateFunctor implements Consumer<JobStatus> { + boolean mChanged; + + @Override + public void accept(JobStatus jobStatus) { + mChanged |= updateTaskStateLocked(jobStatus); + } + } + + final class DeviceIdleJobsDelayHandler extends Handler { + public DeviceIdleJobsDelayHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case PROCESS_BACKGROUND_JOBS: + // Just process all the jobs, the ones in foreground should already be running. + synchronized (mLock) { + mDeviceIdleUpdateFunctor.mChanged = false; + mService.getJobStore().forEachJob(mDeviceIdleUpdateFunctor); + if (mDeviceIdleUpdateFunctor.mChanged) { + mStateChangedListener.onControllerStateChanged(); + } + } + break; + } + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java new file mode 100644 index 000000000000..c0b3204192d6 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; +import com.android.server.job.controllers.idle.CarIdlenessTracker; +import com.android.server.job.controllers.idle.DeviceIdlenessTracker; +import com.android.server.job.controllers.idle.IdlenessListener; +import com.android.server.job.controllers.idle.IdlenessTracker; + +import java.util.function.Predicate; + +/** + * Simple controller that tracks whether the device is idle or not. Idleness depends on the device + * type and is not related to device-idle (Doze mode) despite the similar naming. + * + * @see CarIdlenessTracker + * @see DeviceIdlenessTracker + * @see IdlenessTracker + */ +public final class IdleController extends RestrictingController implements IdlenessListener { + private static final String TAG = "JobScheduler.IdleController"; + // Policy: we decide that we're "idle" if the device has been unused / + // screen off or dreaming or wireless charging dock idle for at least this long + final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>(); + IdlenessTracker mIdleTracker; + + public IdleController(JobSchedulerService service) { + super(service); + initIdleStateTracking(mContext); + } + + /** + * StateController interface + */ + @Override + public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { + if (taskStatus.hasIdleConstraint()) { + mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_IDLE); + taskStatus.setIdleConstraintSatisfied(mIdleTracker.isIdle()); + } + } + + @Override + public void startTrackingRestrictedJobLocked(JobStatus jobStatus) { + maybeStartTrackingJobLocked(jobStatus, null); + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, + boolean forUpdate) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_IDLE)) { + mTrackedTasks.remove(taskStatus); + } + } + + @Override + public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) { + if (!jobStatus.hasIdleConstraint()) { + maybeStopTrackingJobLocked(jobStatus, null, false); + } + } + + /** + * State-change notifications from the idleness tracker + */ + @Override + public void reportNewIdleState(boolean isIdle) { + synchronized (mLock) { + for (int i = mTrackedTasks.size()-1; i >= 0; i--) { + mTrackedTasks.valueAt(i).setIdleConstraintSatisfied(isIdle); + } + } + mStateChangedListener.onControllerStateChanged(); + } + + /** + * Idle state tracking, and messaging with the task manager when + * significant state changes occur + */ + private void initIdleStateTracking(Context ctx) { + final boolean isCar = mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_AUTOMOTIVE); + if (isCar) { + mIdleTracker = new CarIdlenessTracker(); + } else { + mIdleTracker = new DeviceIdlenessTracker(); + } + mIdleTracker.startTracking(ctx, this); + } + + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + pw.println("Currently idle: " + mIdleTracker.isIdle()); + pw.println("Idleness tracker:"); mIdleTracker.dump(pw); + pw.println(); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + pw.println(); + } + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.IDLE); + + proto.write(StateControllerProto.IdleController.IS_IDLE, mIdleTracker.isIdle()); + mIdleTracker.dump(proto, StateControllerProto.IdleController.IDLENESS_TRACKER); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + final long jsToken = proto.start(StateControllerProto.IdleController.TRACKED_JOBS); + js.writeToShortProto(proto, StateControllerProto.IdleController.TrackedJob.INFO); + proto.write(StateControllerProto.IdleController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + proto.end(jsToken); + } + + proto.end(mToken); + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java new file mode 100644 index 000000000000..d7be2595e88b --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -0,0 +1,2038 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers; + +import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; +import static com.android.server.job.JobSchedulerService.NEVER_INDEX; +import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; +import static com.android.server.job.JobSchedulerService.WORKING_INDEX; +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.app.AppGlobals; +import android.app.job.JobInfo; +import android.app.job.JobWorkItem; +import android.content.ClipData; +import android.content.ComponentName; +import android.net.Network; +import android.net.Uri; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.MediaStore; +import android.text.format.DateFormat; +import android.util.ArraySet; +import android.util.Pair; +import android.util.Slog; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.FrameworkStatsLog; +import com.android.server.LocalServices; +import com.android.server.job.GrantedUriPermissions; +import com.android.server.job.JobSchedulerInternal; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobServerProtoEnums; +import com.android.server.job.JobStatusDumpProto; +import com.android.server.job.JobStatusShortInfoProto; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.function.Predicate; + +/** + * Uniquely identifies a job internally. + * Created from the public {@link android.app.job.JobInfo} object when it lands on the scheduler. + * Contains current state of the requirements of the job, as well as a function to evaluate + * whether it's ready to run. + * This object is shared among the various controllers - hence why the different fields are atomic. + * This isn't strictly necessary because each controller is only interested in a specific field, + * and the receivers that are listening for global state change will all run on the main looper, + * but we don't enforce that so this is safer. + * + * Test: atest com.android.server.job.controllers.JobStatusTest + * @hide + */ +public final class JobStatus { + private static final String TAG = "JobScheduler.JobStatus"; + static final boolean DEBUG = JobSchedulerService.DEBUG; + + public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE; + public static final long NO_EARLIEST_RUNTIME = 0L; + + static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 < 0 + static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE; // 1 << 2 + static final int CONSTRAINT_BATTERY_NOT_LOW = JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1 + static final int CONSTRAINT_STORAGE_NOT_LOW = JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW; // 1 << 3 + static final int CONSTRAINT_TIMING_DELAY = 1<<31; + static final int CONSTRAINT_DEADLINE = 1<<30; + static final int CONSTRAINT_CONNECTIVITY = 1 << 28; + static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26; + static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint + static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24; // Implicit constraint + static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint + + /** + * The additional set of dynamic constraints that must be met if the job's effective bucket is + * {@link JobSchedulerService#RESTRICTED_INDEX}. Connectivity can be ignored if the job doesn't + * need network. + */ + private static final int DYNAMIC_RESTRICTED_CONSTRAINTS = + CONSTRAINT_BATTERY_NOT_LOW + | CONSTRAINT_CHARGING + | CONSTRAINT_CONNECTIVITY + | CONSTRAINT_IDLE; + + /** + * Standard media URIs that contain the media files that might be important to the user. + * @see #mHasMediaBackupExemption + */ + private static final Uri[] MEDIA_URIS_FOR_STANDBY_EXEMPTION = { + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + }; + + /** + * The constraints that we want to log to statsd. + * + * Constraints that can be inferred from other atoms have been excluded to avoid logging too + * much information and to reduce redundancy: + * + * * CONSTRAINT_CHARGING can be inferred with PluggedStateChanged (Atom #32) + * * CONSTRAINT_BATTERY_NOT_LOW can be inferred with BatteryLevelChanged (Atom #30) + * * CONSTRAINT_CONNECTIVITY can be partially inferred with ConnectivityStateChanged + * (Atom #98) and BatterySaverModeStateChanged (Atom #20). + * * CONSTRAINT_DEVICE_NOT_DOZING can be mostly inferred with DeviceIdleModeStateChanged + * (Atom #21) + * * CONSTRAINT_BACKGROUND_NOT_RESTRICTED can be inferred with BatterySaverModeStateChanged + * (Atom #20) + */ + private static final int STATSD_CONSTRAINTS_TO_LOG = CONSTRAINT_CONTENT_TRIGGER + | CONSTRAINT_DEADLINE + | CONSTRAINT_IDLE + | CONSTRAINT_STORAGE_NOT_LOW + | CONSTRAINT_TIMING_DELAY + | CONSTRAINT_WITHIN_QUOTA; + + // TODO(b/129954980) + private static final boolean STATS_LOG_ENABLED = false; + + // No override. + public static final int OVERRIDE_NONE = 0; + // Override to improve sorting order. Does not affect constraint evaluation. + public static final int OVERRIDE_SORTING = 1; + // Soft override: ignore constraints like time that don't affect API availability + public static final int OVERRIDE_SOFT = 2; + // Full override: ignore all constraints including API-affecting like connectivity + public static final int OVERRIDE_FULL = 3; + + /** If not specified, trigger update delay is 10 seconds. */ + public static final long DEFAULT_TRIGGER_UPDATE_DELAY = 10*1000; + + /** The minimum possible update delay is 1/2 second. */ + public static final long MIN_TRIGGER_UPDATE_DELAY = 500; + + /** If not specified, trigger maximum delay is 2 minutes. */ + public static final long DEFAULT_TRIGGER_MAX_DELAY = 2*60*1000; + + /** The minimum possible update delay is 1 second. */ + public static final long MIN_TRIGGER_MAX_DELAY = 1000; + + final JobInfo job; + /** + * Uid of the package requesting this job. This can differ from the "source" + * uid when the job was scheduled on the app's behalf, such as with the jobs + * that underly Sync Manager operation. + */ + final int callingUid; + final String batteryName; + + /** + * Identity of the app in which the job is hosted. + */ + final String sourcePackageName; + final int sourceUserId; + final int sourceUid; + final String sourceTag; + + final String tag; + + private GrantedUriPermissions uriPerms; + private boolean prepared; + + static final boolean DEBUG_PREPARE = true; + private Throwable unpreparedPoint = null; + + /** + * Earliest point in the future at which this job will be eligible to run. A value of 0 + * indicates there is no delay constraint. See {@link #hasTimingDelayConstraint()}. + */ + private final long earliestRunTimeElapsedMillis; + /** + * Latest point in the future at which this job must be run. A value of {@link Long#MAX_VALUE} + * indicates there is no deadline constraint. See {@link #hasDeadlineConstraint()}. + */ + private final long latestRunTimeElapsedMillis; + + /** + * Valid only for periodic jobs. The original latest point in the future at which this + * job was expected to run. + */ + private long mOriginalLatestRunTimeElapsedMillis; + + /** How many times this job has failed, used to compute back-off. */ + private final int numFailures; + + /** + * Which app standby bucket this job's app is in. Updated when the app is moved to a + * different bucket. + */ + private int standbyBucket; + + /** + * Debugging: timestamp if we ever defer this job based on standby bucketing, this + * is when we did so. + */ + private long whenStandbyDeferred; + + /** The first time this job was force batched. */ + private long mFirstForceBatchedTimeElapsed; + + // Constraints. + final int requiredConstraints; + private final int mRequiredConstraintsOfInterest; + int satisfiedConstraints = 0; + private int mSatisfiedConstraintsOfInterest = 0; + /** + * Set of constraints that must be satisfied for the job if/because it's in the RESTRICTED + * bucket. + */ + private int mDynamicConstraints = 0; + + /** + * Indicates whether the job is responsible for backing up media, so we can be lenient in + * applying standby throttling. + * + * Doesn't exempt jobs with a deadline constraint, as they can be started without any content or + * network changes, in which case this exemption does not make sense. + * + * TODO(b/149519887): Use a more explicit signal, maybe an API flag, that the scheduling package + * needs to provide at the time of scheduling a job. + */ + private final boolean mHasMediaBackupExemption; + + // Set to true if doze constraint was satisfied due to app being whitelisted. + public boolean dozeWhitelisted; + + // Set to true when the app is "active" per AppStateTracker + public boolean uidActive; + + /** + * Flag for {@link #trackingControllers}: the battery controller is currently tracking this job. + */ + public static final int TRACKING_BATTERY = 1<<0; + /** + * Flag for {@link #trackingControllers}: the network connectivity controller is currently + * tracking this job. + */ + public static final int TRACKING_CONNECTIVITY = 1<<1; + /** + * Flag for {@link #trackingControllers}: the content observer controller is currently + * tracking this job. + */ + public static final int TRACKING_CONTENT = 1<<2; + /** + * Flag for {@link #trackingControllers}: the idle controller is currently tracking this job. + */ + public static final int TRACKING_IDLE = 1<<3; + /** + * Flag for {@link #trackingControllers}: the storage controller is currently tracking this job. + */ + public static final int TRACKING_STORAGE = 1<<4; + /** + * Flag for {@link #trackingControllers}: the time controller is currently tracking this job. + */ + public static final int TRACKING_TIME = 1<<5; + /** + * Flag for {@link #trackingControllers}: the quota controller is currently tracking this job. + */ + public static final int TRACKING_QUOTA = 1 << 6; + + /** + * Bit mask of controllers that are currently tracking the job. + */ + private int trackingControllers; + + /** + * Flag for {@link #mInternalFlags}: this job was scheduled when the app that owns the job + * service (not necessarily the caller) was in the foreground and the job has no time + * constraints, which makes it exempted from the battery saver job restriction. + * + * @hide + */ + public static final int INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION = 1 << 0; + + /** + * Versatile, persistable flags for a job that's updated within the system server, + * as opposed to {@link JobInfo#flags} that's set by callers. + */ + private int mInternalFlags; + + // These are filled in by controllers when preparing for execution. + public ArraySet<Uri> changedUris; + public ArraySet<String> changedAuthorities; + public Network network; + + public int lastEvaluatedPriority; + + // If non-null, this is work that has been enqueued for the job. + public ArrayList<JobWorkItem> pendingWork; + + // If non-null, this is work that is currently being executed. + public ArrayList<JobWorkItem> executingWork; + + public int nextPendingWorkId = 1; + + // Used by shell commands + public int overrideState = JobStatus.OVERRIDE_NONE; + + // When this job was enqueued, for ordering. (in elapsedRealtimeMillis) + public long enqueueTime; + + // Metrics about queue latency. (in uptimeMillis) + public long madePending; + public long madeActive; + + /** + * Last time a job finished successfully for a periodic job, in the currentTimeMillis time, + * for dumpsys. + */ + private long mLastSuccessfulRunTime; + + /** + * Last time a job finished unsuccessfully, in the currentTimeMillis time, for dumpsys. + */ + private long mLastFailedRunTime; + + /** + * Transient: when a job is inflated from disk before we have a reliable RTC clock time, + * we retain the canonical (delay, deadline) scheduling tuple read out of the persistent + * store in UTC so that we can fix up the job's scheduling criteria once we get a good + * wall-clock time. If we have to persist the job again before the clock has been updated, + * we record these times again rather than calculating based on the earliest/latest elapsed + * time base figures. + * + * 'first' is the earliest/delay time, and 'second' is the latest/deadline time. + */ + private Pair<Long, Long> mPersistedUtcTimes; + + /** + * For use only by ContentObserverController: state it is maintaining about content URIs + * being observed. + */ + ContentObserverController.JobInstance contentObserverJobInstance; + + private long mTotalNetworkDownloadBytes = JobInfo.NETWORK_BYTES_UNKNOWN; + private long mTotalNetworkUploadBytes = JobInfo.NETWORK_BYTES_UNKNOWN; + + /////// Booleans that track if a job is ready to run. They should be updated whenever dependent + /////// states change. + + /** + * The deadline for the job has passed. This is only good for non-periodic jobs. A periodic job + * should only run if its constraints are satisfied. + * Computed as: NOT periodic AND has deadline constraint AND deadline constraint satisfied. + */ + private boolean mReadyDeadlineSatisfied; + + /** + * The device isn't Dozing or this job will be in the foreground. This implicit constraint must + * be satisfied. + */ + private boolean mReadyNotDozing; + + /** + * The job is not restricted from running in the background (due to Battery Saver). This + * implicit constraint must be satisfied. + */ + private boolean mReadyNotRestrictedInBg; + + /** The job is within its quota based on its standby bucket. */ + private boolean mReadyWithinQuota; + + /** The job's dynamic requirements have been satisfied. */ + private boolean mReadyDynamicSatisfied; + + /** Provide a handle to the service that this job will be run on. */ + public int getServiceToken() { + return callingUid; + } + + /** + * Core constructor for JobStatus instances. All other ctors funnel down to this one. + * + * @param job The actual requested parameters for the job + * @param callingUid Identity of the app that is scheduling the job. This may not be the + * app in which the job is implemented; such as with sync jobs. + * @param sourcePackageName The package name of the app in which the job will run. + * @param sourceUserId The user in which the job will run + * @param standbyBucket The standby bucket that the source package is currently assigned to, + * cached here for speed of handling during runnability evaluations (and updated when bucket + * assignments are changed) + * @param tag A string associated with the job for debugging/logging purposes. + * @param numFailures Count of how many times this job has requested a reschedule because + * its work was not yet finished. + * @param earliestRunTimeElapsedMillis Milestone: earliest point in time at which the job + * is to be considered runnable + * @param latestRunTimeElapsedMillis Milestone: point in time at which the job will be + * considered overdue + * @param lastSuccessfulRunTime When did we last run this job to completion? + * @param lastFailedRunTime When did we last run this job only to have it stop incomplete? + * @param internalFlags Non-API property flags about this job + */ + private JobStatus(JobInfo job, int callingUid, String sourcePackageName, + int sourceUserId, int standbyBucket, String tag, int numFailures, + long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis, + long lastSuccessfulRunTime, long lastFailedRunTime, int internalFlags) { + this.job = job; + this.callingUid = callingUid; + this.standbyBucket = standbyBucket; + + int tempSourceUid = -1; + if (sourceUserId != -1 && sourcePackageName != null) { + try { + tempSourceUid = AppGlobals.getPackageManager().getPackageUid(sourcePackageName, 0, + sourceUserId); + } catch (RemoteException ex) { + // Can't happen, PackageManager runs in the same process. + } + } + if (tempSourceUid == -1) { + this.sourceUid = callingUid; + this.sourceUserId = UserHandle.getUserId(callingUid); + this.sourcePackageName = job.getService().getPackageName(); + this.sourceTag = null; + } else { + this.sourceUid = tempSourceUid; + this.sourceUserId = sourceUserId; + this.sourcePackageName = sourcePackageName; + this.sourceTag = tag; + } + + this.batteryName = this.sourceTag != null + ? this.sourceTag + ":" + job.getService().getPackageName() + : job.getService().flattenToShortString(); + this.tag = "*job*/" + this.batteryName; + + this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis; + this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis; + this.mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsedMillis; + this.numFailures = numFailures; + + boolean requiresNetwork = false; + int requiredConstraints = job.getConstraintFlags(); + if (job.getRequiredNetwork() != null) { + requiredConstraints |= CONSTRAINT_CONNECTIVITY; + requiresNetwork = true; + } + if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME) { + requiredConstraints |= CONSTRAINT_TIMING_DELAY; + } + if (latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) { + requiredConstraints |= CONSTRAINT_DEADLINE; + } + boolean exemptedMediaUrisOnly = false; + if (job.getTriggerContentUris() != null) { + requiredConstraints |= CONSTRAINT_CONTENT_TRIGGER; + exemptedMediaUrisOnly = true; + for (JobInfo.TriggerContentUri uri : job.getTriggerContentUris()) { + if (!ArrayUtils.contains(MEDIA_URIS_FOR_STANDBY_EXEMPTION, uri.getUri())) { + exemptedMediaUrisOnly = false; + break; + } + } + } + this.requiredConstraints = requiredConstraints; + mRequiredConstraintsOfInterest = requiredConstraints & CONSTRAINTS_OF_INTEREST; + mReadyNotDozing = (job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0; + if (standbyBucket == RESTRICTED_INDEX) { + addDynamicConstraints(DYNAMIC_RESTRICTED_CONSTRAINTS); + } else { + mReadyDynamicSatisfied = false; + } + + mLastSuccessfulRunTime = lastSuccessfulRunTime; + mLastFailedRunTime = lastFailedRunTime; + + mInternalFlags = internalFlags; + + updateEstimatedNetworkBytesLocked(); + + if (job.getRequiredNetwork() != null) { + // Later, when we check if a given network satisfies the required + // network, we need to know the UID that is requesting it, so push + // our source UID into place. + job.getRequiredNetwork().networkCapabilities.setSingleUid(this.sourceUid); + } + final JobSchedulerInternal jsi = LocalServices.getService(JobSchedulerInternal.class); + mHasMediaBackupExemption = !job.hasLateConstraint() && exemptedMediaUrisOnly + && requiresNetwork && this.sourcePackageName.equals(jsi.getMediaBackupPackage()); + } + + /** Copy constructor: used specifically when cloning JobStatus objects for persistence, + * so we preserve RTC window bounds if the source object has them. */ + public JobStatus(JobStatus jobStatus) { + this(jobStatus.getJob(), jobStatus.getUid(), + jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(), + jobStatus.getStandbyBucket(), + jobStatus.getSourceTag(), jobStatus.getNumFailures(), + jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(), + jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime(), + jobStatus.getInternalFlags()); + mPersistedUtcTimes = jobStatus.mPersistedUtcTimes; + if (jobStatus.mPersistedUtcTimes != null) { + if (DEBUG) { + Slog.i(TAG, "Cloning job with persisted run times", new RuntimeException("here")); + } + } + } + + /** + * Create a new JobStatus that was loaded from disk. We ignore the provided + * {@link android.app.job.JobInfo} time criteria because we can load a persisted periodic job + * from the {@link com.android.server.job.JobStore} and still want to respect its + * wallclock runtime rather than resetting it on every boot. + * We consider a freshly loaded job to no longer be in back-off, and the associated + * standby bucket is whatever the OS thinks it should be at this moment. + */ + public JobStatus(JobInfo job, int callingUid, String sourcePkgName, int sourceUserId, + int standbyBucket, String sourceTag, + long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis, + long lastSuccessfulRunTime, long lastFailedRunTime, + Pair<Long, Long> persistedExecutionTimesUTC, + int innerFlags) { + this(job, callingUid, sourcePkgName, sourceUserId, + standbyBucket, + sourceTag, 0, + earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, + lastSuccessfulRunTime, lastFailedRunTime, innerFlags); + + // Only during initial inflation do we record the UTC-timebase execution bounds + // read from the persistent store. If we ever have to recreate the JobStatus on + // the fly, it means we're rescheduling the job; and this means that the calculated + // elapsed timebase bounds intrinsically become correct. + this.mPersistedUtcTimes = persistedExecutionTimesUTC; + if (persistedExecutionTimesUTC != null) { + if (DEBUG) { + Slog.i(TAG, "+ restored job with RTC times because of bad boot clock"); + } + } + } + + /** Create a new job to be rescheduled with the provided parameters. */ + public JobStatus(JobStatus rescheduling, + long newEarliestRuntimeElapsedMillis, + long newLatestRuntimeElapsedMillis, int backoffAttempt, + long lastSuccessfulRunTime, long lastFailedRunTime) { + this(rescheduling.job, rescheduling.getUid(), + rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(), + rescheduling.getStandbyBucket(), + rescheduling.getSourceTag(), backoffAttempt, newEarliestRuntimeElapsedMillis, + newLatestRuntimeElapsedMillis, + lastSuccessfulRunTime, lastFailedRunTime, rescheduling.getInternalFlags()); + } + + /** + * Create a newly scheduled job. + * @param callingUid Uid of the package that scheduled this job. + * @param sourcePkg Package name of the app that will actually run the job. Null indicates + * that the calling package is the source. + * @param sourceUserId User id for whom this job is scheduled. -1 indicates this is same as the + * caller. + */ + public static JobStatus createFromJobInfo(JobInfo job, int callingUid, String sourcePkg, + int sourceUserId, String tag) { + final long elapsedNow = sElapsedRealtimeClock.millis(); + final long earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis; + if (job.isPeriodic()) { + // Make sure period is in the interval [min_possible_period, max_possible_period]. + final long period = Math.max(JobInfo.getMinPeriodMillis(), + Math.min(JobSchedulerService.MAX_ALLOWED_PERIOD_MS, job.getIntervalMillis())); + latestRunTimeElapsedMillis = elapsedNow + period; + earliestRunTimeElapsedMillis = latestRunTimeElapsedMillis + // Make sure flex is in the interval [min_possible_flex, period]. + - Math.max(JobInfo.getMinFlexMillis(), Math.min(period, job.getFlexMillis())); + } else { + earliestRunTimeElapsedMillis = job.hasEarlyConstraint() ? + elapsedNow + job.getMinLatencyMillis() : NO_EARLIEST_RUNTIME; + latestRunTimeElapsedMillis = job.hasLateConstraint() ? + elapsedNow + job.getMaxExecutionDelayMillis() : NO_LATEST_RUNTIME; + } + String jobPackage = (sourcePkg != null) ? sourcePkg : job.getService().getPackageName(); + + int standbyBucket = JobSchedulerService.standbyBucketForPackage(jobPackage, + sourceUserId, elapsedNow); + return new JobStatus(job, callingUid, sourcePkg, sourceUserId, + standbyBucket, tag, 0, + earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, + 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */, + /*innerFlags=*/ 0); + } + + public void enqueueWorkLocked(JobWorkItem work) { + if (pendingWork == null) { + pendingWork = new ArrayList<>(); + } + work.setWorkId(nextPendingWorkId); + nextPendingWorkId++; + if (work.getIntent() != null + && GrantedUriPermissions.checkGrantFlags(work.getIntent().getFlags())) { + work.setGrants(GrantedUriPermissions.createFromIntent(work.getIntent(), sourceUid, + sourcePackageName, sourceUserId, toShortString())); + } + pendingWork.add(work); + updateEstimatedNetworkBytesLocked(); + } + + public JobWorkItem dequeueWorkLocked() { + if (pendingWork != null && pendingWork.size() > 0) { + JobWorkItem work = pendingWork.remove(0); + if (work != null) { + if (executingWork == null) { + executingWork = new ArrayList<>(); + } + executingWork.add(work); + work.bumpDeliveryCount(); + } + updateEstimatedNetworkBytesLocked(); + return work; + } + return null; + } + + public boolean hasWorkLocked() { + return (pendingWork != null && pendingWork.size() > 0) || hasExecutingWorkLocked(); + } + + public boolean hasExecutingWorkLocked() { + return executingWork != null && executingWork.size() > 0; + } + + private static void ungrantWorkItem(JobWorkItem work) { + if (work.getGrants() != null) { + ((GrantedUriPermissions)work.getGrants()).revoke(); + } + } + + public boolean completeWorkLocked(int workId) { + if (executingWork != null) { + final int N = executingWork.size(); + for (int i = 0; i < N; i++) { + JobWorkItem work = executingWork.get(i); + if (work.getWorkId() == workId) { + executingWork.remove(i); + ungrantWorkItem(work); + return true; + } + } + } + return false; + } + + private static void ungrantWorkList(ArrayList<JobWorkItem> list) { + if (list != null) { + final int N = list.size(); + for (int i = 0; i < N; i++) { + ungrantWorkItem(list.get(i)); + } + } + } + + public void stopTrackingJobLocked(JobStatus incomingJob) { + if (incomingJob != null) { + // We are replacing with a new job -- transfer the work! We do any executing + // work first, since that was originally at the front of the pending work. + if (executingWork != null && executingWork.size() > 0) { + incomingJob.pendingWork = executingWork; + } + if (incomingJob.pendingWork == null) { + incomingJob.pendingWork = pendingWork; + } else if (pendingWork != null && pendingWork.size() > 0) { + incomingJob.pendingWork.addAll(pendingWork); + } + pendingWork = null; + executingWork = null; + incomingJob.nextPendingWorkId = nextPendingWorkId; + incomingJob.updateEstimatedNetworkBytesLocked(); + } else { + // We are completely stopping the job... need to clean up work. + ungrantWorkList(pendingWork); + pendingWork = null; + ungrantWorkList(executingWork); + executingWork = null; + } + updateEstimatedNetworkBytesLocked(); + } + + public void prepareLocked() { + if (prepared) { + Slog.wtf(TAG, "Already prepared: " + this); + return; + } + prepared = true; + if (DEBUG_PREPARE) { + unpreparedPoint = null; + } + final ClipData clip = job.getClipData(); + if (clip != null) { + uriPerms = GrantedUriPermissions.createFromClip(clip, sourceUid, sourcePackageName, + sourceUserId, job.getClipGrantFlags(), toShortString()); + } + } + + public void unprepareLocked() { + if (!prepared) { + Slog.wtf(TAG, "Hasn't been prepared: " + this); + if (DEBUG_PREPARE && unpreparedPoint != null) { + Slog.e(TAG, "Was already unprepared at ", unpreparedPoint); + } + return; + } + prepared = false; + if (DEBUG_PREPARE) { + unpreparedPoint = new Throwable().fillInStackTrace(); + } + if (uriPerms != null) { + uriPerms.revoke(); + uriPerms = null; + } + } + + public boolean isPreparedLocked() { + return prepared; + } + + public JobInfo getJob() { + return job; + } + + public int getJobId() { + return job.getId(); + } + + public void printUniqueId(PrintWriter pw) { + UserHandle.formatUid(pw, callingUid); + pw.print("/"); + pw.print(job.getId()); + } + + public int getNumFailures() { + return numFailures; + } + + public ComponentName getServiceComponent() { + return job.getService(); + } + + public String getSourcePackageName() { + return sourcePackageName; + } + + public int getSourceUid() { + return sourceUid; + } + + public int getSourceUserId() { + return sourceUserId; + } + + public int getUserId() { + return UserHandle.getUserId(callingUid); + } + + /** + * Returns an appropriate standby bucket for the job, taking into account any standby + * exemptions. + */ + public int getEffectiveStandbyBucket() { + if (uidActive || getJob().isExemptedFromAppStandby()) { + // Treat these cases as if they're in the ACTIVE bucket so that they get throttled + // like other ACTIVE apps. + return ACTIVE_INDEX; + } + final int actualBucket = getStandbyBucket(); + if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX + && mHasMediaBackupExemption) { + // Cap it at WORKING_INDEX as media back up jobs are important to the user, and the + // source package may not have been used directly in a while. + return Math.min(WORKING_INDEX, actualBucket); + } + return actualBucket; + } + + /** Returns the real standby bucket of the job. */ + public int getStandbyBucket() { + return standbyBucket; + } + + public void setStandbyBucket(int newBucket) { + if (newBucket == RESTRICTED_INDEX) { + // Adding to the bucket. + addDynamicConstraints(DYNAMIC_RESTRICTED_CONSTRAINTS); + } else if (standbyBucket == RESTRICTED_INDEX) { + // Removing from the RESTRICTED bucket. + removeDynamicConstraints(DYNAMIC_RESTRICTED_CONSTRAINTS); + } + + standbyBucket = newBucket; + } + + // Called only by the standby monitoring code + public long getWhenStandbyDeferred() { + return whenStandbyDeferred; + } + + // Called only by the standby monitoring code + public void setWhenStandbyDeferred(long now) { + whenStandbyDeferred = now; + } + + /** + * Returns the first time this job was force batched, in the elapsed realtime timebase. Will be + * 0 if this job was never force batched. + */ + public long getFirstForceBatchedTimeElapsed() { + return mFirstForceBatchedTimeElapsed; + } + + public void setFirstForceBatchedTimeElapsed(long now) { + mFirstForceBatchedTimeElapsed = now; + } + + public String getSourceTag() { + return sourceTag; + } + + public int getUid() { + return callingUid; + } + + public String getBatteryName() { + return batteryName; + } + + public String getTag() { + return tag; + } + + public int getPriority() { + return job.getPriority(); + } + + public int getFlags() { + return job.getFlags(); + } + + public int getInternalFlags() { + return mInternalFlags; + } + + public void addInternalFlags(int flags) { + mInternalFlags |= flags; + } + + public int getSatisfiedConstraintFlags() { + return satisfiedConstraints; + } + + public void maybeAddForegroundExemption(Predicate<Integer> uidForegroundChecker) { + // Jobs with time constraints shouldn't be exempted. + if (job.hasEarlyConstraint() || job.hasLateConstraint()) { + return; + } + // Already exempted, skip the foreground check. + if ((mInternalFlags & INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0) { + return; + } + if (uidForegroundChecker.test(getSourceUid())) { + addInternalFlags(INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION); + } + } + + private void updateEstimatedNetworkBytesLocked() { + mTotalNetworkDownloadBytes = job.getEstimatedNetworkDownloadBytes(); + mTotalNetworkUploadBytes = job.getEstimatedNetworkUploadBytes(); + + if (pendingWork != null) { + for (int i = 0; i < pendingWork.size(); i++) { + if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + // If any component of the job has unknown usage, we don't have a + // complete picture of what data will be used, and we have to treat the + // entire up/download as unknown. + long downloadBytes = pendingWork.get(i).getEstimatedNetworkDownloadBytes(); + if (downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + mTotalNetworkDownloadBytes += downloadBytes; + } + } + if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + // If any component of the job has unknown usage, we don't have a + // complete picture of what data will be used, and we have to treat the + // entire up/download as unknown. + long uploadBytes = pendingWork.get(i).getEstimatedNetworkUploadBytes(); + if (uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + mTotalNetworkUploadBytes += uploadBytes; + } + } + } + } + } + + public long getEstimatedNetworkDownloadBytes() { + return mTotalNetworkDownloadBytes; + } + + public long getEstimatedNetworkUploadBytes() { + return mTotalNetworkUploadBytes; + } + + /** Does this job have any sort of networking constraint? */ + public boolean hasConnectivityConstraint() { + // No need to check mDynamicConstraints since connectivity will only be in that list if + // it's already in the requiredConstraints list. + return (requiredConstraints&CONSTRAINT_CONNECTIVITY) != 0; + } + + public boolean hasChargingConstraint() { + return hasConstraint(CONSTRAINT_CHARGING); + } + + public boolean hasBatteryNotLowConstraint() { + return hasConstraint(CONSTRAINT_BATTERY_NOT_LOW); + } + + /** Returns true if the job requires charging OR battery not low. */ + boolean hasPowerConstraint() { + return hasConstraint(CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW); + } + + public boolean hasStorageNotLowConstraint() { + return hasConstraint(CONSTRAINT_STORAGE_NOT_LOW); + } + + public boolean hasTimingDelayConstraint() { + return hasConstraint(CONSTRAINT_TIMING_DELAY); + } + + public boolean hasDeadlineConstraint() { + return hasConstraint(CONSTRAINT_DEADLINE); + } + + public boolean hasIdleConstraint() { + return hasConstraint(CONSTRAINT_IDLE); + } + + public boolean hasContentTriggerConstraint() { + // No need to check mDynamicConstraints since content trigger will only be in that list if + // it's already in the requiredConstraints list. + return (requiredConstraints&CONSTRAINT_CONTENT_TRIGGER) != 0; + } + + /** + * Checks both {@link #requiredConstraints} and {@link #mDynamicConstraints} to see if this job + * requires the specified constraint. + */ + private boolean hasConstraint(int constraint) { + return (requiredConstraints & constraint) != 0 || (mDynamicConstraints & constraint) != 0; + } + + public long getTriggerContentUpdateDelay() { + long time = job.getTriggerContentUpdateDelay(); + if (time < 0) { + return DEFAULT_TRIGGER_UPDATE_DELAY; + } + return Math.max(time, MIN_TRIGGER_UPDATE_DELAY); + } + + public long getTriggerContentMaxDelay() { + long time = job.getTriggerContentMaxDelay(); + if (time < 0) { + return DEFAULT_TRIGGER_MAX_DELAY; + } + return Math.max(time, MIN_TRIGGER_MAX_DELAY); + } + + public boolean isPersisted() { + return job.isPersisted(); + } + + public long getEarliestRunTime() { + return earliestRunTimeElapsedMillis; + } + + public long getLatestRunTimeElapsed() { + return latestRunTimeElapsedMillis; + } + + public long getOriginalLatestRunTimeElapsed() { + return mOriginalLatestRunTimeElapsedMillis; + } + + public void setOriginalLatestRunTimeElapsed(long latestRunTimeElapsed) { + mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsed; + } + + /** + * Return the fractional position of "now" within the "run time" window of + * this job. + * <p> + * For example, if the earliest run time was 10 minutes ago, and the latest + * run time is 30 minutes from now, this would return 0.25. + * <p> + * If the job has no window defined, returns 1. When only an earliest or + * latest time is defined, it's treated as an infinitely small window at + * that time. + */ + public float getFractionRunTime() { + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + if (earliestRunTimeElapsedMillis == 0 && latestRunTimeElapsedMillis == Long.MAX_VALUE) { + return 1; + } else if (earliestRunTimeElapsedMillis == 0) { + return now >= latestRunTimeElapsedMillis ? 1 : 0; + } else if (latestRunTimeElapsedMillis == Long.MAX_VALUE) { + return now >= earliestRunTimeElapsedMillis ? 1 : 0; + } else { + if (now <= earliestRunTimeElapsedMillis) { + return 0; + } else if (now >= latestRunTimeElapsedMillis) { + return 1; + } else { + return (float) (now - earliestRunTimeElapsedMillis) + / (float) (latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis); + } + } + } + + public Pair<Long, Long> getPersistedUtcTimes() { + return mPersistedUtcTimes; + } + + public void clearPersistedUtcTimes() { + mPersistedUtcTimes = null; + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setChargingConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_CHARGING, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setBatteryNotLowConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_BATTERY_NOT_LOW, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setStorageNotLowConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_STORAGE_NOT_LOW, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setTimingDelayConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_TIMING_DELAY, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setDeadlineConstraintSatisfied(boolean state) { + if (setConstraintSatisfied(CONSTRAINT_DEADLINE, state)) { + // The constraint was changed. Update the ready flag. + mReadyDeadlineSatisfied = !job.isPeriodic() && hasDeadlineConstraint() && state; + return true; + } + return false; + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setIdleConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_IDLE, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setConnectivityConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_CONNECTIVITY, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setContentTriggerConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_CONTENT_TRIGGER, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setDeviceNotDozingConstraintSatisfied(boolean state, boolean whitelisted) { + dozeWhitelisted = whitelisted; + if (setConstraintSatisfied(CONSTRAINT_DEVICE_NOT_DOZING, state)) { + // The constraint was changed. Update the ready flag. + mReadyNotDozing = state || (job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0; + return true; + } + return false; + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setBackgroundNotRestrictedConstraintSatisfied(boolean state) { + if (setConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED, state)) { + // The constraint was changed. Update the ready flag. + mReadyNotRestrictedInBg = state; + return true; + } + return false; + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setQuotaConstraintSatisfied(boolean state) { + if (setConstraintSatisfied(CONSTRAINT_WITHIN_QUOTA, state)) { + // The constraint was changed. Update the ready flag. + mReadyWithinQuota = state; + return true; + } + return false; + } + + /** @return true if the state was changed, false otherwise. */ + boolean setUidActive(final boolean newActiveState) { + if (newActiveState != uidActive) { + uidActive = newActiveState; + return true; + } + return false; /* unchanged */ + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setConstraintSatisfied(int constraint, boolean state) { + boolean old = (satisfiedConstraints&constraint) != 0; + if (old == state) { + return false; + } + if (DEBUG) { + Slog.v(TAG, + "Constraint " + constraint + " is " + (!state ? "NOT " : "") + "satisfied for " + + toShortString()); + } + satisfiedConstraints = (satisfiedConstraints&~constraint) | (state ? constraint : 0); + mSatisfiedConstraintsOfInterest = satisfiedConstraints & CONSTRAINTS_OF_INTEREST; + mReadyDynamicSatisfied = mDynamicConstraints != 0 + && mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints); + if (STATS_LOG_ENABLED && (STATSD_CONSTRAINTS_TO_LOG & constraint) != 0) { + FrameworkStatsLog.write_non_chained( + FrameworkStatsLog.SCHEDULED_JOB_CONSTRAINT_CHANGED, + sourceUid, null, getBatteryName(), getProtoConstraint(constraint), + state ? FrameworkStatsLog.SCHEDULED_JOB_CONSTRAINT_CHANGED__STATE__SATISFIED + : FrameworkStatsLog + .SCHEDULED_JOB_CONSTRAINT_CHANGED__STATE__UNSATISFIED); + } + return true; + } + + boolean isConstraintSatisfied(int constraint) { + return (satisfiedConstraints&constraint) != 0; + } + + boolean clearTrackingController(int which) { + if ((trackingControllers&which) != 0) { + trackingControllers &= ~which; + return true; + } + return false; + } + + void setTrackingController(int which) { + trackingControllers |= which; + } + + /** + * Indicates that this job cannot run without the specified constraints. This is evaluated + * separately from the job's explicitly requested constraints and MUST be satisfied before + * the job can run if the app doesn't have quota. + */ + private void addDynamicConstraints(int constraints) { + if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) { + // Quota should never be used as a dynamic constraint. + Slog.wtf(TAG, "Tried to set quota as a dynamic constraint"); + constraints &= ~CONSTRAINT_WITHIN_QUOTA; + } + + // Connectivity and content trigger are special since they're only valid to add if the + // job has requested network or specific content URIs. Adding these constraints to jobs + // that don't need them doesn't make sense. + if (!hasConnectivityConstraint()) { + constraints &= ~CONSTRAINT_CONNECTIVITY; + } + if (!hasContentTriggerConstraint()) { + constraints &= ~CONSTRAINT_CONTENT_TRIGGER; + } + + mDynamicConstraints |= constraints; + mReadyDynamicSatisfied = mDynamicConstraints != 0 + && mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints); + } + + /** + * Removes dynamic constraints from a job, meaning that the requirements are not required for + * the job to run (if the job itself hasn't requested the constraint. This is separate from + * the job's explicitly requested constraints and does not remove those requested constraints. + * + */ + private void removeDynamicConstraints(int constraints) { + mDynamicConstraints &= ~constraints; + mReadyDynamicSatisfied = mDynamicConstraints != 0 + && mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints); + } + + public long getLastSuccessfulRunTime() { + return mLastSuccessfulRunTime; + } + + public long getLastFailedRunTime() { + return mLastFailedRunTime; + } + + /** + * @return Whether or not this job is ready to run, based on its requirements. + */ + public boolean isReady() { + return isReady(mSatisfiedConstraintsOfInterest); + } + + /** + * @return Whether or not this job would be ready to run if it had the specified constraint + * granted, based on its requirements. + */ + boolean wouldBeReadyWithConstraint(int constraint) { + boolean oldValue = false; + int satisfied = mSatisfiedConstraintsOfInterest; + switch (constraint) { + case CONSTRAINT_BACKGROUND_NOT_RESTRICTED: + oldValue = mReadyNotRestrictedInBg; + mReadyNotRestrictedInBg = true; + break; + case CONSTRAINT_DEADLINE: + oldValue = mReadyDeadlineSatisfied; + mReadyDeadlineSatisfied = true; + break; + case CONSTRAINT_DEVICE_NOT_DOZING: + oldValue = mReadyNotDozing; + mReadyNotDozing = true; + break; + case CONSTRAINT_WITHIN_QUOTA: + oldValue = mReadyWithinQuota; + mReadyWithinQuota = true; + break; + default: + satisfied |= constraint; + mReadyDynamicSatisfied = mDynamicConstraints != 0 + && mDynamicConstraints == (satisfied & mDynamicConstraints); + break; + } + + boolean toReturn = isReady(satisfied); + + switch (constraint) { + case CONSTRAINT_BACKGROUND_NOT_RESTRICTED: + mReadyNotRestrictedInBg = oldValue; + break; + case CONSTRAINT_DEADLINE: + mReadyDeadlineSatisfied = oldValue; + break; + case CONSTRAINT_DEVICE_NOT_DOZING: + mReadyNotDozing = oldValue; + break; + case CONSTRAINT_WITHIN_QUOTA: + mReadyWithinQuota = oldValue; + break; + default: + mReadyDynamicSatisfied = mDynamicConstraints != 0 + && mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints); + break; + } + return toReturn; + } + + private boolean isReady(int satisfiedConstraints) { + // Quota and dynamic constraints trump all other constraints. + // NEVER jobs are not supposed to run at all. Since we're using quota to allow parole + // sessions (exempt from dynamic restrictions), we need the additional check to ensure + // that NEVER jobs don't run. + // TODO: cleanup quota and standby bucket management so we don't need the additional checks + if ((!mReadyWithinQuota && !mReadyDynamicSatisfied) + || getEffectiveStandbyBucket() == NEVER_INDEX) { + return false; + } + // Deadline constraint trumps other constraints besides quota and dynamic (except for + // periodic jobs where deadline is an implementation detail. A periodic job should only + // run if its constraints are satisfied). + // DeviceNotDozing implicit constraint must be satisfied + // NotRestrictedInBackground implicit constraint must be satisfied + return mReadyNotDozing && mReadyNotRestrictedInBg && (mReadyDeadlineSatisfied + || isConstraintsSatisfied(satisfiedConstraints)); + } + + /** All constraints besides implicit and deadline. */ + static final int CONSTRAINTS_OF_INTEREST = CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW + | CONSTRAINT_STORAGE_NOT_LOW | CONSTRAINT_TIMING_DELAY | CONSTRAINT_CONNECTIVITY + | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER; + + // Soft override covers all non-"functional" constraints + static final int SOFT_OVERRIDE_CONSTRAINTS = + CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_STORAGE_NOT_LOW + | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE; + + /** Returns true whenever all dynamically set constraints are satisfied. */ + public boolean areDynamicConstraintsSatisfied() { + return mReadyDynamicSatisfied; + } + + /** + * @return Whether the constraints set on this job are satisfied. + */ + public boolean isConstraintsSatisfied() { + return isConstraintsSatisfied(mSatisfiedConstraintsOfInterest); + } + + private boolean isConstraintsSatisfied(int satisfiedConstraints) { + if (overrideState == OVERRIDE_FULL) { + // force override: the job is always runnable + return true; + } + + int sat = satisfiedConstraints; + if (overrideState == OVERRIDE_SOFT) { + // override: pretend all 'soft' requirements are satisfied + sat |= (requiredConstraints & SOFT_OVERRIDE_CONSTRAINTS); + } + + return (sat & mRequiredConstraintsOfInterest) == mRequiredConstraintsOfInterest; + } + + public boolean matches(int uid, int jobId) { + return this.job.getId() == jobId && this.callingUid == uid; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("JobStatus{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" #"); + UserHandle.formatUid(sb, callingUid); + sb.append("/"); + sb.append(job.getId()); + sb.append(' '); + sb.append(batteryName); + sb.append(" u="); + sb.append(getUserId()); + sb.append(" s="); + sb.append(getSourceUid()); + if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME + || latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) { + long now = sElapsedRealtimeClock.millis(); + sb.append(" TIME="); + formatRunTime(sb, earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME, now); + sb.append(":"); + formatRunTime(sb, latestRunTimeElapsedMillis, NO_LATEST_RUNTIME, now); + } + if (job.getRequiredNetwork() != null) { + sb.append(" NET"); + } + if (job.isRequireCharging()) { + sb.append(" CHARGING"); + } + if (job.isRequireBatteryNotLow()) { + sb.append(" BATNOTLOW"); + } + if (job.isRequireStorageNotLow()) { + sb.append(" STORENOTLOW"); + } + if (job.isRequireDeviceIdle()) { + sb.append(" IDLE"); + } + if (job.isPeriodic()) { + sb.append(" PERIODIC"); + } + if (job.isPersisted()) { + sb.append(" PERSISTED"); + } + if ((satisfiedConstraints&CONSTRAINT_DEVICE_NOT_DOZING) == 0) { + sb.append(" WAIT:DEV_NOT_DOZING"); + } + if (job.getTriggerContentUris() != null) { + sb.append(" URIS="); + sb.append(Arrays.toString(job.getTriggerContentUris())); + } + if (numFailures != 0) { + sb.append(" failures="); + sb.append(numFailures); + } + if (isReady()) { + sb.append(" READY"); + } + sb.append("}"); + return sb.toString(); + } + + private void formatRunTime(PrintWriter pw, long runtime, long defaultValue, long now) { + if (runtime == defaultValue) { + pw.print("none"); + } else { + TimeUtils.formatDuration(runtime - now, pw); + } + } + + private void formatRunTime(StringBuilder sb, long runtime, long defaultValue, long now) { + if (runtime == defaultValue) { + sb.append("none"); + } else { + TimeUtils.formatDuration(runtime - now, sb); + } + } + + /** + * Convenience function to identify a job uniquely without pulling all the data that + * {@link #toString()} returns. + */ + public String toShortString() { + StringBuilder sb = new StringBuilder(); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" #"); + UserHandle.formatUid(sb, callingUid); + sb.append("/"); + sb.append(job.getId()); + sb.append(' '); + sb.append(batteryName); + return sb.toString(); + } + + /** + * Convenience function to identify a job uniquely without pulling all the data that + * {@link #toString()} returns. + */ + public String toShortStringExceptUniqueId() { + StringBuilder sb = new StringBuilder(); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(' '); + sb.append(batteryName); + return sb.toString(); + } + + /** + * Convenience function to dump data that identifies a job uniquely to proto. This is intended + * to mimic {@link #toShortString}. + */ + public void writeToShortProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write(JobStatusShortInfoProto.CALLING_UID, callingUid); + proto.write(JobStatusShortInfoProto.JOB_ID, job.getId()); + proto.write(JobStatusShortInfoProto.BATTERY_NAME, batteryName); + + proto.end(token); + } + + void dumpConstraints(PrintWriter pw, int constraints) { + if ((constraints&CONSTRAINT_CHARGING) != 0) { + pw.print(" CHARGING"); + } + if ((constraints& CONSTRAINT_BATTERY_NOT_LOW) != 0) { + pw.print(" BATTERY_NOT_LOW"); + } + if ((constraints& CONSTRAINT_STORAGE_NOT_LOW) != 0) { + pw.print(" STORAGE_NOT_LOW"); + } + if ((constraints&CONSTRAINT_TIMING_DELAY) != 0) { + pw.print(" TIMING_DELAY"); + } + if ((constraints&CONSTRAINT_DEADLINE) != 0) { + pw.print(" DEADLINE"); + } + if ((constraints&CONSTRAINT_IDLE) != 0) { + pw.print(" IDLE"); + } + if ((constraints&CONSTRAINT_CONNECTIVITY) != 0) { + pw.print(" CONNECTIVITY"); + } + if ((constraints&CONSTRAINT_CONTENT_TRIGGER) != 0) { + pw.print(" CONTENT_TRIGGER"); + } + if ((constraints&CONSTRAINT_DEVICE_NOT_DOZING) != 0) { + pw.print(" DEVICE_NOT_DOZING"); + } + if ((constraints&CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) { + pw.print(" BACKGROUND_NOT_RESTRICTED"); + } + if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) { + pw.print(" WITHIN_QUOTA"); + } + if (constraints != 0) { + pw.print(" [0x"); + pw.print(Integer.toHexString(constraints)); + pw.print("]"); + } + } + + /** Returns a {@link JobServerProtoEnums.Constraint} enum value for the given constraint. */ + private int getProtoConstraint(int constraint) { + switch (constraint) { + case CONSTRAINT_BACKGROUND_NOT_RESTRICTED: + return JobServerProtoEnums.CONSTRAINT_BACKGROUND_NOT_RESTRICTED; + case CONSTRAINT_BATTERY_NOT_LOW: + return JobServerProtoEnums.CONSTRAINT_BATTERY_NOT_LOW; + case CONSTRAINT_CHARGING: + return JobServerProtoEnums.CONSTRAINT_CHARGING; + case CONSTRAINT_CONNECTIVITY: + return JobServerProtoEnums.CONSTRAINT_CONNECTIVITY; + case CONSTRAINT_CONTENT_TRIGGER: + return JobServerProtoEnums.CONSTRAINT_CONTENT_TRIGGER; + case CONSTRAINT_DEADLINE: + return JobServerProtoEnums.CONSTRAINT_DEADLINE; + case CONSTRAINT_DEVICE_NOT_DOZING: + return JobServerProtoEnums.CONSTRAINT_DEVICE_NOT_DOZING; + case CONSTRAINT_IDLE: + return JobServerProtoEnums.CONSTRAINT_IDLE; + case CONSTRAINT_STORAGE_NOT_LOW: + return JobServerProtoEnums.CONSTRAINT_STORAGE_NOT_LOW; + case CONSTRAINT_TIMING_DELAY: + return JobServerProtoEnums.CONSTRAINT_TIMING_DELAY; + case CONSTRAINT_WITHIN_QUOTA: + return JobServerProtoEnums.CONSTRAINT_WITHIN_QUOTA; + default: + return JobServerProtoEnums.CONSTRAINT_UNKNOWN; + } + } + + /** Writes constraints to the given repeating proto field. */ + void dumpConstraints(ProtoOutputStream proto, long fieldId, int constraints) { + if ((constraints & CONSTRAINT_CHARGING) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_CHARGING); + } + if ((constraints & CONSTRAINT_BATTERY_NOT_LOW) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_BATTERY_NOT_LOW); + } + if ((constraints & CONSTRAINT_STORAGE_NOT_LOW) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_STORAGE_NOT_LOW); + } + if ((constraints & CONSTRAINT_TIMING_DELAY) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_TIMING_DELAY); + } + if ((constraints & CONSTRAINT_DEADLINE) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_DEADLINE); + } + if ((constraints & CONSTRAINT_IDLE) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_IDLE); + } + if ((constraints & CONSTRAINT_CONNECTIVITY) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_CONNECTIVITY); + } + if ((constraints & CONSTRAINT_CONTENT_TRIGGER) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_CONTENT_TRIGGER); + } + if ((constraints & CONSTRAINT_DEVICE_NOT_DOZING) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_DEVICE_NOT_DOZING); + } + if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_WITHIN_QUOTA); + } + if ((constraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_BACKGROUND_NOT_RESTRICTED); + } + } + + private void dumpJobWorkItem(PrintWriter pw, String prefix, JobWorkItem work, int index) { + pw.print(prefix); pw.print(" #"); pw.print(index); pw.print(": #"); + pw.print(work.getWorkId()); pw.print(" "); pw.print(work.getDeliveryCount()); + pw.print("x "); pw.println(work.getIntent()); + if (work.getGrants() != null) { + pw.print(prefix); pw.println(" URI grants:"); + ((GrantedUriPermissions)work.getGrants()).dump(pw, prefix + " "); + } + } + + private void dumpJobWorkItem(ProtoOutputStream proto, long fieldId, JobWorkItem work) { + final long token = proto.start(fieldId); + + proto.write(JobStatusDumpProto.JobWorkItem.WORK_ID, work.getWorkId()); + proto.write(JobStatusDumpProto.JobWorkItem.DELIVERY_COUNT, work.getDeliveryCount()); + if (work.getIntent() != null) { + work.getIntent().dumpDebug(proto, JobStatusDumpProto.JobWorkItem.INTENT); + } + Object grants = work.getGrants(); + if (grants != null) { + ((GrantedUriPermissions) grants).dump(proto, JobStatusDumpProto.JobWorkItem.URI_GRANTS); + } + + proto.end(token); + } + + /** + * Returns a bucket name based on the normalized bucket indices, not the AppStandby constants. + */ + String getBucketName() { + return bucketName(standbyBucket); + } + + /** + * Returns a bucket name based on the normalized bucket indices, not the AppStandby constants. + */ + static String bucketName(int standbyBucket) { + switch (standbyBucket) { + case 0: return "ACTIVE"; + case 1: return "WORKING_SET"; + case 2: return "FREQUENT"; + case 3: return "RARE"; + case 4: return "NEVER"; + case 5: + return "RESTRICTED"; + default: + return "Unknown: " + standbyBucket; + } + } + + // Dumpsys infrastructure + public void dump(PrintWriter pw, String prefix, boolean full, long elapsedRealtimeMillis) { + pw.print(prefix); UserHandle.formatUid(pw, callingUid); + pw.print(" tag="); pw.println(tag); + pw.print(prefix); + pw.print("Source: uid="); UserHandle.formatUid(pw, getSourceUid()); + pw.print(" user="); pw.print(getSourceUserId()); + pw.print(" pkg="); pw.println(getSourcePackageName()); + if (full) { + pw.print(prefix); pw.println("JobInfo:"); + pw.print(prefix); pw.print(" Service: "); + pw.println(job.getService().flattenToShortString()); + if (job.isPeriodic()) { + pw.print(prefix); pw.print(" PERIODIC: interval="); + TimeUtils.formatDuration(job.getIntervalMillis(), pw); + pw.print(" flex="); TimeUtils.formatDuration(job.getFlexMillis(), pw); + pw.println(); + } + if (job.isPersisted()) { + pw.print(prefix); pw.println(" PERSISTED"); + } + if (job.getPriority() != 0) { + pw.print(prefix); pw.print(" Priority: "); + pw.println(JobInfo.getPriorityString(job.getPriority())); + } + if (job.getFlags() != 0) { + pw.print(prefix); pw.print(" Flags: "); + pw.println(Integer.toHexString(job.getFlags())); + } + if (getInternalFlags() != 0) { + pw.print(prefix); pw.print(" Internal flags: "); + pw.print(Integer.toHexString(getInternalFlags())); + + if ((getInternalFlags()&INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0) { + pw.print(" HAS_FOREGROUND_EXEMPTION"); + } + pw.println(); + } + pw.print(prefix); pw.print(" Requires: charging="); + pw.print(job.isRequireCharging()); pw.print(" batteryNotLow="); + pw.print(job.isRequireBatteryNotLow()); pw.print(" deviceIdle="); + pw.println(job.isRequireDeviceIdle()); + if (job.getTriggerContentUris() != null) { + pw.print(prefix); pw.println(" Trigger content URIs:"); + for (int i = 0; i < job.getTriggerContentUris().length; i++) { + JobInfo.TriggerContentUri trig = job.getTriggerContentUris()[i]; + pw.print(prefix); pw.print(" "); + pw.print(Integer.toHexString(trig.getFlags())); + pw.print(' '); pw.println(trig.getUri()); + } + if (job.getTriggerContentUpdateDelay() >= 0) { + pw.print(prefix); pw.print(" Trigger update delay: "); + TimeUtils.formatDuration(job.getTriggerContentUpdateDelay(), pw); + pw.println(); + } + if (job.getTriggerContentMaxDelay() >= 0) { + pw.print(prefix); pw.print(" Trigger max delay: "); + TimeUtils.formatDuration(job.getTriggerContentMaxDelay(), pw); + pw.println(); + } + } + if (job.getExtras() != null && !job.getExtras().isDefinitelyEmpty()) { + pw.print(prefix); pw.print(" Extras: "); + pw.println(job.getExtras().toShortString()); + } + if (job.getTransientExtras() != null && !job.getTransientExtras().isDefinitelyEmpty()) { + pw.print(prefix); pw.print(" Transient extras: "); + pw.println(job.getTransientExtras().toShortString()); + } + if (job.getClipData() != null) { + pw.print(prefix); pw.print(" Clip data: "); + StringBuilder b = new StringBuilder(128); + b.append(job.getClipData()); + pw.println(b); + } + if (uriPerms != null) { + pw.print(prefix); pw.println(" Granted URI permissions:"); + uriPerms.dump(pw, prefix + " "); + } + if (job.getRequiredNetwork() != null) { + pw.print(prefix); pw.print(" Network type: "); + pw.println(job.getRequiredNetwork()); + } + if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + pw.print(prefix); pw.print(" Network download bytes: "); + pw.println(mTotalNetworkDownloadBytes); + } + if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + pw.print(prefix); pw.print(" Network upload bytes: "); + pw.println(mTotalNetworkUploadBytes); + } + if (job.getMinLatencyMillis() != 0) { + pw.print(prefix); pw.print(" Minimum latency: "); + TimeUtils.formatDuration(job.getMinLatencyMillis(), pw); + pw.println(); + } + if (job.getMaxExecutionDelayMillis() != 0) { + pw.print(prefix); pw.print(" Max execution delay: "); + TimeUtils.formatDuration(job.getMaxExecutionDelayMillis(), pw); + pw.println(); + } + pw.print(prefix); pw.print(" Backoff: policy="); pw.print(job.getBackoffPolicy()); + pw.print(" initial="); TimeUtils.formatDuration(job.getInitialBackoffMillis(), pw); + pw.println(); + if (job.hasEarlyConstraint()) { + pw.print(prefix); pw.println(" Has early constraint"); + } + if (job.hasLateConstraint()) { + pw.print(prefix); pw.println(" Has late constraint"); + } + } + pw.print(prefix); pw.print("Required constraints:"); + dumpConstraints(pw, requiredConstraints); + pw.println(); + pw.print(prefix); + pw.print("Dynamic constraints:"); + dumpConstraints(pw, mDynamicConstraints); + pw.println(); + if (full) { + pw.print(prefix); pw.print("Satisfied constraints:"); + dumpConstraints(pw, satisfiedConstraints); + pw.println(); + pw.print(prefix); pw.print("Unsatisfied constraints:"); + dumpConstraints(pw, + ((requiredConstraints | CONSTRAINT_WITHIN_QUOTA) & ~satisfiedConstraints)); + pw.println(); + if (dozeWhitelisted) { + pw.print(prefix); pw.println("Doze whitelisted: true"); + } + if (uidActive) { + pw.print(prefix); pw.println("Uid: active"); + } + if (job.isExemptedFromAppStandby()) { + pw.print(prefix); pw.println("Is exempted from app standby"); + } + } + if (trackingControllers != 0) { + pw.print(prefix); pw.print("Tracking:"); + if ((trackingControllers&TRACKING_BATTERY) != 0) pw.print(" BATTERY"); + if ((trackingControllers&TRACKING_CONNECTIVITY) != 0) pw.print(" CONNECTIVITY"); + if ((trackingControllers&TRACKING_CONTENT) != 0) pw.print(" CONTENT"); + if ((trackingControllers&TRACKING_IDLE) != 0) pw.print(" IDLE"); + if ((trackingControllers&TRACKING_STORAGE) != 0) pw.print(" STORAGE"); + if ((trackingControllers&TRACKING_TIME) != 0) pw.print(" TIME"); + if ((trackingControllers & TRACKING_QUOTA) != 0) pw.print(" QUOTA"); + pw.println(); + } + + pw.print(prefix); pw.println("Implicit constraints:"); + pw.print(prefix); pw.print(" readyNotDozing: "); + pw.println(mReadyNotDozing); + pw.print(prefix); pw.print(" readyNotRestrictedInBg: "); + pw.println(mReadyNotRestrictedInBg); + if (!job.isPeriodic() && hasDeadlineConstraint()) { + pw.print(prefix); pw.print(" readyDeadlineSatisfied: "); + pw.println(mReadyDeadlineSatisfied); + } + pw.print(prefix); + pw.print(" readyDynamicSatisfied: "); + pw.println(mReadyDynamicSatisfied); + + if (changedAuthorities != null) { + pw.print(prefix); pw.println("Changed authorities:"); + for (int i=0; i<changedAuthorities.size(); i++) { + pw.print(prefix); pw.print(" "); pw.println(changedAuthorities.valueAt(i)); + } + } + if (changedUris != null) { + pw.print(prefix); + pw.println("Changed URIs:"); + for (int i = 0; i < changedUris.size(); i++) { + pw.print(prefix); + pw.print(" "); + pw.println(changedUris.valueAt(i)); + } + } + if (network != null) { + pw.print(prefix); pw.print("Network: "); pw.println(network); + } + if (pendingWork != null && pendingWork.size() > 0) { + pw.print(prefix); pw.println("Pending work:"); + for (int i = 0; i < pendingWork.size(); i++) { + dumpJobWorkItem(pw, prefix, pendingWork.get(i), i); + } + } + if (executingWork != null && executingWork.size() > 0) { + pw.print(prefix); pw.println("Executing work:"); + for (int i = 0; i < executingWork.size(); i++) { + dumpJobWorkItem(pw, prefix, executingWork.get(i), i); + } + } + pw.print(prefix); pw.print("Standby bucket: "); + pw.println(getBucketName()); + if (whenStandbyDeferred != 0) { + pw.print(prefix); pw.print(" Deferred since: "); + TimeUtils.formatDuration(whenStandbyDeferred, elapsedRealtimeMillis, pw); + pw.println(); + } + if (mFirstForceBatchedTimeElapsed != 0) { + pw.print(prefix); + pw.print(" Time since first force batch attempt: "); + TimeUtils.formatDuration(mFirstForceBatchedTimeElapsed, elapsedRealtimeMillis, pw); + pw.println(); + } + pw.print(prefix); pw.print("Enqueue time: "); + TimeUtils.formatDuration(enqueueTime, elapsedRealtimeMillis, pw); + pw.println(); + pw.print(prefix); pw.print("Run time: earliest="); + formatRunTime(pw, earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME, elapsedRealtimeMillis); + pw.print(", latest="); + formatRunTime(pw, latestRunTimeElapsedMillis, NO_LATEST_RUNTIME, elapsedRealtimeMillis); + pw.print(", original latest="); + formatRunTime(pw, mOriginalLatestRunTimeElapsedMillis, + NO_LATEST_RUNTIME, elapsedRealtimeMillis); + pw.println(); + if (numFailures != 0) { + pw.print(prefix); pw.print("Num failures: "); pw.println(numFailures); + } + if (mLastSuccessfulRunTime != 0) { + pw.print(prefix); pw.print("Last successful run: "); + pw.println(formatTime(mLastSuccessfulRunTime)); + } + if (mLastFailedRunTime != 0) { + pw.print(prefix); pw.print("Last failed run: "); + pw.println(formatTime(mLastFailedRunTime)); + } + } + + private static CharSequence formatTime(long time) { + return DateFormat.format("yyyy-MM-dd HH:mm:ss", time); + } + + public void dump(ProtoOutputStream proto, long fieldId, boolean full, long elapsedRealtimeMillis) { + final long token = proto.start(fieldId); + + proto.write(JobStatusDumpProto.CALLING_UID, callingUid); + proto.write(JobStatusDumpProto.TAG, tag); + proto.write(JobStatusDumpProto.SOURCE_UID, getSourceUid()); + proto.write(JobStatusDumpProto.SOURCE_USER_ID, getSourceUserId()); + proto.write(JobStatusDumpProto.SOURCE_PACKAGE_NAME, getSourcePackageName()); + + if (full) { + final long jiToken = proto.start(JobStatusDumpProto.JOB_INFO); + + job.getService().dumpDebug(proto, JobStatusDumpProto.JobInfo.SERVICE); + + proto.write(JobStatusDumpProto.JobInfo.IS_PERIODIC, job.isPeriodic()); + proto.write(JobStatusDumpProto.JobInfo.PERIOD_INTERVAL_MS, job.getIntervalMillis()); + proto.write(JobStatusDumpProto.JobInfo.PERIOD_FLEX_MS, job.getFlexMillis()); + + proto.write(JobStatusDumpProto.JobInfo.IS_PERSISTED, job.isPersisted()); + proto.write(JobStatusDumpProto.JobInfo.PRIORITY, job.getPriority()); + proto.write(JobStatusDumpProto.JobInfo.FLAGS, job.getFlags()); + proto.write(JobStatusDumpProto.INTERNAL_FLAGS, getInternalFlags()); + // Foreground exemption can be determined from internal flags value. + + proto.write(JobStatusDumpProto.JobInfo.REQUIRES_CHARGING, job.isRequireCharging()); + proto.write(JobStatusDumpProto.JobInfo.REQUIRES_BATTERY_NOT_LOW, job.isRequireBatteryNotLow()); + proto.write(JobStatusDumpProto.JobInfo.REQUIRES_DEVICE_IDLE, job.isRequireDeviceIdle()); + + if (job.getTriggerContentUris() != null) { + for (int i = 0; i < job.getTriggerContentUris().length; i++) { + final long tcuToken = proto.start(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_URIS); + JobInfo.TriggerContentUri trig = job.getTriggerContentUris()[i]; + + proto.write(JobStatusDumpProto.JobInfo.TriggerContentUri.FLAGS, trig.getFlags()); + Uri u = trig.getUri(); + if (u != null) { + proto.write(JobStatusDumpProto.JobInfo.TriggerContentUri.URI, u.toString()); + } + + proto.end(tcuToken); + } + if (job.getTriggerContentUpdateDelay() >= 0) { + proto.write(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_UPDATE_DELAY_MS, + job.getTriggerContentUpdateDelay()); + } + if (job.getTriggerContentMaxDelay() >= 0) { + proto.write(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_MAX_DELAY_MS, + job.getTriggerContentMaxDelay()); + } + } + if (job.getExtras() != null && !job.getExtras().isDefinitelyEmpty()) { + job.getExtras().dumpDebug(proto, JobStatusDumpProto.JobInfo.EXTRAS); + } + if (job.getTransientExtras() != null && !job.getTransientExtras().isDefinitelyEmpty()) { + job.getTransientExtras().dumpDebug(proto, JobStatusDumpProto.JobInfo.TRANSIENT_EXTRAS); + } + if (job.getClipData() != null) { + job.getClipData().dumpDebug(proto, JobStatusDumpProto.JobInfo.CLIP_DATA); + } + if (uriPerms != null) { + uriPerms.dump(proto, JobStatusDumpProto.JobInfo.GRANTED_URI_PERMISSIONS); + } + if (job.getRequiredNetwork() != null) { + job.getRequiredNetwork().dumpDebug(proto, JobStatusDumpProto.JobInfo.REQUIRED_NETWORK); + } + if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + proto.write(JobStatusDumpProto.JobInfo.TOTAL_NETWORK_DOWNLOAD_BYTES, + mTotalNetworkDownloadBytes); + } + if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + proto.write(JobStatusDumpProto.JobInfo.TOTAL_NETWORK_UPLOAD_BYTES, + mTotalNetworkUploadBytes); + } + proto.write(JobStatusDumpProto.JobInfo.MIN_LATENCY_MS, job.getMinLatencyMillis()); + proto.write(JobStatusDumpProto.JobInfo.MAX_EXECUTION_DELAY_MS, job.getMaxExecutionDelayMillis()); + + final long bpToken = proto.start(JobStatusDumpProto.JobInfo.BACKOFF_POLICY); + proto.write(JobStatusDumpProto.JobInfo.Backoff.POLICY, job.getBackoffPolicy()); + proto.write(JobStatusDumpProto.JobInfo.Backoff.INITIAL_BACKOFF_MS, + job.getInitialBackoffMillis()); + proto.end(bpToken); + + proto.write(JobStatusDumpProto.JobInfo.HAS_EARLY_CONSTRAINT, job.hasEarlyConstraint()); + proto.write(JobStatusDumpProto.JobInfo.HAS_LATE_CONSTRAINT, job.hasLateConstraint()); + + proto.end(jiToken); + } + + dumpConstraints(proto, JobStatusDumpProto.REQUIRED_CONSTRAINTS, requiredConstraints); + dumpConstraints(proto, JobStatusDumpProto.DYNAMIC_CONSTRAINTS, mDynamicConstraints); + if (full) { + dumpConstraints(proto, JobStatusDumpProto.SATISFIED_CONSTRAINTS, satisfiedConstraints); + dumpConstraints(proto, JobStatusDumpProto.UNSATISFIED_CONSTRAINTS, + ((requiredConstraints | CONSTRAINT_WITHIN_QUOTA) & ~satisfiedConstraints)); + proto.write(JobStatusDumpProto.IS_DOZE_WHITELISTED, dozeWhitelisted); + proto.write(JobStatusDumpProto.IS_UID_ACTIVE, uidActive); + proto.write(JobStatusDumpProto.IS_EXEMPTED_FROM_APP_STANDBY, + job.isExemptedFromAppStandby()); + } + + // Tracking controllers + if ((trackingControllers&TRACKING_BATTERY) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_BATTERY); + } + if ((trackingControllers&TRACKING_CONNECTIVITY) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_CONNECTIVITY); + } + if ((trackingControllers&TRACKING_CONTENT) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_CONTENT); + } + if ((trackingControllers&TRACKING_IDLE) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_IDLE); + } + if ((trackingControllers&TRACKING_STORAGE) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_STORAGE); + } + if ((trackingControllers&TRACKING_TIME) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_TIME); + } + if ((trackingControllers & TRACKING_QUOTA) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_QUOTA); + } + + // Implicit constraints + final long icToken = proto.start(JobStatusDumpProto.IMPLICIT_CONSTRAINTS); + proto.write(JobStatusDumpProto.ImplicitConstraints.IS_NOT_DOZING, mReadyNotDozing); + proto.write(JobStatusDumpProto.ImplicitConstraints.IS_NOT_RESTRICTED_IN_BG, + mReadyNotRestrictedInBg); + // mReadyDeadlineSatisfied isn't an implicit constraint...and can be determined from other + // field values. + proto.write(JobStatusDumpProto.ImplicitConstraints.IS_DYNAMIC_SATISFIED, + mReadyDynamicSatisfied); + proto.end(icToken); + + if (changedAuthorities != null) { + for (int k = 0; k < changedAuthorities.size(); k++) { + proto.write(JobStatusDumpProto.CHANGED_AUTHORITIES, changedAuthorities.valueAt(k)); + } + } + if (changedUris != null) { + for (int i = 0; i < changedUris.size(); i++) { + Uri u = changedUris.valueAt(i); + proto.write(JobStatusDumpProto.CHANGED_URIS, u.toString()); + } + } + + if (network != null) { + network.dumpDebug(proto, JobStatusDumpProto.NETWORK); + } + + if (pendingWork != null) { + for (int i = 0; i < pendingWork.size(); i++) { + dumpJobWorkItem(proto, JobStatusDumpProto.PENDING_WORK, pendingWork.get(i)); + } + } + if (executingWork != null) { + for (int i = 0; i < executingWork.size(); i++) { + dumpJobWorkItem(proto, JobStatusDumpProto.EXECUTING_WORK, executingWork.get(i)); + } + } + + proto.write(JobStatusDumpProto.STANDBY_BUCKET, standbyBucket); + proto.write(JobStatusDumpProto.ENQUEUE_DURATION_MS, elapsedRealtimeMillis - enqueueTime); + proto.write(JobStatusDumpProto.TIME_SINCE_FIRST_DEFERRAL_MS, + whenStandbyDeferred == 0 ? 0 : elapsedRealtimeMillis - whenStandbyDeferred); + proto.write(JobStatusDumpProto.TIME_SINCE_FIRST_FORCE_BATCH_ATTEMPT_MS, + mFirstForceBatchedTimeElapsed == 0 + ? 0 : elapsedRealtimeMillis - mFirstForceBatchedTimeElapsed); + if (earliestRunTimeElapsedMillis == NO_EARLIEST_RUNTIME) { + proto.write(JobStatusDumpProto.TIME_UNTIL_EARLIEST_RUNTIME_MS, 0); + } else { + proto.write(JobStatusDumpProto.TIME_UNTIL_EARLIEST_RUNTIME_MS, + earliestRunTimeElapsedMillis - elapsedRealtimeMillis); + } + if (latestRunTimeElapsedMillis == NO_LATEST_RUNTIME) { + proto.write(JobStatusDumpProto.TIME_UNTIL_LATEST_RUNTIME_MS, 0); + } else { + proto.write(JobStatusDumpProto.TIME_UNTIL_LATEST_RUNTIME_MS, + latestRunTimeElapsedMillis - elapsedRealtimeMillis); + } + proto.write(JobStatusDumpProto.ORIGINAL_LATEST_RUNTIME_ELAPSED, + mOriginalLatestRunTimeElapsedMillis); + + proto.write(JobStatusDumpProto.NUM_FAILURES, numFailures); + proto.write(JobStatusDumpProto.LAST_SUCCESSFUL_RUN_TIME, mLastSuccessfulRunTime); + proto.write(JobStatusDumpProto.LAST_FAILED_RUN_TIME, mLastFailedRunTime); + + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java new file mode 100644 index 000000000000..d108f0b698f7 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -0,0 +1,2911 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.job.controllers; + +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; + +import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; +import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; +import static com.android.server.job.JobSchedulerService.NEVER_INDEX; +import static com.android.server.job.JobSchedulerService.RARE_INDEX; +import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; +import static com.android.server.job.JobSchedulerService.WORKING_INDEX; +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.app.AlarmManager; +import android.app.AppGlobals; +import android.app.IUidObserver; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.BatteryManager; +import android.os.BatteryManagerInternal; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.ArraySet; +import android.util.KeyValueListParser; +import android.util.Log; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseArrayMap; +import android.util.SparseBooleanArray; +import android.util.SparseSetArray; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.LocalServices; +import com.android.server.job.ConstantsProto; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobServiceContext; +import com.android.server.job.StateControllerProto; +import com.android.server.usage.AppStandbyInternal; +import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Controller that tracks whether an app has exceeded its standby bucket quota. + * + * With initial defaults, each app in each bucket is given 10 minutes to run within its respective + * time window. Active jobs can run indefinitely, working set jobs can run for 10 minutes within a + * 2 hour window, frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run + * 10 minutes in a 24 hour window. The windows are rolling, so as soon as a job would have some + * quota based on its bucket, it will be eligible to run. When a job's bucket changes, its new + * quota is immediately applied to it. + * + * Job and session count limits are included to prevent abuse/spam. Each bucket has its own limit on + * the number of jobs or sessions that can run within the window. Regardless of bucket, apps will + * not be allowed to run more than 20 jobs within the past 10 minutes. + * + * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run + * freely when an app enters the foreground state and are restricted when the app leaves the + * foreground state. However, jobs that are started while the app is in the TOP state do not count + * towards any quota and are not restricted regardless of the app's state change. + * + * Jobs will not be throttled when the device is charging. The device is considered to be charging + * once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast. + * + * Note: all limits are enforced per bucket window unless explicitly stated otherwise. + * All stated values are configurable and subject to change. See {@link QcConstants} for current + * defaults. + * + * Test: atest com.android.server.job.controllers.QuotaControllerTest + */ +public final class QuotaController extends StateController { + private static final String TAG = "JobScheduler.Quota"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private static final String ALARM_TAG_CLEANUP = "*job.cleanup*"; + private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*"; + + /** + * Standardize the output of userId-packageName combo. + */ + private static String string(int userId, String packageName) { + return "<" + userId + ">" + packageName; + } + + private static final class Package { + public final String packageName; + public final int userId; + + Package(int userId, String packageName) { + this.userId = userId; + this.packageName = packageName; + } + + @Override + public String toString() { + return string(userId, packageName); + } + + public void dumpDebug(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write(StateControllerProto.QuotaController.Package.USER_ID, userId); + proto.write(StateControllerProto.QuotaController.Package.NAME, packageName); + + proto.end(token); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Package) { + Package other = (Package) obj; + return userId == other.userId && Objects.equals(packageName, other.packageName); + } else { + return false; + } + } + + @Override + public int hashCode() { + return packageName.hashCode() + userId; + } + } + + private static int hashLong(long val) { + return (int) (val ^ (val >>> 32)); + } + + @VisibleForTesting + static class ExecutionStats { + /** + * The time after which this record should be considered invalid (out of date), in the + * elapsed realtime timebase. + */ + public long expirationTimeElapsed; + + public long windowSizeMs; + public int jobCountLimit; + public int sessionCountLimit; + + /** The total amount of time the app ran in its respective bucket window size. */ + public long executionTimeInWindowMs; + public int bgJobCountInWindow; + + /** The total amount of time the app ran in the last {@link #MAX_PERIOD_MS}. */ + public long executionTimeInMaxPeriodMs; + public int bgJobCountInMaxPeriod; + + /** + * The number of {@link TimingSession}s within the bucket window size. This will include + * sessions that started before the window as long as they end within the window. + */ + public int sessionCountInWindow; + + /** + * The time after which the app will be under the bucket quota and can start running jobs + * again. This is only valid if + * {@link #executionTimeInWindowMs} >= {@link #mAllowedTimePerPeriodMs}, + * {@link #executionTimeInMaxPeriodMs} >= {@link #mMaxExecutionTimeMs}, + * {@link #bgJobCountInWindow} >= {@link #jobCountLimit}, or + * {@link #sessionCountInWindow} >= {@link #sessionCountLimit}. + */ + public long inQuotaTimeElapsed; + + /** + * The time after which {@link #jobCountInRateLimitingWindow} should be considered invalid, + * in the elapsed realtime timebase. + */ + public long jobRateLimitExpirationTimeElapsed; + + /** + * The number of jobs that ran in at least the last {@link #mRateLimitingWindowMs}. + * It may contain a few stale entries since cleanup won't happen exactly every + * {@link #mRateLimitingWindowMs}. + */ + public int jobCountInRateLimitingWindow; + + /** + * The time after which {@link #sessionCountInRateLimitingWindow} should be considered + * invalid, in the elapsed realtime timebase. + */ + public long sessionRateLimitExpirationTimeElapsed; + + /** + * The number of {@link TimingSession}s that ran in at least the last + * {@link #mRateLimitingWindowMs}. It may contain a few stale entries since cleanup won't + * happen exactly every {@link #mRateLimitingWindowMs}. This should only be considered + * valid before elapsed realtime has reached {@link #sessionRateLimitExpirationTimeElapsed}. + */ + public int sessionCountInRateLimitingWindow; + + @Override + public String toString() { + return "expirationTime=" + expirationTimeElapsed + ", " + + "windowSizeMs=" + windowSizeMs + ", " + + "jobCountLimit=" + jobCountLimit + ", " + + "sessionCountLimit=" + sessionCountLimit + ", " + + "executionTimeInWindow=" + executionTimeInWindowMs + ", " + + "bgJobCountInWindow=" + bgJobCountInWindow + ", " + + "executionTimeInMaxPeriod=" + executionTimeInMaxPeriodMs + ", " + + "bgJobCountInMaxPeriod=" + bgJobCountInMaxPeriod + ", " + + "sessionCountInWindow=" + sessionCountInWindow + ", " + + "inQuotaTime=" + inQuotaTimeElapsed + ", " + + "rateLimitJobCountExpirationTime=" + jobRateLimitExpirationTimeElapsed + ", " + + "rateLimitJobCountWindow=" + jobCountInRateLimitingWindow + ", " + + "rateLimitSessionCountExpirationTime=" + + sessionRateLimitExpirationTimeElapsed + ", " + + "rateLimitSessionCountWindow=" + sessionCountInRateLimitingWindow; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ExecutionStats) { + ExecutionStats other = (ExecutionStats) obj; + return this.expirationTimeElapsed == other.expirationTimeElapsed + && this.windowSizeMs == other.windowSizeMs + && this.jobCountLimit == other.jobCountLimit + && this.sessionCountLimit == other.sessionCountLimit + && this.executionTimeInWindowMs == other.executionTimeInWindowMs + && this.bgJobCountInWindow == other.bgJobCountInWindow + && this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs + && this.sessionCountInWindow == other.sessionCountInWindow + && this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod + && this.inQuotaTimeElapsed == other.inQuotaTimeElapsed + && this.jobRateLimitExpirationTimeElapsed + == other.jobRateLimitExpirationTimeElapsed + && this.jobCountInRateLimitingWindow == other.jobCountInRateLimitingWindow + && this.sessionRateLimitExpirationTimeElapsed + == other.sessionRateLimitExpirationTimeElapsed + && this.sessionCountInRateLimitingWindow + == other.sessionCountInRateLimitingWindow; + } else { + return false; + } + } + + @Override + public int hashCode() { + int result = 0; + result = 31 * result + hashLong(expirationTimeElapsed); + result = 31 * result + hashLong(windowSizeMs); + result = 31 * result + hashLong(jobCountLimit); + result = 31 * result + hashLong(sessionCountLimit); + result = 31 * result + hashLong(executionTimeInWindowMs); + result = 31 * result + bgJobCountInWindow; + result = 31 * result + hashLong(executionTimeInMaxPeriodMs); + result = 31 * result + bgJobCountInMaxPeriod; + result = 31 * result + sessionCountInWindow; + result = 31 * result + hashLong(inQuotaTimeElapsed); + result = 31 * result + hashLong(jobRateLimitExpirationTimeElapsed); + result = 31 * result + jobCountInRateLimitingWindow; + result = 31 * result + hashLong(sessionRateLimitExpirationTimeElapsed); + result = 31 * result + sessionCountInRateLimitingWindow; + return result; + } + } + + /** List of all tracked jobs keyed by source package-userId combo. */ + private final SparseArrayMap<ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>(); + + /** Timer for each package-userId combo. */ + private final SparseArrayMap<Timer> mPkgTimers = new SparseArrayMap<>(); + + /** List of all timing sessions for a package-userId combo, in chronological order. */ + private final SparseArrayMap<List<TimingSession>> mTimingSessions = new SparseArrayMap<>(); + + /** + * Listener to track and manage when each package comes back within quota. + */ + @GuardedBy("mLock") + private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener(); + + /** Cached calculation results for each app, with the standby buckets as the array indices. */ + private final SparseArrayMap<ExecutionStats[]> mExecutionStatsCache = new SparseArrayMap<>(); + + /** List of UIDs currently in the foreground. */ + private final SparseBooleanArray mForegroundUids = new SparseBooleanArray(); + + /** Cached mapping of UIDs (for all users) to a list of packages in the UID. */ + private final SparseSetArray<String> mUidToPackageCache = new SparseSetArray<>(); + + /** + * List of jobs that started while the UID was in the TOP state. There will be no more than + * 16 ({@link JobSchedulerService#MAX_JOB_CONTEXTS_COUNT}) running at once, so an ArraySet is + * fine. + */ + private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>(); + + private final ActivityManagerInternal mActivityManagerInternal; + private final AlarmManager mAlarmManager; + private final ChargingTracker mChargeTracker; + private final Handler mHandler; + private final QcConstants mQcConstants; + + /** How much time each app will have to run jobs within their standby bucket window. */ + private long mAllowedTimePerPeriodMs = QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; + + /** + * The maximum amount of time an app can have its jobs running within a {@link #MAX_PERIOD_MS} + * window. + */ + private long mMaxExecutionTimeMs = QcConstants.DEFAULT_MAX_EXECUTION_TIME_MS; + + /** + * How much time the app should have before transitioning from out-of-quota to in-quota. + * This should not affect processing if the app is already in-quota. + */ + private long mQuotaBufferMs = QcConstants.DEFAULT_IN_QUOTA_BUFFER_MS; + + /** + * {@link #mAllowedTimePerPeriodMs} - {@link #mQuotaBufferMs}. This can be used to determine + * when an app will have enough quota to transition from out-of-quota to in-quota. + */ + private long mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs; + + /** + * {@link #mMaxExecutionTimeMs} - {@link #mQuotaBufferMs}. This can be used to determine when an + * app will have enough quota to transition from out-of-quota to in-quota. + */ + private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + + /** The period of time used to rate limit recently run jobs. */ + private long mRateLimitingWindowMs = QcConstants.DEFAULT_RATE_LIMITING_WINDOW_MS; + + /** The maximum number of jobs that can run within the past {@link #mRateLimitingWindowMs}. */ + private int mMaxJobCountPerRateLimitingWindow = + QcConstants.DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; + + /** + * The maximum number of {@link TimingSession}s that can run within the past {@link + * #mRateLimitingWindowMs}. + */ + private int mMaxSessionCountPerRateLimitingWindow = + QcConstants.DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; + + private long mNextCleanupTimeElapsed = 0; + private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener = + new AlarmManager.OnAlarmListener() { + @Override + public void onAlarm() { + mHandler.obtainMessage(MSG_CLEAN_UP_SESSIONS).sendToTarget(); + } + }; + + private final IUidObserver mUidObserver = new IUidObserver.Stub() { + @Override + public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) { + mHandler.obtainMessage(MSG_UID_PROCESS_STATE_CHANGED, uid, procState).sendToTarget(); + } + + @Override + public void onUidGone(int uid, boolean disabled) { + } + + @Override + public void onUidActive(int uid) { + } + + @Override + public void onUidIdle(int uid, boolean disabled) { + } + + @Override + public void onUidCachedChanged(int uid, boolean cached) { + } + }; + + private final BroadcastReceiver mPackageAddedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null) { + return; + } + if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + return; + } + final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); + synchronized (mLock) { + mUidToPackageCache.remove(uid); + } + } + }; + + /** + * The rolling window size for each standby bucket. Within each window, an app will have 10 + * minutes to run its jobs. + */ + private final long[] mBucketPeriodsMs = new long[]{ + QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS, + QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS, + QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS, + QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS, + 0, // NEVER + QcConstants.DEFAULT_WINDOW_SIZE_RESTRICTED_MS + }; + + /** The maximum period any bucket can have. */ + private static final long MAX_PERIOD_MS = 24 * 60 * MINUTE_IN_MILLIS; + + /** + * The maximum number of jobs based on its standby bucket. For each max value count in the + * array, the app will not be allowed to run more than that many number of jobs within the + * latest time interval of its rolling window size. + * + * @see #mBucketPeriodsMs + */ + private final int[] mMaxBucketJobCounts = new int[]{ + QcConstants.DEFAULT_MAX_JOB_COUNT_ACTIVE, + QcConstants.DEFAULT_MAX_JOB_COUNT_WORKING, + QcConstants.DEFAULT_MAX_JOB_COUNT_FREQUENT, + QcConstants.DEFAULT_MAX_JOB_COUNT_RARE, + 0, // NEVER + QcConstants.DEFAULT_MAX_JOB_COUNT_RESTRICTED + }; + + /** + * The maximum number of {@link TimingSession}s based on its standby bucket. For each max value + * count in the array, the app will not be allowed to have more than that many number of + * {@link TimingSession}s within the latest time interval of its rolling window size. + * + * @see #mBucketPeriodsMs + */ + private final int[] mMaxBucketSessionCounts = new int[]{ + QcConstants.DEFAULT_MAX_SESSION_COUNT_ACTIVE, + QcConstants.DEFAULT_MAX_SESSION_COUNT_WORKING, + QcConstants.DEFAULT_MAX_SESSION_COUNT_FREQUENT, + QcConstants.DEFAULT_MAX_SESSION_COUNT_RARE, + 0, // NEVER + QcConstants.DEFAULT_MAX_SESSION_COUNT_RESTRICTED, + }; + + /** + * Treat two distinct {@link TimingSession}s as the same if they start and end within this + * amount of time of each other. + */ + private long mTimingSessionCoalescingDurationMs = + QcConstants.DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS; + + /** An app has reached its quota. The message should contain a {@link Package} object. */ + private static final int MSG_REACHED_QUOTA = 0; + /** Drop any old timing sessions. */ + private static final int MSG_CLEAN_UP_SESSIONS = 1; + /** Check if a package is now within its quota. */ + private static final int MSG_CHECK_PACKAGE = 2; + /** Process state for a UID has changed. */ + private static final int MSG_UID_PROCESS_STATE_CHANGED = 3; + + public QuotaController(JobSchedulerService service) { + super(service); + mHandler = new QcHandler(mContext.getMainLooper()); + mChargeTracker = new ChargingTracker(); + mChargeTracker.startTracking(); + mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + mQcConstants = new QcConstants(mHandler); + + final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + mContext.registerReceiverAsUser(mPackageAddedReceiver, UserHandle.ALL, filter, null, null); + + // Set up the app standby bucketing tracker + AppStandbyInternal appStandby = LocalServices.getService(AppStandbyInternal.class); + appStandby.addListener(new StandbyTracker()); + + try { + ActivityManager.getService().registerUidObserver(mUidObserver, + ActivityManager.UID_OBSERVER_PROCSTATE, + ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null); + } catch (RemoteException e) { + // ignored; both services live in system_server + } + } + + @Override + public void onSystemServicesReady() { + mQcConstants.start(mContext.getContentResolver()); + } + + @Override + public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { + final int userId = jobStatus.getSourceUserId(); + final String pkgName = jobStatus.getSourcePackageName(); + ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); + if (jobs == null) { + jobs = new ArraySet<>(); + mTrackedJobs.add(userId, pkgName, jobs); + } + jobs.add(jobStatus); + jobStatus.setTrackingController(JobStatus.TRACKING_QUOTA); + final boolean isWithinQuota = isWithinQuotaLocked(jobStatus); + setConstraintSatisfied(jobStatus, isWithinQuota); + if (!isWithinQuota) { + maybeScheduleStartAlarmLocked(userId, pkgName, jobStatus.getEffectiveStandbyBucket()); + } + } + + @Override + public void prepareForExecutionLocked(JobStatus jobStatus) { + if (DEBUG) { + Slog.d(TAG, "Prepping for " + jobStatus.toShortString()); + } + + final int uid = jobStatus.getSourceUid(); + if (mActivityManagerInternal.getUidProcessState(uid) <= ActivityManager.PROCESS_STATE_TOP) { + if (DEBUG) { + Slog.d(TAG, jobStatus.toShortString() + " is top started job"); + } + mTopStartedJobs.add(jobStatus); + // Top jobs won't count towards quota so there's no need to involve the Timer. + return; + } + + final int userId = jobStatus.getSourceUserId(); + final String packageName = jobStatus.getSourcePackageName(); + Timer timer = mPkgTimers.get(userId, packageName); + if (timer == null) { + timer = new Timer(uid, userId, packageName); + mPkgTimers.add(userId, packageName, timer); + } + timer.startTrackingJobLocked(jobStatus); + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate) { + if (jobStatus.clearTrackingController(JobStatus.TRACKING_QUOTA)) { + Timer timer = mPkgTimers.get(jobStatus.getSourceUserId(), + jobStatus.getSourcePackageName()); + if (timer != null) { + timer.stopTrackingJob(jobStatus); + } + ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUserId(), + jobStatus.getSourcePackageName()); + if (jobs != null) { + jobs.remove(jobStatus); + } + mTopStartedJobs.remove(jobStatus); + } + } + + @Override + public void onAppRemovedLocked(String packageName, int uid) { + if (packageName == null) { + Slog.wtf(TAG, "Told app removed but given null package name."); + return; + } + clearAppStats(UserHandle.getUserId(uid), packageName); + mForegroundUids.delete(uid); + mUidToPackageCache.remove(uid); + } + + @Override + public void onUserRemovedLocked(int userId) { + mTrackedJobs.delete(userId); + mPkgTimers.delete(userId); + mTimingSessions.delete(userId); + mInQuotaAlarmListener.removeAlarmsLocked(userId); + mExecutionStatsCache.delete(userId); + mUidToPackageCache.clear(); + } + + /** Drop all historical stats and stop tracking any active sessions for the specified app. */ + public void clearAppStats(int userId, @NonNull String packageName) { + mTrackedJobs.delete(userId, packageName); + Timer timer = mPkgTimers.get(userId, packageName); + if (timer != null) { + if (timer.isActive()) { + Slog.e(TAG, "clearAppStats called before Timer turned off."); + timer.dropEverythingLocked(); + } + mPkgTimers.delete(userId, packageName); + } + mTimingSessions.delete(userId, packageName); + mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); + mExecutionStatsCache.delete(userId, packageName); + } + + private boolean isUidInForeground(int uid) { + if (UserHandle.isCore(uid)) { + return true; + } + synchronized (mLock) { + return mForegroundUids.get(uid); + } + } + + /** @return true if the job was started while the app was in the TOP state. */ + private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) { + return mTopStartedJobs.contains(jobStatus); + } + + /** Returns the maximum amount of time this job could run for. */ + public long getMaxJobExecutionTimeMsLocked(@NonNull final JobStatus jobStatus) { + // If quota is currently "free", then the job can run for the full amount of time. + if (mChargeTracker.isCharging() + || isTopStartedJobLocked(jobStatus) + || isUidInForeground(jobStatus.getSourceUid())) { + return JobServiceContext.EXECUTING_TIMESLICE_MILLIS; + } + return getRemainingExecutionTimeLocked(jobStatus); + } + + @VisibleForTesting + boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) { + final int standbyBucket = jobStatus.getEffectiveStandbyBucket(); + // A job is within quota if one of the following is true: + // 1. it was started while the app was in the TOP state + // 2. the app is currently in the foreground + // 3. the app overall is within its quota + return isTopStartedJobLocked(jobStatus) + || isUidInForeground(jobStatus.getSourceUid()) + || isWithinQuotaLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket); + } + + @VisibleForTesting + boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName, + final int standbyBucket) { + if (standbyBucket == NEVER_INDEX) return false; + + // Quota constraint is not enforced while charging. + if (mChargeTracker.isCharging()) { + // Restricted jobs require additional constraints when charging, so don't immediately + // mark quota as free when charging. + if (standbyBucket != RESTRICTED_INDEX) { + return true; + } + } + + ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); + return getRemainingExecutionTimeLocked(stats) > 0 + && isUnderJobCountQuotaLocked(stats, standbyBucket) + && isUnderSessionCountQuotaLocked(stats, standbyBucket); + } + + private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats, + final int standbyBucket) { + final long now = sElapsedRealtimeClock.millis(); + final boolean isUnderAllowedTimeQuota = + (stats.jobRateLimitExpirationTimeElapsed <= now + || stats.jobCountInRateLimitingWindow < mMaxJobCountPerRateLimitingWindow); + return isUnderAllowedTimeQuota + && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]); + } + + private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats, + final int standbyBucket) { + final long now = sElapsedRealtimeClock.millis(); + final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now + || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow); + return isUnderAllowedTimeQuota + && stats.sessionCountInWindow < mMaxBucketSessionCounts[standbyBucket]; + } + + @VisibleForTesting + long getRemainingExecutionTimeLocked(@NonNull final JobStatus jobStatus) { + return getRemainingExecutionTimeLocked(jobStatus.getSourceUserId(), + jobStatus.getSourcePackageName(), + jobStatus.getEffectiveStandbyBucket()); + } + + @VisibleForTesting + long getRemainingExecutionTimeLocked(final int userId, @NonNull final String packageName) { + final int standbyBucket = JobSchedulerService.standbyBucketForPackage(packageName, + userId, sElapsedRealtimeClock.millis()); + return getRemainingExecutionTimeLocked(userId, packageName, standbyBucket); + } + + /** + * Returns the amount of time, in milliseconds, that this job has remaining to run based on its + * current standby bucket. Time remaining could be negative if the app was moved from a less + * restricted to a more restricted bucket. + */ + private long getRemainingExecutionTimeLocked(final int userId, + @NonNull final String packageName, final int standbyBucket) { + if (standbyBucket == NEVER_INDEX) { + return 0; + } + return getRemainingExecutionTimeLocked( + getExecutionStatsLocked(userId, packageName, standbyBucket)); + } + + private long getRemainingExecutionTimeLocked(@NonNull ExecutionStats stats) { + return Math.min(mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs, + mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs); + } + + /** + * Returns the amount of time, in milliseconds, until the package would have reached its + * duration quota, assuming it has a job counting towards its quota the entire time. This takes + * into account any {@link TimingSession}s that may roll out of the window as the job is + * running. + */ + @VisibleForTesting + long getTimeUntilQuotaConsumedLocked(final int userId, @NonNull final String packageName) { + final long nowElapsed = sElapsedRealtimeClock.millis(); + final int standbyBucket = JobSchedulerService.standbyBucketForPackage( + packageName, userId, nowElapsed); + if (standbyBucket == NEVER_INDEX) { + return 0; + } + List<TimingSession> sessions = mTimingSessions.get(userId, packageName); + if (sessions == null || sessions.size() == 0) { + return mAllowedTimePerPeriodMs; + } + + final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); + final long startWindowElapsed = nowElapsed - stats.windowSizeMs; + final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS; + final long allowedTimeRemainingMs = mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs; + final long maxExecutionTimeRemainingMs = + mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs; + + // Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can + // essentially run until they reach the maximum limit. + if (stats.windowSizeMs == mAllowedTimePerPeriodMs) { + return calculateTimeUntilQuotaConsumedLocked( + sessions, startMaxElapsed, maxExecutionTimeRemainingMs); + } + + // Need to check both max time and period time in case one is less than the other. + // For example, max time remaining could be less than bucket time remaining, but sessions + // contributing to the max time remaining could phase out enough that we'd want to use the + // bucket value. + return Math.min( + calculateTimeUntilQuotaConsumedLocked( + sessions, startMaxElapsed, maxExecutionTimeRemainingMs), + calculateTimeUntilQuotaConsumedLocked( + sessions, startWindowElapsed, allowedTimeRemainingMs)); + } + + /** + * Calculates how much time it will take, in milliseconds, until the quota is fully consumed. + * + * @param windowStartElapsed The start of the window, in the elapsed realtime timebase. + * @param deadSpaceMs How much time can be allowed to count towards the quota + */ + private long calculateTimeUntilQuotaConsumedLocked(@NonNull List<TimingSession> sessions, + final long windowStartElapsed, long deadSpaceMs) { + long timeUntilQuotaConsumedMs = 0; + long start = windowStartElapsed; + for (int i = 0; i < sessions.size(); ++i) { + TimingSession session = sessions.get(i); + + if (session.endTimeElapsed < windowStartElapsed) { + // Outside of window. Ignore. + continue; + } else if (session.startTimeElapsed <= windowStartElapsed) { + // Overlapping session. Can extend time by portion of session in window. + timeUntilQuotaConsumedMs += session.endTimeElapsed - windowStartElapsed; + start = session.endTimeElapsed; + } else { + // Completely within the window. Can only consider if there's enough dead space + // to get to the start of the session. + long diff = session.startTimeElapsed - start; + if (diff > deadSpaceMs) { + break; + } + timeUntilQuotaConsumedMs += diff + + (session.endTimeElapsed - session.startTimeElapsed); + deadSpaceMs -= diff; + start = session.endTimeElapsed; + } + } + // Will be non-zero if the loop didn't look at any sessions. + timeUntilQuotaConsumedMs += deadSpaceMs; + if (timeUntilQuotaConsumedMs > mMaxExecutionTimeMs) { + Slog.wtf(TAG, "Calculated quota consumed time too high: " + timeUntilQuotaConsumedMs); + } + return timeUntilQuotaConsumedMs; + } + + /** Returns the execution stats of the app in the most recent window. */ + @VisibleForTesting + @NonNull + ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName, + final int standbyBucket) { + return getExecutionStatsLocked(userId, packageName, standbyBucket, true); + } + + @NonNull + private ExecutionStats getExecutionStatsLocked(final int userId, + @NonNull final String packageName, final int standbyBucket, + final boolean refreshStatsIfOld) { + if (standbyBucket == NEVER_INDEX) { + Slog.wtf(TAG, "getExecutionStatsLocked called for a NEVER app."); + return new ExecutionStats(); + } + ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName); + if (appStats == null) { + appStats = new ExecutionStats[mBucketPeriodsMs.length]; + mExecutionStatsCache.add(userId, packageName, appStats); + } + ExecutionStats stats = appStats[standbyBucket]; + if (stats == null) { + stats = new ExecutionStats(); + appStats[standbyBucket] = stats; + } + if (refreshStatsIfOld) { + final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket]; + final int jobCountLimit = mMaxBucketJobCounts[standbyBucket]; + final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket]; + Timer timer = mPkgTimers.get(userId, packageName); + if ((timer != null && timer.isActive()) + || stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis() + || stats.windowSizeMs != bucketWindowSizeMs + || stats.jobCountLimit != jobCountLimit + || stats.sessionCountLimit != sessionCountLimit) { + // The stats are no longer valid. + stats.windowSizeMs = bucketWindowSizeMs; + stats.jobCountLimit = jobCountLimit; + stats.sessionCountLimit = sessionCountLimit; + updateExecutionStatsLocked(userId, packageName, stats); + } + } + + return stats; + } + + @VisibleForTesting + void updateExecutionStatsLocked(final int userId, @NonNull final String packageName, + @NonNull ExecutionStats stats) { + stats.executionTimeInWindowMs = 0; + stats.bgJobCountInWindow = 0; + stats.executionTimeInMaxPeriodMs = 0; + stats.bgJobCountInMaxPeriod = 0; + stats.sessionCountInWindow = 0; + if (stats.jobCountLimit == 0 || stats.sessionCountLimit == 0) { + // App won't be in quota until configuration changes. + stats.inQuotaTimeElapsed = Long.MAX_VALUE; + } else { + stats.inQuotaTimeElapsed = 0; + } + + Timer timer = mPkgTimers.get(userId, packageName); + final long nowElapsed = sElapsedRealtimeClock.millis(); + stats.expirationTimeElapsed = nowElapsed + MAX_PERIOD_MS; + if (timer != null && timer.isActive()) { + // Exclude active sessions from the session count so that new jobs aren't prevented + // from starting due to an app hitting the session limit. + stats.executionTimeInWindowMs = + stats.executionTimeInMaxPeriodMs = timer.getCurrentDuration(nowElapsed); + stats.bgJobCountInWindow = stats.bgJobCountInMaxPeriod = timer.getBgJobCount(); + // If the timer is active, the value will be stale at the next method call, so + // invalidate now. + stats.expirationTimeElapsed = nowElapsed; + if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + nowElapsed - mAllowedTimeIntoQuotaMs + stats.windowSizeMs); + } + if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + nowElapsed - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); + } + if (stats.bgJobCountInWindow >= stats.jobCountLimit) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + nowElapsed + stats.windowSizeMs); + } + } + + List<TimingSession> sessions = mTimingSessions.get(userId, packageName); + if (sessions == null || sessions.size() == 0) { + return; + } + + final long startWindowElapsed = nowElapsed - stats.windowSizeMs; + final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS; + int sessionCountInWindow = 0; + // The minimum time between the start time and the beginning of the sessions that were + // looked at --> how much time the stats will be valid for. + long emptyTimeMs = Long.MAX_VALUE; + // Sessions are non-overlapping and in order of occurrence, so iterating backwards will get + // the most recent ones. + final int loopStart = sessions.size() - 1; + for (int i = loopStart; i >= 0; --i) { + TimingSession session = sessions.get(i); + + // Window management. + if (startWindowElapsed < session.endTimeElapsed) { + final long start; + if (startWindowElapsed < session.startTimeElapsed) { + start = session.startTimeElapsed; + emptyTimeMs = + Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed); + } else { + // The session started before the window but ended within the window. Only + // include the portion that was within the window. + start = startWindowElapsed; + emptyTimeMs = 0; + } + + stats.executionTimeInWindowMs += session.endTimeElapsed - start; + stats.bgJobCountInWindow += session.bgJobCount; + if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + start + stats.executionTimeInWindowMs - mAllowedTimeIntoQuotaMs + + stats.windowSizeMs); + } + if (stats.bgJobCountInWindow >= stats.jobCountLimit) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + session.endTimeElapsed + stats.windowSizeMs); + } + if (i == loopStart + || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed) + > mTimingSessionCoalescingDurationMs) { + // Coalesce sessions if they are very close to each other in time + sessionCountInWindow++; + + if (sessionCountInWindow >= stats.sessionCountLimit) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + session.endTimeElapsed + stats.windowSizeMs); + } + } + } + + // Max period check. + if (startMaxElapsed < session.startTimeElapsed) { + stats.executionTimeInMaxPeriodMs += + session.endTimeElapsed - session.startTimeElapsed; + stats.bgJobCountInMaxPeriod += session.bgJobCount; + emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed); + if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + session.startTimeElapsed + stats.executionTimeInMaxPeriodMs + - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); + } + } else if (startMaxElapsed < session.endTimeElapsed) { + // The session started before the window but ended within the window. Only include + // the portion that was within the window. + stats.executionTimeInMaxPeriodMs += session.endTimeElapsed - startMaxElapsed; + stats.bgJobCountInMaxPeriod += session.bgJobCount; + emptyTimeMs = 0; + if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + startMaxElapsed + stats.executionTimeInMaxPeriodMs + - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); + } + } else { + // This session ended before the window. No point in going any further. + break; + } + } + stats.expirationTimeElapsed = nowElapsed + emptyTimeMs; + stats.sessionCountInWindow = sessionCountInWindow; + } + + /** Invalidate ExecutionStats for all apps. */ + @VisibleForTesting + void invalidateAllExecutionStatsLocked() { + final long nowElapsed = sElapsedRealtimeClock.millis(); + mExecutionStatsCache.forEach((appStats) -> { + if (appStats != null) { + for (int i = 0; i < appStats.length; ++i) { + ExecutionStats stats = appStats[i]; + if (stats != null) { + stats.expirationTimeElapsed = nowElapsed; + } + } + } + }); + } + + @VisibleForTesting + void invalidateAllExecutionStatsLocked(final int userId, + @NonNull final String packageName) { + ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName); + if (appStats != null) { + final long nowElapsed = sElapsedRealtimeClock.millis(); + for (int i = 0; i < appStats.length; ++i) { + ExecutionStats stats = appStats[i]; + if (stats != null) { + stats.expirationTimeElapsed = nowElapsed; + } + } + } + } + + @VisibleForTesting + void incrementJobCount(final int userId, @NonNull final String packageName, int count) { + final long now = sElapsedRealtimeClock.millis(); + ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName); + if (appStats == null) { + appStats = new ExecutionStats[mBucketPeriodsMs.length]; + mExecutionStatsCache.add(userId, packageName, appStats); + } + for (int i = 0; i < appStats.length; ++i) { + ExecutionStats stats = appStats[i]; + if (stats == null) { + stats = new ExecutionStats(); + appStats[i] = stats; + } + if (stats.jobRateLimitExpirationTimeElapsed <= now) { + stats.jobRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs; + stats.jobCountInRateLimitingWindow = 0; + } + stats.jobCountInRateLimitingWindow += count; + } + } + + private void incrementTimingSessionCount(final int userId, @NonNull final String packageName) { + final long now = sElapsedRealtimeClock.millis(); + ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName); + if (appStats == null) { + appStats = new ExecutionStats[mBucketPeriodsMs.length]; + mExecutionStatsCache.add(userId, packageName, appStats); + } + for (int i = 0; i < appStats.length; ++i) { + ExecutionStats stats = appStats[i]; + if (stats == null) { + stats = new ExecutionStats(); + appStats[i] = stats; + } + if (stats.sessionRateLimitExpirationTimeElapsed <= now) { + stats.sessionRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs; + stats.sessionCountInRateLimitingWindow = 0; + } + stats.sessionCountInRateLimitingWindow++; + } + } + + @VisibleForTesting + void saveTimingSession(final int userId, @NonNull final String packageName, + @NonNull final TimingSession session) { + synchronized (mLock) { + List<TimingSession> sessions = mTimingSessions.get(userId, packageName); + if (sessions == null) { + sessions = new ArrayList<>(); + mTimingSessions.add(userId, packageName, sessions); + } + sessions.add(session); + // Adding a new session means that the current stats are now incorrect. + invalidateAllExecutionStatsLocked(userId, packageName); + + maybeScheduleCleanupAlarmLocked(); + } + } + + private final class EarliestEndTimeFunctor implements Consumer<List<TimingSession>> { + public long earliestEndElapsed = Long.MAX_VALUE; + + @Override + public void accept(List<TimingSession> sessions) { + if (sessions != null && sessions.size() > 0) { + earliestEndElapsed = Math.min(earliestEndElapsed, sessions.get(0).endTimeElapsed); + } + } + + void reset() { + earliestEndElapsed = Long.MAX_VALUE; + } + } + + private final EarliestEndTimeFunctor mEarliestEndTimeFunctor = new EarliestEndTimeFunctor(); + + /** Schedule a cleanup alarm if necessary and there isn't already one scheduled. */ + @VisibleForTesting + void maybeScheduleCleanupAlarmLocked() { + if (mNextCleanupTimeElapsed > sElapsedRealtimeClock.millis()) { + // There's already an alarm scheduled. Just stick with that one. There's no way we'll + // end up scheduling an earlier alarm. + if (DEBUG) { + Slog.v(TAG, "Not scheduling cleanup since there's already one at " + + mNextCleanupTimeElapsed + " (in " + (mNextCleanupTimeElapsed + - sElapsedRealtimeClock.millis()) + "ms)"); + } + return; + } + mEarliestEndTimeFunctor.reset(); + mTimingSessions.forEach(mEarliestEndTimeFunctor); + final long earliestEndElapsed = mEarliestEndTimeFunctor.earliestEndElapsed; + if (earliestEndElapsed == Long.MAX_VALUE) { + // Couldn't find a good time to clean up. Maybe this was called after we deleted all + // timing sessions. + if (DEBUG) { + Slog.d(TAG, "Didn't find a time to schedule cleanup"); + } + return; + } + // Need to keep sessions for all apps up to the max period, regardless of their current + // standby bucket. + long nextCleanupElapsed = earliestEndElapsed + MAX_PERIOD_MS; + if (nextCleanupElapsed - mNextCleanupTimeElapsed <= 10 * MINUTE_IN_MILLIS) { + // No need to clean up too often. Delay the alarm if the next cleanup would be too soon + // after it. + nextCleanupElapsed += 10 * MINUTE_IN_MILLIS; + } + mNextCleanupTimeElapsed = nextCleanupElapsed; + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextCleanupElapsed, ALARM_TAG_CLEANUP, + mSessionCleanupAlarmListener, mHandler); + if (DEBUG) { + Slog.d(TAG, "Scheduled next cleanup for " + mNextCleanupTimeElapsed); + } + } + + private class TimerChargingUpdateFunctor implements Consumer<Timer> { + private long mNowElapsed; + private boolean mIsCharging; + + private void setStatus(long nowElapsed, boolean isCharging) { + mNowElapsed = nowElapsed; + mIsCharging = isCharging; + } + + @Override + public void accept(Timer timer) { + if (JobSchedulerService.standbyBucketForPackage(timer.mPkg.packageName, + timer.mPkg.userId, mNowElapsed) != RESTRICTED_INDEX) { + // Restricted jobs need additional constraints even when charging, so don't + // immediately say that quota is free. + timer.onStateChangedLocked(mNowElapsed, mIsCharging); + } + } + } + + private final TimerChargingUpdateFunctor + mTimerChargingUpdateFunctor = new TimerChargingUpdateFunctor(); + + private void handleNewChargingStateLocked() { + mTimerChargingUpdateFunctor.setStatus(sElapsedRealtimeClock.millis(), + mChargeTracker.isCharging()); + if (DEBUG) { + Slog.d(TAG, "handleNewChargingStateLocked: " + mChargeTracker.isCharging()); + } + // Deal with Timers first. + mPkgTimers.forEach(mTimerChargingUpdateFunctor); + // Now update jobs. + maybeUpdateAllConstraintsLocked(); + } + + private void maybeUpdateAllConstraintsLocked() { + boolean changed = false; + for (int u = 0; u < mTrackedJobs.numMaps(); ++u) { + final int userId = mTrackedJobs.keyAt(u); + for (int p = 0; p < mTrackedJobs.numElementsForKey(userId); ++p) { + final String packageName = mTrackedJobs.keyAt(u, p); + changed |= maybeUpdateConstraintForPkgLocked(userId, packageName); + } + } + if (changed) { + mStateChangedListener.onControllerStateChanged(); + } + } + + /** + * Update the CONSTRAINT_WITHIN_QUOTA bit for all of the Jobs for a given package. + * + * @return true if at least one job had its bit changed + */ + private boolean maybeUpdateConstraintForPkgLocked(final int userId, + @NonNull final String packageName) { + ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName); + if (jobs == null || jobs.size() == 0) { + return false; + } + + // Quota is the same for all jobs within a package. + final int realStandbyBucket = jobs.valueAt(0).getStandbyBucket(); + final boolean realInQuota = isWithinQuotaLocked(userId, packageName, realStandbyBucket); + boolean changed = false; + for (int i = jobs.size() - 1; i >= 0; --i) { + final JobStatus js = jobs.valueAt(i); + if (isTopStartedJobLocked(js)) { + // Job was started while the app was in the TOP state so we should allow it to + // finish. + changed |= js.setQuotaConstraintSatisfied(true); + } else if (realStandbyBucket != ACTIVE_INDEX + && realStandbyBucket == js.getEffectiveStandbyBucket()) { + // An app in the ACTIVE bucket may be out of quota while the job could be in quota + // for some reason. Therefore, avoid setting the real value here and check each job + // individually. + changed |= setConstraintSatisfied(js, realInQuota); + } else { + // This job is somehow exempted. Need to determine its own quota status. + changed |= setConstraintSatisfied(js, isWithinQuotaLocked(js)); + } + } + if (!realInQuota) { + // Don't want to use the effective standby bucket here since that bump the bucket to + // ACTIVE for one of the jobs, which doesn't help with other jobs that aren't + // exempted. + maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket); + } else { + mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); + } + return changed; + } + + private class UidConstraintUpdater implements Consumer<JobStatus> { + private final SparseArrayMap<Integer> mToScheduleStartAlarms = new SparseArrayMap<>(); + public boolean wasJobChanged; + + @Override + public void accept(JobStatus jobStatus) { + wasJobChanged |= setConstraintSatisfied(jobStatus, isWithinQuotaLocked(jobStatus)); + final int userId = jobStatus.getSourceUserId(); + final String packageName = jobStatus.getSourcePackageName(); + final int realStandbyBucket = jobStatus.getStandbyBucket(); + if (isWithinQuotaLocked(userId, packageName, realStandbyBucket)) { + mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); + } else { + mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket); + } + } + + void postProcess() { + for (int u = 0; u < mToScheduleStartAlarms.numMaps(); ++u) { + final int userId = mToScheduleStartAlarms.keyAt(u); + for (int p = 0; p < mToScheduleStartAlarms.numElementsForKey(userId); ++p) { + final String packageName = mToScheduleStartAlarms.keyAt(u, p); + final int standbyBucket = mToScheduleStartAlarms.get(userId, packageName); + maybeScheduleStartAlarmLocked(userId, packageName, standbyBucket); + } + } + } + + void reset() { + wasJobChanged = false; + mToScheduleStartAlarms.clear(); + } + } + + private final UidConstraintUpdater mUpdateUidConstraints = new UidConstraintUpdater(); + + private boolean maybeUpdateConstraintForUidLocked(final int uid) { + mService.getJobStore().forEachJobForSourceUid(uid, mUpdateUidConstraints); + + mUpdateUidConstraints.postProcess(); + boolean changed = mUpdateUidConstraints.wasJobChanged; + mUpdateUidConstraints.reset(); + return changed; + } + + /** + * Maybe schedule a non-wakeup alarm for the next time this package will have quota to run + * again. This should only be called if the package is already out of quota. + */ + @VisibleForTesting + void maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName, + final int standbyBucket) { + if (standbyBucket == NEVER_INDEX) { + return; + } + + final String pkgString = string(userId, packageName); + ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); + final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket); + final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats, + standbyBucket); + + if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs + && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs + && isUnderJobCountQuota + && isUnderTimingSessionCountQuota) { + // Already in quota. Why was this method called? + if (DEBUG) { + Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString + + " even though it already has " + + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket) + + "ms in its quota."); + } + mInQuotaAlarmListener.removeAlarmLocked(userId, packageName); + mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget(); + return; + } + + // The time this app will have quota again. + long inQuotaTimeElapsed = stats.inQuotaTimeElapsed; + if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) { + // App hit the rate limit. + inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, + stats.jobRateLimitExpirationTimeElapsed); + } + if (!isUnderTimingSessionCountQuota + && stats.sessionCountInWindow < stats.sessionCountLimit) { + // App hit the rate limit. + inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, + stats.sessionRateLimitExpirationTimeElapsed); + } + if (inQuotaTimeElapsed <= sElapsedRealtimeClock.millis()) { + final long nowElapsed = sElapsedRealtimeClock.millis(); + Slog.wtf(TAG, + "In quota time is " + (nowElapsed - inQuotaTimeElapsed) + "ms old. Now=" + + nowElapsed + ", inQuotaTime=" + inQuotaTimeElapsed + ": " + stats); + inQuotaTimeElapsed = nowElapsed + 5 * MINUTE_IN_MILLIS; + } + mInQuotaAlarmListener.addAlarmLocked(userId, packageName, inQuotaTimeElapsed); + } + + private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, boolean isWithinQuota) { + if (!isWithinQuota && jobStatus.getWhenStandbyDeferred() == 0) { + // Mark that the job is being deferred due to buckets. + jobStatus.setWhenStandbyDeferred(sElapsedRealtimeClock.millis()); + } + return jobStatus.setQuotaConstraintSatisfied(isWithinQuota); + } + + private final class ChargingTracker extends BroadcastReceiver { + /** + * Track whether we're charging. This has a slightly different definition than that of + * BatteryController. + */ + private boolean mCharging; + + ChargingTracker() { + } + + public void startTracking() { + IntentFilter filter = new IntentFilter(); + + // Charging/not charging. + filter.addAction(BatteryManager.ACTION_CHARGING); + filter.addAction(BatteryManager.ACTION_DISCHARGING); + mContext.registerReceiver(this, filter); + + // Initialise tracker state. + BatteryManagerInternal batteryManagerInternal = + LocalServices.getService(BatteryManagerInternal.class); + mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY); + } + + public boolean isCharging() { + return mCharging; + } + + @Override + public void onReceive(Context context, Intent intent) { + synchronized (mLock) { + final String action = intent.getAction(); + if (BatteryManager.ACTION_CHARGING.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Received charging intent, fired @ " + + sElapsedRealtimeClock.millis()); + } + mCharging = true; + handleNewChargingStateLocked(); + } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Disconnected from power."); + } + mCharging = false; + handleNewChargingStateLocked(); + } + } + } + } + + @VisibleForTesting + static final class TimingSession { + // Start timestamp in elapsed realtime timebase. + public final long startTimeElapsed; + // End timestamp in elapsed realtime timebase. + public final long endTimeElapsed; + // How many background jobs ran during this session. + public final int bgJobCount; + + private final int mHashCode; + + TimingSession(long startElapsed, long endElapsed, int bgJobCount) { + this.startTimeElapsed = startElapsed; + this.endTimeElapsed = endElapsed; + this.bgJobCount = bgJobCount; + + int hashCode = 0; + hashCode = 31 * hashCode + hashLong(startTimeElapsed); + hashCode = 31 * hashCode + hashLong(endTimeElapsed); + hashCode = 31 * hashCode + bgJobCount; + mHashCode = hashCode; + } + + @Override + public String toString() { + return "TimingSession{" + startTimeElapsed + "->" + endTimeElapsed + ", " + bgJobCount + + "}"; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TimingSession) { + TimingSession other = (TimingSession) obj; + return startTimeElapsed == other.startTimeElapsed + && endTimeElapsed == other.endTimeElapsed + && bgJobCount == other.bgJobCount; + } else { + return false; + } + } + + @Override + public int hashCode() { + return mHashCode; + } + + public void dump(IndentingPrintWriter pw) { + pw.print(startTimeElapsed); + pw.print(" -> "); + pw.print(endTimeElapsed); + pw.print(" ("); + pw.print(endTimeElapsed - startTimeElapsed); + pw.print("), "); + pw.print(bgJobCount); + pw.print(" bg jobs."); + pw.println(); + } + + public void dump(@NonNull ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write(StateControllerProto.QuotaController.TimingSession.START_TIME_ELAPSED, + startTimeElapsed); + proto.write(StateControllerProto.QuotaController.TimingSession.END_TIME_ELAPSED, + endTimeElapsed); + proto.write(StateControllerProto.QuotaController.TimingSession.BG_JOB_COUNT, + bgJobCount); + + proto.end(token); + } + } + + private final class Timer { + private final Package mPkg; + private final int mUid; + + // List of jobs currently running for this app that started when the app wasn't in the + // foreground. + private final ArraySet<JobStatus> mRunningBgJobs = new ArraySet<>(); + private long mStartTimeElapsed; + private int mBgJobCount; + + Timer(int uid, int userId, String packageName) { + mPkg = new Package(userId, packageName); + mUid = uid; + } + + void startTrackingJobLocked(@NonNull JobStatus jobStatus) { + if (isTopStartedJobLocked(jobStatus)) { + // We intentionally don't pay attention to fg state changes after a TOP job has + // started. + if (DEBUG) { + Slog.v(TAG, + "Timer ignoring " + jobStatus.toShortString() + " because isTop"); + } + return; + } + if (DEBUG) { + Slog.v(TAG, "Starting to track " + jobStatus.toShortString()); + } + // Always track jobs, even when charging. + mRunningBgJobs.add(jobStatus); + if (shouldTrackLocked()) { + mBgJobCount++; + incrementJobCount(mPkg.userId, mPkg.packageName, 1); + if (mRunningBgJobs.size() == 1) { + // Started tracking the first job. + mStartTimeElapsed = sElapsedRealtimeClock.millis(); + // Starting the timer means that all cached execution stats are now incorrect. + invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName); + scheduleCutoff(); + } + } + } + + void stopTrackingJob(@NonNull JobStatus jobStatus) { + if (DEBUG) { + Slog.v(TAG, "Stopping tracking of " + jobStatus.toShortString()); + } + synchronized (mLock) { + if (mRunningBgJobs.size() == 0) { + // maybeStopTrackingJobLocked can be called when an app cancels a job, so a + // timer may not be running when it's asked to stop tracking a job. + if (DEBUG) { + Slog.d(TAG, "Timer isn't tracking any jobs but still told to stop"); + } + return; + } + if (mRunningBgJobs.remove(jobStatus) + && !mChargeTracker.isCharging() && mRunningBgJobs.size() == 0) { + emitSessionLocked(sElapsedRealtimeClock.millis()); + cancelCutoff(); + } + } + } + + /** + * Stops tracking all jobs and cancels any pending alarms. This should only be called if + * the Timer is not going to be used anymore. + */ + void dropEverythingLocked() { + mRunningBgJobs.clear(); + cancelCutoff(); + } + + private void emitSessionLocked(long nowElapsed) { + if (mBgJobCount <= 0) { + // Nothing to emit. + return; + } + TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mBgJobCount); + saveTimingSession(mPkg.userId, mPkg.packageName, ts); + mBgJobCount = 0; + // Don't reset the tracked jobs list as we need to keep tracking the current number + // of jobs. + // However, cancel the currently scheduled cutoff since it's not currently useful. + cancelCutoff(); + incrementTimingSessionCount(mPkg.userId, mPkg.packageName); + } + + /** + * Returns true if the Timer is actively tracking, as opposed to passively ref counting + * during charging. + */ + public boolean isActive() { + synchronized (mLock) { + return mBgJobCount > 0; + } + } + + boolean isRunning(JobStatus jobStatus) { + return mRunningBgJobs.contains(jobStatus); + } + + long getCurrentDuration(long nowElapsed) { + synchronized (mLock) { + return !isActive() ? 0 : nowElapsed - mStartTimeElapsed; + } + } + + int getBgJobCount() { + synchronized (mLock) { + return mBgJobCount; + } + } + + private boolean shouldTrackLocked() { + final int standbyBucket = JobSchedulerService.standbyBucketForPackage(mPkg.packageName, + mPkg.userId, sElapsedRealtimeClock.millis()); + return (standbyBucket == RESTRICTED_INDEX || !mChargeTracker.isCharging()) + && !mForegroundUids.get(mUid); + } + + void onStateChangedLocked(long nowElapsed, boolean isQuotaFree) { + if (isQuotaFree) { + emitSessionLocked(nowElapsed); + } else if (!isActive() && shouldTrackLocked()) { + // Start timing from unplug. + if (mRunningBgJobs.size() > 0) { + mStartTimeElapsed = nowElapsed; + // NOTE: this does have the unfortunate consequence that if the device is + // repeatedly plugged in and unplugged, or an app changes foreground state + // very frequently, the job count for a package may be artificially high. + mBgJobCount = mRunningBgJobs.size(); + incrementJobCount(mPkg.userId, mPkg.packageName, mBgJobCount); + // Starting the timer means that all cached execution stats are now + // incorrect. + invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName); + // Schedule cutoff since we're now actively tracking for quotas again. + scheduleCutoff(); + } + } + } + + void rescheduleCutoff() { + cancelCutoff(); + scheduleCutoff(); + } + + private void scheduleCutoff() { + // Each package can only be in one standby bucket, so we only need to have one + // message per timer. We only need to reschedule when restarting timer or when + // standby bucket changes. + synchronized (mLock) { + if (!isActive()) { + return; + } + Message msg = mHandler.obtainMessage(MSG_REACHED_QUOTA, mPkg); + final long timeRemainingMs = getTimeUntilQuotaConsumedLocked(mPkg.userId, + mPkg.packageName); + if (DEBUG) { + Slog.i(TAG, "Job for " + mPkg + " has " + timeRemainingMs + "ms left."); + } + // If the job was running the entire time, then the system would be up, so it's + // fine to use uptime millis for these messages. + mHandler.sendMessageDelayed(msg, timeRemainingMs); + } + } + + private void cancelCutoff() { + mHandler.removeMessages(MSG_REACHED_QUOTA, mPkg); + } + + public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) { + pw.print("Timer{"); + pw.print(mPkg); + pw.print("} "); + if (isActive()) { + pw.print("started at "); + pw.print(mStartTimeElapsed); + pw.print(" ("); + pw.print(sElapsedRealtimeClock.millis() - mStartTimeElapsed); + pw.print("ms ago)"); + } else { + pw.print("NOT active"); + } + pw.print(", "); + pw.print(mBgJobCount); + pw.print(" running bg jobs"); + pw.println(); + pw.increaseIndent(); + for (int i = 0; i < mRunningBgJobs.size(); i++) { + JobStatus js = mRunningBgJobs.valueAt(i); + if (predicate.test(js)) { + pw.println(js.toShortString()); + } + } + pw.decreaseIndent(); + } + + public void dump(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + + mPkg.dumpDebug(proto, StateControllerProto.QuotaController.Timer.PKG); + proto.write(StateControllerProto.QuotaController.Timer.IS_ACTIVE, isActive()); + proto.write(StateControllerProto.QuotaController.Timer.START_TIME_ELAPSED, + mStartTimeElapsed); + proto.write(StateControllerProto.QuotaController.Timer.BG_JOB_COUNT, mBgJobCount); + for (int i = 0; i < mRunningBgJobs.size(); i++) { + JobStatus js = mRunningBgJobs.valueAt(i); + if (predicate.test(js)) { + js.writeToShortProto(proto, + StateControllerProto.QuotaController.Timer.RUNNING_JOBS); + } + } + + proto.end(token); + } + } + + /** + * Tracking of app assignments to standby buckets + */ + final class StandbyTracker extends AppIdleStateChangeListener { + + @Override + public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId, + boolean idle, int bucket, int reason) { + // Update job bookkeeping out of band. + BackgroundThread.getHandler().post(() -> { + final int bucketIndex = JobSchedulerService.standbyBucketToBucketIndex(bucket); + if (DEBUG) { + Slog.i(TAG, "Moving pkg " + string(userId, packageName) + " to bucketIndex " + + bucketIndex); + } + List<JobStatus> restrictedChanges = new ArrayList<>(); + synchronized (mLock) { + ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName); + if (jobs == null || jobs.size() == 0) { + return; + } + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus js = jobs.valueAt(i); + // Effective standby bucket can change after this in some situations so + // use the real bucket so that the job is tracked by the controllers. + if ((bucketIndex == RESTRICTED_INDEX + || js.getStandbyBucket() == RESTRICTED_INDEX) + && bucketIndex != js.getStandbyBucket()) { + restrictedChanges.add(js); + } + js.setStandbyBucket(bucketIndex); + } + Timer timer = mPkgTimers.get(userId, packageName); + if (timer != null && timer.isActive()) { + timer.rescheduleCutoff(); + } + if (maybeUpdateConstraintForPkgLocked(userId, packageName)) { + mStateChangedListener.onControllerStateChanged(); + } + } + if (restrictedChanges.size() > 0) { + mStateChangedListener.onRestrictedBucketChanged(restrictedChanges); + } + }); + } + } + + private final class DeleteTimingSessionsFunctor implements Consumer<List<TimingSession>> { + private final Predicate<TimingSession> mTooOld = new Predicate<TimingSession>() { + public boolean test(TimingSession ts) { + return ts.endTimeElapsed <= sElapsedRealtimeClock.millis() - MAX_PERIOD_MS; + } + }; + + @Override + public void accept(List<TimingSession> sessions) { + if (sessions != null) { + // Remove everything older than MAX_PERIOD_MS time ago. + sessions.removeIf(mTooOld); + } + } + } + + private final DeleteTimingSessionsFunctor mDeleteOldSessionsFunctor = + new DeleteTimingSessionsFunctor(); + + @VisibleForTesting + void deleteObsoleteSessionsLocked() { + mTimingSessions.forEach(mDeleteOldSessionsFunctor); + } + + private class QcHandler extends Handler { + QcHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + synchronized (mLock) { + switch (msg.what) { + case MSG_REACHED_QUOTA: { + Package pkg = (Package) msg.obj; + if (DEBUG) { + Slog.d(TAG, "Checking if " + pkg + " has reached its quota."); + } + + long timeRemainingMs = getRemainingExecutionTimeLocked(pkg.userId, + pkg.packageName); + if (timeRemainingMs <= 50) { + // Less than 50 milliseconds left. Start process of shutting down jobs. + if (DEBUG) Slog.d(TAG, pkg + " has reached its quota."); + if (maybeUpdateConstraintForPkgLocked(pkg.userId, pkg.packageName)) { + mStateChangedListener.onControllerStateChanged(); + } + } else { + // This could potentially happen if an old session phases out while a + // job is currently running. + // Reschedule message + Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg); + timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId, + pkg.packageName); + if (DEBUG) { + Slog.d(TAG, pkg + " has " + timeRemainingMs + "ms left."); + } + sendMessageDelayed(rescheduleMsg, timeRemainingMs); + } + break; + } + case MSG_CLEAN_UP_SESSIONS: + if (DEBUG) { + Slog.d(TAG, "Cleaning up timing sessions."); + } + deleteObsoleteSessionsLocked(); + maybeScheduleCleanupAlarmLocked(); + + break; + case MSG_CHECK_PACKAGE: { + String packageName = (String) msg.obj; + int userId = msg.arg1; + if (DEBUG) { + Slog.d(TAG, "Checking pkg " + string(userId, packageName)); + } + if (maybeUpdateConstraintForPkgLocked(userId, packageName)) { + mStateChangedListener.onControllerStateChanged(); + } + break; + } + case MSG_UID_PROCESS_STATE_CHANGED: { + final int uid = msg.arg1; + final int procState = msg.arg2; + final int userId = UserHandle.getUserId(uid); + final long nowElapsed = sElapsedRealtimeClock.millis(); + + synchronized (mLock) { + boolean isQuotaFree; + if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { + mForegroundUids.put(uid, true); + isQuotaFree = true; + } else { + mForegroundUids.delete(uid); + isQuotaFree = false; + } + // Update Timers first. + if (mPkgTimers.indexOfKey(userId) >= 0) { + ArraySet<String> packages = mUidToPackageCache.get(uid); + if (packages == null) { + try { + String[] pkgs = AppGlobals.getPackageManager() + .getPackagesForUid(uid); + if (pkgs != null) { + for (String pkg : pkgs) { + mUidToPackageCache.add(uid, pkg); + } + packages = mUidToPackageCache.get(uid); + } + } catch (RemoteException e) { + Slog.wtf(TAG, "Failed to get package list", e); + } + } + if (packages != null) { + for (int i = packages.size() - 1; i >= 0; --i) { + Timer t = mPkgTimers.get(userId, packages.valueAt(i)); + if (t != null) { + t.onStateChangedLocked(nowElapsed, isQuotaFree); + } + } + } + } + if (maybeUpdateConstraintForUidLocked(uid)) { + mStateChangedListener.onControllerStateChanged(); + } + } + break; + } + } + } + } + } + + static class AlarmQueue extends PriorityQueue<Pair<Package, Long>> { + AlarmQueue() { + super(1, (o1, o2) -> (int) (o1.second - o2.second)); + } + + /** + * Remove any instances of the Package from the queue. + * + * @return true if an instance was removed, false otherwise. + */ + boolean remove(@NonNull Package pkg) { + boolean removed = false; + Pair[] alarms = toArray(new Pair[size()]); + for (int i = alarms.length - 1; i >= 0; --i) { + if (pkg.equals(alarms[i].first)) { + remove(alarms[i]); + removed = true; + } + } + return removed; + } + } + + /** Track when UPTCs are expected to come back into quota. */ + private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener { + @GuardedBy("mLock") + private final AlarmQueue mAlarmQueue = new AlarmQueue(); + /** The next time the alarm is set to go off, in the elapsed realtime timebase. */ + @GuardedBy("mLock") + private long mTriggerTimeElapsed = 0; + /** The minimum amount of time between quota check alarms. */ + @GuardedBy("mLock") + private long mMinQuotaCheckDelayMs = QcConstants.DEFAULT_MIN_QUOTA_CHECK_DELAY_MS; + + @GuardedBy("mLock") + void addAlarmLocked(int userId, @NonNull String pkgName, long inQuotaTimeElapsed) { + final Package pkg = new Package(userId, pkgName); + mAlarmQueue.remove(pkg); + mAlarmQueue.offer(new Pair<>(pkg, inQuotaTimeElapsed)); + setNextAlarmLocked(); + } + + @GuardedBy("mLock") + void setMinQuotaCheckDelayMs(long minDelayMs) { + mMinQuotaCheckDelayMs = minDelayMs; + } + + @GuardedBy("mLock") + void removeAlarmLocked(@NonNull Package pkg) { + if (mAlarmQueue.remove(pkg)) { + setNextAlarmLocked(); + } + } + + @GuardedBy("mLock") + void removeAlarmLocked(int userId, @NonNull String packageName) { + removeAlarmLocked(new Package(userId, packageName)); + } + + @GuardedBy("mLock") + void removeAlarmsLocked(int userId) { + boolean removed = false; + Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); + for (int i = alarms.length - 1; i >= 0; --i) { + final Package pkg = (Package) alarms[i].first; + if (userId == pkg.userId) { + mAlarmQueue.remove(alarms[i]); + removed = true; + } + } + if (removed) { + setNextAlarmLocked(); + } + } + + @GuardedBy("mLock") + private void setNextAlarmLocked() { + setNextAlarmLocked(sElapsedRealtimeClock.millis()); + } + + @GuardedBy("mLock") + private void setNextAlarmLocked(long earliestTriggerElapsed) { + if (mAlarmQueue.size() > 0) { + final Pair<Package, Long> alarm = mAlarmQueue.peek(); + final long nextTriggerTimeElapsed = Math.max(earliestTriggerElapsed, alarm.second); + // Only schedule the alarm if one of the following is true: + // 1. There isn't one currently scheduled + // 2. The new alarm is significantly earlier than the previous alarm. If it's + // earlier but not significantly so, then we essentially delay the job a few extra + // minutes. + // 3. The alarm is after the current alarm. + if (mTriggerTimeElapsed == 0 + || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS + || mTriggerTimeElapsed < nextTriggerTimeElapsed) { + if (DEBUG) { + Slog.d(TAG, "Scheduling start alarm at " + nextTriggerTimeElapsed + + " for app " + alarm.first); + } + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed, + ALARM_TAG_QUOTA_CHECK, this, mHandler); + mTriggerTimeElapsed = nextTriggerTimeElapsed; + } + } else { + mAlarmManager.cancel(this); + mTriggerTimeElapsed = 0; + } + } + + @Override + public void onAlarm() { + synchronized (mLock) { + while (mAlarmQueue.size() > 0) { + final Pair<Package, Long> alarm = mAlarmQueue.peek(); + if (alarm.second <= sElapsedRealtimeClock.millis()) { + mHandler.obtainMessage(MSG_CHECK_PACKAGE, alarm.first.userId, 0, + alarm.first.packageName).sendToTarget(); + mAlarmQueue.remove(alarm); + } else { + break; + } + } + setNextAlarmLocked(sElapsedRealtimeClock.millis() + mMinQuotaCheckDelayMs); + } + } + + @GuardedBy("mLock") + void dumpLocked(IndentingPrintWriter pw) { + pw.println("In quota alarms:"); + pw.increaseIndent(); + + if (mAlarmQueue.size() == 0) { + pw.println("NOT WAITING"); + } else { + Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); + for (int i = 0; i < alarms.length; ++i) { + final Package pkg = (Package) alarms[i].first; + pw.print(pkg); + pw.print(": "); + pw.print(alarms[i].second); + pw.println(); + } + } + + pw.decreaseIndent(); + } + + @GuardedBy("mLock") + void dumpLocked(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write( + StateControllerProto.QuotaController.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED, + mTriggerTimeElapsed); + + Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); + for (int i = 0; i < alarms.length; ++i) { + final long aToken = proto.start( + StateControllerProto.QuotaController.InQuotaAlarmListener.ALARMS); + + final Package pkg = (Package) alarms[i].first; + pkg.dumpDebug(proto, + StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.PKG); + proto.write( + StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED, + (Long) alarms[i].second); + + proto.end(aToken); + } + + proto.end(token); + } + } + + @VisibleForTesting + class QcConstants extends ContentObserver { + private ContentResolver mResolver; + private final KeyValueListParser mParser = new KeyValueListParser(','); + + private static final String KEY_ALLOWED_TIME_PER_PERIOD_MS = "allowed_time_per_period_ms"; + private static final String KEY_IN_QUOTA_BUFFER_MS = "in_quota_buffer_ms"; + private static final String KEY_WINDOW_SIZE_ACTIVE_MS = "window_size_active_ms"; + private static final String KEY_WINDOW_SIZE_WORKING_MS = "window_size_working_ms"; + private static final String KEY_WINDOW_SIZE_FREQUENT_MS = "window_size_frequent_ms"; + private static final String KEY_WINDOW_SIZE_RARE_MS = "window_size_rare_ms"; + private static final String KEY_WINDOW_SIZE_RESTRICTED_MS = "window_size_restricted_ms"; + private static final String KEY_MAX_EXECUTION_TIME_MS = "max_execution_time_ms"; + private static final String KEY_MAX_JOB_COUNT_ACTIVE = "max_job_count_active"; + private static final String KEY_MAX_JOB_COUNT_WORKING = "max_job_count_working"; + private static final String KEY_MAX_JOB_COUNT_FREQUENT = "max_job_count_frequent"; + private static final String KEY_MAX_JOB_COUNT_RARE = "max_job_count_rare"; + private static final String KEY_MAX_JOB_COUNT_RESTRICTED = "max_job_count_restricted"; + private static final String KEY_RATE_LIMITING_WINDOW_MS = "rate_limiting_window_ms"; + private static final String KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = + "max_job_count_per_rate_limiting_window"; + private static final String KEY_MAX_SESSION_COUNT_ACTIVE = "max_session_count_active"; + private static final String KEY_MAX_SESSION_COUNT_WORKING = "max_session_count_working"; + private static final String KEY_MAX_SESSION_COUNT_FREQUENT = "max_session_count_frequent"; + private static final String KEY_MAX_SESSION_COUNT_RARE = "max_session_count_rare"; + private static final String KEY_MAX_SESSION_COUNT_RESTRICTED = + "max_session_count_restricted"; + private static final String KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = + "max_session_count_per_rate_limiting_window"; + private static final String KEY_TIMING_SESSION_COALESCING_DURATION_MS = + "timing_session_coalescing_duration_ms"; + private static final String KEY_MIN_QUOTA_CHECK_DELAY_MS = "min_quota_check_delay_ms"; + + private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_MS = + 10 * 60 * 1000L; // 10 minutes + private static final long DEFAULT_IN_QUOTA_BUFFER_MS = + 30 * 1000L; // 30 seconds + private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS = + DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; // ACTIVE apps can run jobs at any time + private static final long DEFAULT_WINDOW_SIZE_WORKING_MS = + 2 * 60 * 60 * 1000L; // 2 hours + private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS = + 8 * 60 * 60 * 1000L; // 8 hours + private static final long DEFAULT_WINDOW_SIZE_RARE_MS = + 24 * 60 * 60 * 1000L; // 24 hours + private static final long DEFAULT_WINDOW_SIZE_RESTRICTED_MS = + 24 * 60 * 60 * 1000L; // 24 hours + private static final long DEFAULT_MAX_EXECUTION_TIME_MS = + 4 * HOUR_IN_MILLIS; + private static final long DEFAULT_RATE_LIMITING_WINDOW_MS = + MINUTE_IN_MILLIS; + private static final int DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 20; + private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = + 75; // 75/window = 450/hr = 1/session + private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session + (int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS); + private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session + (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS); + private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session + (int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS); + private static final int DEFAULT_MAX_JOB_COUNT_RESTRICTED = 10; + private static final int DEFAULT_MAX_SESSION_COUNT_ACTIVE = + 75; // 450/hr + private static final int DEFAULT_MAX_SESSION_COUNT_WORKING = + 10; // 5/hr + private static final int DEFAULT_MAX_SESSION_COUNT_FREQUENT = + 8; // 1/hr + private static final int DEFAULT_MAX_SESSION_COUNT_RARE = + 3; // .125/hr + private static final int DEFAULT_MAX_SESSION_COUNT_RESTRICTED = 1; // 1/day + private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20; + private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds + private static final long DEFAULT_MIN_QUOTA_CHECK_DELAY_MS = MINUTE_IN_MILLIS; + + /** How much time each app will have to run jobs within their standby bucket window. */ + public long ALLOWED_TIME_PER_PERIOD_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; + + /** + * How much time the package should have before transitioning from out-of-quota to in-quota. + * This should not affect processing if the package is already in-quota. + */ + public long IN_QUOTA_BUFFER_MS = DEFAULT_IN_QUOTA_BUFFER_MS; + + /** + * The quota window size of the particular standby bucket. Apps in this standby bucket are + * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past + * WINDOW_SIZE_MS. + */ + public long WINDOW_SIZE_ACTIVE_MS = DEFAULT_WINDOW_SIZE_ACTIVE_MS; + + /** + * The quota window size of the particular standby bucket. Apps in this standby bucket are + * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past + * WINDOW_SIZE_MS. + */ + public long WINDOW_SIZE_WORKING_MS = DEFAULT_WINDOW_SIZE_WORKING_MS; + + /** + * The quota window size of the particular standby bucket. Apps in this standby bucket are + * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past + * WINDOW_SIZE_MS. + */ + public long WINDOW_SIZE_FREQUENT_MS = DEFAULT_WINDOW_SIZE_FREQUENT_MS; + + /** + * The quota window size of the particular standby bucket. Apps in this standby bucket are + * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past + * WINDOW_SIZE_MS. + */ + public long WINDOW_SIZE_RARE_MS = DEFAULT_WINDOW_SIZE_RARE_MS; + + /** + * The quota window size of the particular standby bucket. Apps in this standby bucket are + * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past + * WINDOW_SIZE_MS. + */ + public long WINDOW_SIZE_RESTRICTED_MS = DEFAULT_WINDOW_SIZE_RESTRICTED_MS; + + /** + * The maximum amount of time an app can have its jobs running within a 24 hour window. + */ + public long MAX_EXECUTION_TIME_MS = DEFAULT_MAX_EXECUTION_TIME_MS; + + /** + * The maximum number of jobs an app can run within this particular standby bucket's + * window size. + */ + public int MAX_JOB_COUNT_ACTIVE = DEFAULT_MAX_JOB_COUNT_ACTIVE; + + /** + * The maximum number of jobs an app can run within this particular standby bucket's + * window size. + */ + public int MAX_JOB_COUNT_WORKING = DEFAULT_MAX_JOB_COUNT_WORKING; + + /** + * The maximum number of jobs an app can run within this particular standby bucket's + * window size. + */ + public int MAX_JOB_COUNT_FREQUENT = DEFAULT_MAX_JOB_COUNT_FREQUENT; + + /** + * The maximum number of jobs an app can run within this particular standby bucket's + * window size. + */ + public int MAX_JOB_COUNT_RARE = DEFAULT_MAX_JOB_COUNT_RARE; + + /** + * The maximum number of jobs an app can run within this particular standby bucket's + * window size. + */ + public int MAX_JOB_COUNT_RESTRICTED = DEFAULT_MAX_JOB_COUNT_RESTRICTED; + + /** The period of time used to rate limit recently run jobs. */ + public long RATE_LIMITING_WINDOW_MS = DEFAULT_RATE_LIMITING_WINDOW_MS; + + /** + * The maximum number of jobs that can run within the past {@link #RATE_LIMITING_WINDOW_MS}. + */ + public int MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = + DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_ACTIVE = DEFAULT_MAX_SESSION_COUNT_ACTIVE; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_WORKING = DEFAULT_MAX_SESSION_COUNT_WORKING; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_FREQUENT = DEFAULT_MAX_SESSION_COUNT_FREQUENT; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_RARE = DEFAULT_MAX_SESSION_COUNT_RARE; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_RESTRICTED = DEFAULT_MAX_SESSION_COUNT_RESTRICTED; + + /** + * The maximum number of {@link TimingSession}s that can run within the past + * {@link #ALLOWED_TIME_PER_PERIOD_MS}. + */ + public int MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = + DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; + + /** + * Treat two distinct {@link TimingSession}s as the same if they start and end within this + * amount of time of each other. + */ + public long TIMING_SESSION_COALESCING_DURATION_MS = + DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS; + + /** The minimum amount of time between quota check alarms. */ + public long MIN_QUOTA_CHECK_DELAY_MS = DEFAULT_MIN_QUOTA_CHECK_DELAY_MS; + + // Safeguards + + /** The minimum number of jobs that any bucket will be allowed to run within its window. */ + private static final int MIN_BUCKET_JOB_COUNT = 10; + + /** + * The minimum number of {@link TimingSession}s that any bucket will be allowed to run + * within its window. + */ + private static final int MIN_BUCKET_SESSION_COUNT = 1; + + /** The minimum value that {@link #MAX_EXECUTION_TIME_MS} can have. */ + private static final long MIN_MAX_EXECUTION_TIME_MS = 60 * MINUTE_IN_MILLIS; + + /** The minimum value that {@link #MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW} can have. */ + private static final int MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 10; + + /** The minimum value that {@link #MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW} can have. */ + private static final int MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 10; + + /** The minimum value that {@link #RATE_LIMITING_WINDOW_MS} can have. */ + private static final long MIN_RATE_LIMITING_WINDOW_MS = 30 * SECOND_IN_MILLIS; + + QcConstants(Handler handler) { + super(handler); + } + + private void start(ContentResolver resolver) { + mResolver = resolver; + mResolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.JOB_SCHEDULER_QUOTA_CONTROLLER_CONSTANTS), false, this); + onChange(true, null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + final String constants = Settings.Global.getString( + mResolver, Settings.Global.JOB_SCHEDULER_QUOTA_CONTROLLER_CONSTANTS); + + try { + mParser.setString(constants); + } catch (Exception e) { + // Failed to parse the settings string, log this and move on with defaults. + Slog.e(TAG, "Bad jobscheduler quota controller settings", e); + } + + ALLOWED_TIME_PER_PERIOD_MS = mParser.getDurationMillis( + KEY_ALLOWED_TIME_PER_PERIOD_MS, DEFAULT_ALLOWED_TIME_PER_PERIOD_MS); + IN_QUOTA_BUFFER_MS = mParser.getDurationMillis( + KEY_IN_QUOTA_BUFFER_MS, DEFAULT_IN_QUOTA_BUFFER_MS); + WINDOW_SIZE_ACTIVE_MS = mParser.getDurationMillis( + KEY_WINDOW_SIZE_ACTIVE_MS, DEFAULT_WINDOW_SIZE_ACTIVE_MS); + WINDOW_SIZE_WORKING_MS = mParser.getDurationMillis( + KEY_WINDOW_SIZE_WORKING_MS, DEFAULT_WINDOW_SIZE_WORKING_MS); + WINDOW_SIZE_FREQUENT_MS = mParser.getDurationMillis( + KEY_WINDOW_SIZE_FREQUENT_MS, DEFAULT_WINDOW_SIZE_FREQUENT_MS); + WINDOW_SIZE_RARE_MS = mParser.getDurationMillis( + KEY_WINDOW_SIZE_RARE_MS, DEFAULT_WINDOW_SIZE_RARE_MS); + WINDOW_SIZE_RESTRICTED_MS = mParser.getDurationMillis( + KEY_WINDOW_SIZE_RESTRICTED_MS, DEFAULT_WINDOW_SIZE_RESTRICTED_MS); + MAX_EXECUTION_TIME_MS = mParser.getDurationMillis( + KEY_MAX_EXECUTION_TIME_MS, DEFAULT_MAX_EXECUTION_TIME_MS); + MAX_JOB_COUNT_ACTIVE = mParser.getInt( + KEY_MAX_JOB_COUNT_ACTIVE, DEFAULT_MAX_JOB_COUNT_ACTIVE); + MAX_JOB_COUNT_WORKING = mParser.getInt( + KEY_MAX_JOB_COUNT_WORKING, DEFAULT_MAX_JOB_COUNT_WORKING); + MAX_JOB_COUNT_FREQUENT = mParser.getInt( + KEY_MAX_JOB_COUNT_FREQUENT, DEFAULT_MAX_JOB_COUNT_FREQUENT); + MAX_JOB_COUNT_RARE = mParser.getInt( + KEY_MAX_JOB_COUNT_RARE, DEFAULT_MAX_JOB_COUNT_RARE); + MAX_JOB_COUNT_RESTRICTED = mParser.getInt( + KEY_MAX_JOB_COUNT_RESTRICTED, DEFAULT_MAX_JOB_COUNT_RESTRICTED); + RATE_LIMITING_WINDOW_MS = mParser.getLong( + KEY_RATE_LIMITING_WINDOW_MS, DEFAULT_RATE_LIMITING_WINDOW_MS); + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt( + KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); + MAX_SESSION_COUNT_ACTIVE = mParser.getInt( + KEY_MAX_SESSION_COUNT_ACTIVE, DEFAULT_MAX_SESSION_COUNT_ACTIVE); + MAX_SESSION_COUNT_WORKING = mParser.getInt( + KEY_MAX_SESSION_COUNT_WORKING, DEFAULT_MAX_SESSION_COUNT_WORKING); + MAX_SESSION_COUNT_FREQUENT = mParser.getInt( + KEY_MAX_SESSION_COUNT_FREQUENT, DEFAULT_MAX_SESSION_COUNT_FREQUENT); + MAX_SESSION_COUNT_RARE = mParser.getInt( + KEY_MAX_SESSION_COUNT_RARE, DEFAULT_MAX_SESSION_COUNT_RARE); + MAX_SESSION_COUNT_RESTRICTED = mParser.getInt( + KEY_MAX_SESSION_COUNT_RESTRICTED, DEFAULT_MAX_SESSION_COUNT_RESTRICTED); + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt( + KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); + TIMING_SESSION_COALESCING_DURATION_MS = mParser.getLong( + KEY_TIMING_SESSION_COALESCING_DURATION_MS, + DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS); + MIN_QUOTA_CHECK_DELAY_MS = mParser.getDurationMillis(KEY_MIN_QUOTA_CHECK_DELAY_MS, + DEFAULT_MIN_QUOTA_CHECK_DELAY_MS); + + updateConstants(); + } + + @VisibleForTesting + void updateConstants() { + synchronized (mLock) { + boolean changed = false; + + long newMaxExecutionTimeMs = Math.max(MIN_MAX_EXECUTION_TIME_MS, + Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS)); + if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) { + mMaxExecutionTimeMs = newMaxExecutionTimeMs; + mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + changed = true; + } + long newAllowedTimeMs = Math.min(mMaxExecutionTimeMs, + Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_MS)); + if (mAllowedTimePerPeriodMs != newAllowedTimeMs) { + mAllowedTimePerPeriodMs = newAllowedTimeMs; + mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs; + changed = true; + } + // Make sure quota buffer is non-negative, not greater than allowed time per period, + // and no more than 5 minutes. + long newQuotaBufferMs = Math.max(0, Math.min(mAllowedTimePerPeriodMs, + Math.min(5 * MINUTE_IN_MILLIS, IN_QUOTA_BUFFER_MS))); + if (mQuotaBufferMs != newQuotaBufferMs) { + mQuotaBufferMs = newQuotaBufferMs; + mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs; + mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + changed = true; + } + long newActivePeriodMs = Math.max(mAllowedTimePerPeriodMs, + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_ACTIVE_MS)); + if (mBucketPeriodsMs[ACTIVE_INDEX] != newActivePeriodMs) { + mBucketPeriodsMs[ACTIVE_INDEX] = newActivePeriodMs; + changed = true; + } + long newWorkingPeriodMs = Math.max(mAllowedTimePerPeriodMs, + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_WORKING_MS)); + if (mBucketPeriodsMs[WORKING_INDEX] != newWorkingPeriodMs) { + mBucketPeriodsMs[WORKING_INDEX] = newWorkingPeriodMs; + changed = true; + } + long newFrequentPeriodMs = Math.max(mAllowedTimePerPeriodMs, + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_FREQUENT_MS)); + if (mBucketPeriodsMs[FREQUENT_INDEX] != newFrequentPeriodMs) { + mBucketPeriodsMs[FREQUENT_INDEX] = newFrequentPeriodMs; + changed = true; + } + long newRarePeriodMs = Math.max(mAllowedTimePerPeriodMs, + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_RARE_MS)); + if (mBucketPeriodsMs[RARE_INDEX] != newRarePeriodMs) { + mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs; + changed = true; + } + // Fit in the range [allowed time (10 mins), 1 week]. + long newRestrictedPeriodMs = Math.max(mAllowedTimePerPeriodMs, + Math.min(7 * 24 * 60 * MINUTE_IN_MILLIS, WINDOW_SIZE_RESTRICTED_MS)); + if (mBucketPeriodsMs[RESTRICTED_INDEX] != newRestrictedPeriodMs) { + mBucketPeriodsMs[RESTRICTED_INDEX] = newRestrictedPeriodMs; + changed = true; + } + long newRateLimitingWindowMs = Math.min(MAX_PERIOD_MS, + Math.max(MIN_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS)); + if (mRateLimitingWindowMs != newRateLimitingWindowMs) { + mRateLimitingWindowMs = newRateLimitingWindowMs; + changed = true; + } + int newMaxJobCountPerRateLimitingWindow = Math.max( + MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); + if (mMaxJobCountPerRateLimitingWindow != newMaxJobCountPerRateLimitingWindow) { + mMaxJobCountPerRateLimitingWindow = newMaxJobCountPerRateLimitingWindow; + changed = true; + } + int newActiveMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE); + if (mMaxBucketJobCounts[ACTIVE_INDEX] != newActiveMaxJobCount) { + mMaxBucketJobCounts[ACTIVE_INDEX] = newActiveMaxJobCount; + changed = true; + } + int newWorkingMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_WORKING); + if (mMaxBucketJobCounts[WORKING_INDEX] != newWorkingMaxJobCount) { + mMaxBucketJobCounts[WORKING_INDEX] = newWorkingMaxJobCount; + changed = true; + } + int newFrequentMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_FREQUENT); + if (mMaxBucketJobCounts[FREQUENT_INDEX] != newFrequentMaxJobCount) { + mMaxBucketJobCounts[FREQUENT_INDEX] = newFrequentMaxJobCount; + changed = true; + } + int newRareMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE); + if (mMaxBucketJobCounts[RARE_INDEX] != newRareMaxJobCount) { + mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount; + changed = true; + } + int newRestrictedMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, + MAX_JOB_COUNT_RESTRICTED); + if (mMaxBucketJobCounts[RESTRICTED_INDEX] != newRestrictedMaxJobCount) { + mMaxBucketJobCounts[RESTRICTED_INDEX] = newRestrictedMaxJobCount; + changed = true; + } + int newMaxSessionCountPerRateLimitPeriod = Math.max( + MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); + if (mMaxSessionCountPerRateLimitingWindow != newMaxSessionCountPerRateLimitPeriod) { + mMaxSessionCountPerRateLimitingWindow = newMaxSessionCountPerRateLimitPeriod; + changed = true; + } + int newActiveMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_ACTIVE); + if (mMaxBucketSessionCounts[ACTIVE_INDEX] != newActiveMaxSessionCount) { + mMaxBucketSessionCounts[ACTIVE_INDEX] = newActiveMaxSessionCount; + changed = true; + } + int newWorkingMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_WORKING); + if (mMaxBucketSessionCounts[WORKING_INDEX] != newWorkingMaxSessionCount) { + mMaxBucketSessionCounts[WORKING_INDEX] = newWorkingMaxSessionCount; + changed = true; + } + int newFrequentMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_FREQUENT); + if (mMaxBucketSessionCounts[FREQUENT_INDEX] != newFrequentMaxSessionCount) { + mMaxBucketSessionCounts[FREQUENT_INDEX] = newFrequentMaxSessionCount; + changed = true; + } + int newRareMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_RARE); + if (mMaxBucketSessionCounts[RARE_INDEX] != newRareMaxSessionCount) { + mMaxBucketSessionCounts[RARE_INDEX] = newRareMaxSessionCount; + changed = true; + } + int newRestrictedMaxSessionCount = Math.max(0, MAX_SESSION_COUNT_RESTRICTED); + if (mMaxBucketSessionCounts[RESTRICTED_INDEX] != newRestrictedMaxSessionCount) { + mMaxBucketSessionCounts[RESTRICTED_INDEX] = newRestrictedMaxSessionCount; + changed = true; + } + long newSessionCoalescingDurationMs = Math.min(15 * MINUTE_IN_MILLIS, + Math.max(0, TIMING_SESSION_COALESCING_DURATION_MS)); + if (mTimingSessionCoalescingDurationMs != newSessionCoalescingDurationMs) { + mTimingSessionCoalescingDurationMs = newSessionCoalescingDurationMs; + changed = true; + } + // Don't set changed to true for this one since we don't need to re-evaluate + // execution stats or constraint status. Limit the delay to the range [0, 15] + // minutes. + mInQuotaAlarmListener.setMinQuotaCheckDelayMs( + Math.min(15 * MINUTE_IN_MILLIS, Math.max(0, MIN_QUOTA_CHECK_DELAY_MS))); + + if (changed) { + // Update job bookkeeping out of band. + BackgroundThread.getHandler().post(() -> { + synchronized (mLock) { + invalidateAllExecutionStatsLocked(); + maybeUpdateAllConstraintsLocked(); + } + }); + } + } + } + + private void dump(IndentingPrintWriter pw) { + pw.println(); + pw.println("QuotaController:"); + pw.increaseIndent(); + pw.printPair(KEY_ALLOWED_TIME_PER_PERIOD_MS, ALLOWED_TIME_PER_PERIOD_MS).println(); + pw.printPair(KEY_IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS).println(); + pw.printPair(KEY_WINDOW_SIZE_ACTIVE_MS, WINDOW_SIZE_ACTIVE_MS).println(); + pw.printPair(KEY_WINDOW_SIZE_WORKING_MS, WINDOW_SIZE_WORKING_MS).println(); + pw.printPair(KEY_WINDOW_SIZE_FREQUENT_MS, WINDOW_SIZE_FREQUENT_MS).println(); + pw.printPair(KEY_WINDOW_SIZE_RARE_MS, WINDOW_SIZE_RARE_MS).println(); + pw.printPair(KEY_WINDOW_SIZE_RESTRICTED_MS, WINDOW_SIZE_RESTRICTED_MS).println(); + pw.printPair(KEY_MAX_EXECUTION_TIME_MS, MAX_EXECUTION_TIME_MS).println(); + pw.printPair(KEY_MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE).println(); + pw.printPair(KEY_MAX_JOB_COUNT_WORKING, MAX_JOB_COUNT_WORKING).println(); + pw.printPair(KEY_MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT).println(); + pw.printPair(KEY_MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE).println(); + pw.printPair(KEY_MAX_JOB_COUNT_RESTRICTED, MAX_JOB_COUNT_RESTRICTED).println(); + pw.printPair(KEY_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS).println(); + pw.printPair(KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW).println(); + pw.printPair(KEY_MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE).println(); + pw.printPair(KEY_MAX_SESSION_COUNT_WORKING, MAX_SESSION_COUNT_WORKING).println(); + pw.printPair(KEY_MAX_SESSION_COUNT_FREQUENT, MAX_SESSION_COUNT_FREQUENT).println(); + pw.printPair(KEY_MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE).println(); + pw.printPair(KEY_MAX_SESSION_COUNT_RESTRICTED, MAX_SESSION_COUNT_RESTRICTED).println(); + pw.printPair(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW).println(); + pw.printPair(KEY_TIMING_SESSION_COALESCING_DURATION_MS, + TIMING_SESSION_COALESCING_DURATION_MS).println(); + pw.printPair(KEY_MIN_QUOTA_CHECK_DELAY_MS, MIN_QUOTA_CHECK_DELAY_MS).println(); + pw.decreaseIndent(); + } + + private void dump(ProtoOutputStream proto) { + final long qcToken = proto.start(ConstantsProto.QUOTA_CONTROLLER); + proto.write(ConstantsProto.QuotaController.ALLOWED_TIME_PER_PERIOD_MS, + ALLOWED_TIME_PER_PERIOD_MS); + proto.write(ConstantsProto.QuotaController.IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS); + proto.write(ConstantsProto.QuotaController.ACTIVE_WINDOW_SIZE_MS, + WINDOW_SIZE_ACTIVE_MS); + proto.write(ConstantsProto.QuotaController.WORKING_WINDOW_SIZE_MS, + WINDOW_SIZE_WORKING_MS); + proto.write(ConstantsProto.QuotaController.FREQUENT_WINDOW_SIZE_MS, + WINDOW_SIZE_FREQUENT_MS); + proto.write(ConstantsProto.QuotaController.RARE_WINDOW_SIZE_MS, WINDOW_SIZE_RARE_MS); + proto.write(ConstantsProto.QuotaController.RESTRICTED_WINDOW_SIZE_MS, + WINDOW_SIZE_RESTRICTED_MS); + proto.write(ConstantsProto.QuotaController.MAX_EXECUTION_TIME_MS, + MAX_EXECUTION_TIME_MS); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_WORKING, + MAX_JOB_COUNT_WORKING); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT, + MAX_JOB_COUNT_FREQUENT); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RESTRICTED, + MAX_JOB_COUNT_RESTRICTED); + proto.write(ConstantsProto.QuotaController.RATE_LIMITING_WINDOW_MS, + RATE_LIMITING_WINDOW_MS); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_ACTIVE, + MAX_SESSION_COUNT_ACTIVE); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_WORKING, + MAX_SESSION_COUNT_WORKING); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_FREQUENT, + MAX_SESSION_COUNT_FREQUENT); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RARE, + MAX_SESSION_COUNT_RARE); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RESTRICTED, + MAX_SESSION_COUNT_RESTRICTED); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); + proto.write(ConstantsProto.QuotaController.TIMING_SESSION_COALESCING_DURATION_MS, + TIMING_SESSION_COALESCING_DURATION_MS); + proto.write(ConstantsProto.QuotaController.MIN_QUOTA_CHECK_DELAY_MS, + MIN_QUOTA_CHECK_DELAY_MS); + proto.end(qcToken); + } + } + + //////////////////////// TESTING HELPERS ///////////////////////////// + + @VisibleForTesting + long getAllowedTimePerPeriodMs() { + return mAllowedTimePerPeriodMs; + } + + @VisibleForTesting + @NonNull + int[] getBucketMaxJobCounts() { + return mMaxBucketJobCounts; + } + + @VisibleForTesting + @NonNull + int[] getBucketMaxSessionCounts() { + return mMaxBucketSessionCounts; + } + + @VisibleForTesting + @NonNull + long[] getBucketWindowSizes() { + return mBucketPeriodsMs; + } + + @VisibleForTesting + @NonNull + SparseBooleanArray getForegroundUids() { + return mForegroundUids; + } + + @VisibleForTesting + @NonNull + Handler getHandler() { + return mHandler; + } + + @VisibleForTesting + long getInQuotaBufferMs() { + return mQuotaBufferMs; + } + + @VisibleForTesting + long getMaxExecutionTimeMs() { + return mMaxExecutionTimeMs; + } + + @VisibleForTesting + int getMaxJobCountPerRateLimitingWindow() { + return mMaxJobCountPerRateLimitingWindow; + } + + @VisibleForTesting + int getMaxSessionCountPerRateLimitingWindow() { + return mMaxSessionCountPerRateLimitingWindow; + } + + @VisibleForTesting + long getRateLimitingWindowMs() { + return mRateLimitingWindowMs; + } + + @VisibleForTesting + long getTimingSessionCoalescingDurationMs() { + return mTimingSessionCoalescingDurationMs; + } + + @VisibleForTesting + @Nullable + List<TimingSession> getTimingSessions(int userId, String packageName) { + return mTimingSessions.get(userId, packageName); + } + + @VisibleForTesting + @NonNull + QcConstants getQcConstants() { + return mQcConstants; + } + + //////////////////////////// DATA DUMP ////////////////////////////// + + @Override + public void dumpControllerStateLocked(final IndentingPrintWriter pw, + final Predicate<JobStatus> predicate) { + pw.println("Is charging: " + mChargeTracker.isCharging()); + pw.println("Current elapsed time: " + sElapsedRealtimeClock.millis()); + pw.println(); + + pw.print("Foreground UIDs: "); + pw.println(mForegroundUids.toString()); + pw.println(); + + pw.println("Cached UID->package map:"); + pw.increaseIndent(); + for (int i = 0; i < mUidToPackageCache.size(); ++i) { + final int uid = mUidToPackageCache.keyAt(i); + pw.print(uid); + pw.print(": "); + pw.println(mUidToPackageCache.get(uid)); + } + pw.decreaseIndent(); + pw.println(); + + mTrackedJobs.forEach((jobs) -> { + for (int j = 0; j < jobs.size(); j++) { + final JobStatus js = jobs.valueAt(j); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + if (mTopStartedJobs.contains(js)) { + pw.print(" (TOP)"); + } + pw.println(); + + pw.increaseIndent(); + pw.print(JobStatus.bucketName(js.getEffectiveStandbyBucket())); + pw.print(", "); + if (js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) { + pw.print("within quota"); + } else { + pw.print("not within quota"); + } + pw.print(", "); + pw.print(getRemainingExecutionTimeLocked(js)); + pw.print("ms remaining in quota"); + pw.decreaseIndent(); + pw.println(); + } + }); + + pw.println(); + for (int u = 0; u < mPkgTimers.numMaps(); ++u) { + final int userId = mPkgTimers.keyAt(u); + for (int p = 0; p < mPkgTimers.numElementsForKey(userId); ++p) { + final String pkgName = mPkgTimers.keyAt(u, p); + mPkgTimers.valueAt(u, p).dump(pw, predicate); + pw.println(); + List<TimingSession> sessions = mTimingSessions.get(userId, pkgName); + if (sessions != null) { + pw.increaseIndent(); + pw.println("Saved sessions:"); + pw.increaseIndent(); + for (int j = sessions.size() - 1; j >= 0; j--) { + TimingSession session = sessions.get(j); + session.dump(pw); + } + pw.decreaseIndent(); + pw.decreaseIndent(); + pw.println(); + } + } + } + + pw.println("Cached execution stats:"); + pw.increaseIndent(); + for (int u = 0; u < mExecutionStatsCache.numMaps(); ++u) { + final int userId = mExecutionStatsCache.keyAt(u); + for (int p = 0; p < mExecutionStatsCache.numElementsForKey(userId); ++p) { + final String pkgName = mExecutionStatsCache.keyAt(u, p); + ExecutionStats[] stats = mExecutionStatsCache.valueAt(u, p); + + pw.println(string(userId, pkgName)); + pw.increaseIndent(); + for (int i = 0; i < stats.length; ++i) { + ExecutionStats executionStats = stats[i]; + if (executionStats != null) { + pw.print(JobStatus.bucketName(i)); + pw.print(": "); + pw.println(executionStats); + } + } + pw.decreaseIndent(); + } + } + pw.decreaseIndent(); + + pw.println(); + mInQuotaAlarmListener.dumpLocked(pw); + pw.decreaseIndent(); + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.QUOTA); + + proto.write(StateControllerProto.QuotaController.IS_CHARGING, mChargeTracker.isCharging()); + proto.write(StateControllerProto.QuotaController.ELAPSED_REALTIME, + sElapsedRealtimeClock.millis()); + + for (int i = 0; i < mForegroundUids.size(); ++i) { + proto.write(StateControllerProto.QuotaController.FOREGROUND_UIDS, + mForegroundUids.keyAt(i)); + } + + for (int i = 0; i < mUidToPackageCache.size(); ++i) { + final long upToken = proto.start( + StateControllerProto.QuotaController.UID_TO_PACKAGE_CACHE); + + final int uid = mUidToPackageCache.keyAt(i); + ArraySet<String> packages = mUidToPackageCache.get(uid); + + proto.write(StateControllerProto.QuotaController.UidPackageMapping.UID, uid); + for (int j = 0; j < packages.size(); ++j) { + proto.write(StateControllerProto.QuotaController.UidPackageMapping.PACKAGE_NAMES, + packages.valueAt(j)); + } + + proto.end(upToken); + } + + mTrackedJobs.forEach((jobs) -> { + for (int j = 0; j < jobs.size(); j++) { + final JobStatus js = jobs.valueAt(j); + if (!predicate.test(js)) { + continue; + } + final long jsToken = proto.start(StateControllerProto.QuotaController.TRACKED_JOBS); + js.writeToShortProto(proto, StateControllerProto.QuotaController.TrackedJob.INFO); + proto.write(StateControllerProto.QuotaController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + proto.write( + StateControllerProto.QuotaController.TrackedJob.EFFECTIVE_STANDBY_BUCKET, + js.getEffectiveStandbyBucket()); + proto.write(StateControllerProto.QuotaController.TrackedJob.IS_TOP_STARTED_JOB, + mTopStartedJobs.contains(js)); + proto.write(StateControllerProto.QuotaController.TrackedJob.HAS_QUOTA, + js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + proto.write(StateControllerProto.QuotaController.TrackedJob.REMAINING_QUOTA_MS, + getRemainingExecutionTimeLocked(js)); + proto.end(jsToken); + } + }); + + for (int u = 0; u < mPkgTimers.numMaps(); ++u) { + final int userId = mPkgTimers.keyAt(u); + for (int p = 0; p < mPkgTimers.numElementsForKey(userId); ++p) { + final String pkgName = mPkgTimers.keyAt(u, p); + final long psToken = proto.start( + StateControllerProto.QuotaController.PACKAGE_STATS); + mPkgTimers.valueAt(u, p).dump(proto, + StateControllerProto.QuotaController.PackageStats.TIMER, predicate); + + List<TimingSession> sessions = mTimingSessions.get(userId, pkgName); + if (sessions != null) { + for (int j = sessions.size() - 1; j >= 0; j--) { + TimingSession session = sessions.get(j); + session.dump(proto, + StateControllerProto.QuotaController.PackageStats.SAVED_SESSIONS); + } + } + + ExecutionStats[] stats = mExecutionStatsCache.get(userId, pkgName); + if (stats != null) { + for (int bucketIndex = 0; bucketIndex < stats.length; ++bucketIndex) { + ExecutionStats es = stats[bucketIndex]; + if (es == null) { + continue; + } + final long esToken = proto.start( + StateControllerProto.QuotaController.PackageStats.EXECUTION_STATS); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.STANDBY_BUCKET, + bucketIndex); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.EXPIRATION_TIME_ELAPSED, + es.expirationTimeElapsed); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.WINDOW_SIZE_MS, + es.windowSizeMs); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_LIMIT, + es.jobCountLimit); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_LIMIT, + es.sessionCountLimit); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_WINDOW_MS, + es.executionTimeInWindowMs); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_WINDOW, + es.bgJobCountInWindow); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_MAX_PERIOD_MS, + es.executionTimeInMaxPeriodMs); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_MAX_PERIOD, + es.bgJobCountInMaxPeriod); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_WINDOW, + es.sessionCountInWindow); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.IN_QUOTA_TIME_ELAPSED, + es.inQuotaTimeElapsed); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_EXPIRATION_TIME_ELAPSED, + es.jobRateLimitExpirationTimeElapsed); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_RATE_LIMITING_WINDOW, + es.jobCountInRateLimitingWindow); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_EXPIRATION_TIME_ELAPSED, + es.sessionRateLimitExpirationTimeElapsed); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_RATE_LIMITING_WINDOW, + es.sessionCountInRateLimitingWindow); + proto.end(esToken); + } + } + + proto.end(psToken); + } + } + + mInQuotaAlarmListener.dumpLocked(proto, + StateControllerProto.QuotaController.IN_QUOTA_ALARM_LISTENER); + + proto.end(mToken); + proto.end(token); + } + + @Override + public void dumpConstants(IndentingPrintWriter pw) { + mQcConstants.dump(pw); + } + + @Override + public void dumpConstants(ProtoOutputStream proto) { + mQcConstants.dump(proto); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/RestrictingController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/RestrictingController.java new file mode 100644 index 000000000000..5c637bb92ccc --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/RestrictingController.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.job.controllers; + +import com.android.server.job.JobSchedulerService; + +/** + * Controller that can also handle jobs in the + * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket. + */ +public abstract class RestrictingController extends StateController { + RestrictingController(JobSchedulerService service) { + super(service); + } + + /** + * Start tracking a job that has been added to the + * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket. + */ + public abstract void startTrackingRestrictedJobLocked(JobStatus jobStatus); + + /** + * Stop tracking a job that has been removed from the + * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket. + */ + public abstract void stopTrackingRestrictedJobLocked(JobStatus jobStatus); +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java new file mode 100644 index 000000000000..51be38be990d --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers; + +import static com.android.server.job.JobSchedulerService.DEBUG; + +import android.content.Context; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobSchedulerService.Constants; +import com.android.server.job.StateChangedListener; + +import java.util.function.Predicate; + +/** + * Incorporates shared controller logic between the various controllers of the JobManager. + * These are solely responsible for tracking a list of jobs, and notifying the JM when these + * are ready to run, or whether they must be stopped. + */ +public abstract class StateController { + private static final String TAG = "JobScheduler.SC"; + + protected final JobSchedulerService mService; + protected final StateChangedListener mStateChangedListener; + protected final Context mContext; + protected final Object mLock; + protected final Constants mConstants; + + StateController(JobSchedulerService service) { + mService = service; + mStateChangedListener = service; + mContext = service.getTestableContext(); + mLock = service.getLock(); + mConstants = service.getConstants(); + } + + /** + * Called when the system boot phase has reached + * {@link com.android.server.SystemService#PHASE_SYSTEM_SERVICES_READY}. + */ + public void onSystemServicesReady() { + } + + /** + * Implement the logic here to decide whether a job should be tracked by this controller. + * This logic is put here so the JobManager can be completely agnostic of Controller logic. + * Also called when updating a task, so implementing controllers have to be aware of + * preexisting tasks. + */ + public abstract void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob); + + /** + * Optionally implement logic here to prepare the job to be executed. + */ + public void prepareForExecutionLocked(JobStatus jobStatus) { + } + + /** + * Remove task - this will happen if the task is cancelled, completed, etc. + */ + public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate); + + /** + * Called when a new job is being created to reschedule an old failed job. + */ + public void rescheduleForFailureLocked(JobStatus newJob, JobStatus failureToReschedule) { + } + + /** + * Called when the JobScheduler.Constants are updated. + */ + public void onConstantsUpdatedLocked() { + } + + /** Called when a package is uninstalled from the device (not for an update). */ + public void onAppRemovedLocked(String packageName, int uid) { + } + + /** Called when a user is removed from the device. */ + public void onUserRemovedLocked(int userId) { + } + + /** + * Called when JobSchedulerService has determined that the job is not ready to be run. The + * Controller can evaluate if it can or should do something to promote this job's readiness. + */ + public void evaluateStateLocked(JobStatus jobStatus) { + } + + /** + * Called when something with the UID has changed. The controller should re-evaluate any + * internal state tracking dependent on this UID. + */ + public void reevaluateStateLocked(int uid) { + } + + protected boolean wouldBeReadyWithConstraintLocked(JobStatus jobStatus, int constraint) { + // This is very cheap to check (just a few conditions on data in JobStatus). + final boolean jobWouldBeReady = jobStatus.wouldBeReadyWithConstraint(constraint); + if (DEBUG) { + Slog.v(TAG, "wouldBeReadyWithConstraintLocked: " + jobStatus.toShortString() + + " constraint=" + constraint + + " readyWithConstraint=" + jobWouldBeReady); + } + if (!jobWouldBeReady) { + // If the job wouldn't be ready, nothing to do here. + return false; + } + + // This is potentially more expensive since JSS may have to query component + // presence. + return mService.areComponentsInPlaceLocked(jobStatus); + } + + public abstract void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate); + public abstract void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate); + + /** Dump any internal constants the Controller may have. */ + public void dumpConstants(IndentingPrintWriter pw) { + } + + /** Dump any internal constants the Controller may have. */ + public void dumpConstants(ProtoOutputStream proto) { + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java new file mode 100644 index 000000000000..51187dff4d59 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; +import com.android.server.storage.DeviceStorageMonitorService; + +import java.util.function.Predicate; + +/** + * Simple controller that tracks the status of the device's storage. + */ +public final class StorageController extends StateController { + private static final String TAG = "JobScheduler.Storage"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<JobStatus>(); + private final StorageTracker mStorageTracker; + + @VisibleForTesting + public StorageTracker getTracker() { + return mStorageTracker; + } + + public StorageController(JobSchedulerService service) { + super(service); + mStorageTracker = new StorageTracker(); + mStorageTracker.startTracking(); + } + + @Override + public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { + if (taskStatus.hasStorageNotLowConstraint()) { + mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_STORAGE); + taskStatus.setStorageNotLowConstraintSatisfied(mStorageTracker.isStorageNotLow()); + } + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, + boolean forUpdate) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_STORAGE)) { + mTrackedTasks.remove(taskStatus); + } + } + + private void maybeReportNewStorageState() { + final boolean storageNotLow = mStorageTracker.isStorageNotLow(); + boolean reportChange = false; + synchronized (mLock) { + for (int i = mTrackedTasks.size() - 1; i >= 0; i--) { + final JobStatus ts = mTrackedTasks.valueAt(i); + reportChange |= ts.setStorageNotLowConstraintSatisfied(storageNotLow); + } + } + if (storageNotLow) { + // Tell the scheduler that any ready jobs should be flushed. + mStateChangedListener.onRunJobNow(null); + } else if (reportChange) { + // Let the scheduler know that state has changed. This may or may not result in an + // execution. + mStateChangedListener.onControllerStateChanged(); + } + } + + public final class StorageTracker extends BroadcastReceiver { + /** + * Track whether storage is low. + */ + private boolean mStorageLow; + /** Sequence number of last broadcast. */ + private int mLastStorageSeq = -1; + + public StorageTracker() { + } + + public void startTracking() { + IntentFilter filter = new IntentFilter(); + + // Storage status. Just need to register, since STORAGE_LOW is a sticky + // broadcast we will receive that if it is currently active. + filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW); + filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); + mContext.registerReceiver(this, filter); + } + + public boolean isStorageNotLow() { + return !mStorageLow; + } + + public int getSeq() { + return mLastStorageSeq; + } + + @Override + public void onReceive(Context context, Intent intent) { + onReceiveInternal(intent); + } + + @VisibleForTesting + public void onReceiveInternal(Intent intent) { + final String action = intent.getAction(); + mLastStorageSeq = intent.getIntExtra(DeviceStorageMonitorService.EXTRA_SEQUENCE, + mLastStorageSeq); + if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Available storage too low to do work. @ " + + sElapsedRealtimeClock.millis()); + } + mStorageLow = true; + maybeReportNewStorageState(); + } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Available storage high enough to do work. @ " + + sElapsedRealtimeClock.millis()); + } + mStorageLow = false; + maybeReportNewStorageState(); + } + } + } + + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + pw.println("Not low: " + mStorageTracker.isStorageNotLow()); + pw.println("Sequence: " + mStorageTracker.getSeq()); + pw.println(); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + pw.println(); + } + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.STORAGE); + + proto.write(StateControllerProto.StorageController.IS_STORAGE_NOT_LOW, + mStorageTracker.isStorageNotLow()); + proto.write(StateControllerProto.StorageController.LAST_BROADCAST_SEQUENCE_NUMBER, + mStorageTracker.getSeq()); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + final long jsToken = proto.start(StateControllerProto.StorageController.TRACKED_JOBS); + js.writeToShortProto(proto, StateControllerProto.StorageController.TrackedJob.INFO); + proto.write(StateControllerProto.StorageController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + proto.end(jsToken); + } + + proto.end(mToken); + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java new file mode 100644 index 000000000000..1bb9e967c025 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java @@ -0,0 +1,604 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AlarmManager; +import android.app.AlarmManager.OnAlarmListener; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Process; +import android.os.UserHandle; +import android.os.WorkSource; +import android.provider.Settings; +import android.util.KeyValueListParser; +import android.util.Log; +import android.util.Slog; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.ConstantsProto; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.function.Predicate; + +/** + * This class sets an alarm for the next expiring job, and determines whether a job's minimum + * delay has been satisfied. + */ +public final class TimeController extends StateController { + private static final String TAG = "JobScheduler.Time"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + /** Deadline alarm tag for logging purposes */ + private final String DEADLINE_TAG = "*job.deadline*"; + /** Delay alarm tag for logging purposes */ + private final String DELAY_TAG = "*job.delay*"; + + private final Handler mHandler; + private final TcConstants mTcConstants; + + private long mNextJobExpiredElapsedMillis; + private long mNextDelayExpiredElapsedMillis; + + private final boolean mChainedAttributionEnabled; + + private AlarmManager mAlarmService = null; + /** List of tracked jobs, sorted asc. by deadline */ + private final List<JobStatus> mTrackedJobs = new LinkedList<>(); + + public TimeController(JobSchedulerService service) { + super(service); + + mNextJobExpiredElapsedMillis = Long.MAX_VALUE; + mNextDelayExpiredElapsedMillis = Long.MAX_VALUE; + mChainedAttributionEnabled = mService.isChainedAttributionEnabled(); + + mHandler = new Handler(mContext.getMainLooper()); + mTcConstants = new TcConstants(mHandler); + } + + @Override + public void onSystemServicesReady() { + mTcConstants.start(mContext.getContentResolver()); + } + + /** + * Check if the job has a timing constraint, and if so determine where to insert it in our + * list. + */ + @Override + public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) { + if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) { + maybeStopTrackingJobLocked(job, null, false); + + // First: check the constraints now, because if they are already satisfied + // then there is no need to track it. This gives us a fast path for a common + // pattern of having a job with a 0 deadline constraint ("run immediately"). + // Unlike most controllers, once one of our constraints has been satisfied, it + // will never be unsatisfied (our time base can not go backwards). + final long nowElapsedMillis = sElapsedRealtimeClock.millis(); + if (job.hasDeadlineConstraint() && evaluateDeadlineConstraint(job, nowElapsedMillis)) { + return; + } else if (job.hasTimingDelayConstraint() && evaluateTimingDelayConstraint(job, + nowElapsedMillis)) { + if (!job.hasDeadlineConstraint()) { + // If it doesn't have a deadline, we'll never have to touch it again. + return; + } + } + + boolean isInsert = false; + ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size()); + while (it.hasPrevious()) { + JobStatus ts = it.previous(); + if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) { + // Insert + isInsert = true; + break; + } + } + if (isInsert) { + it.next(); + } + it.add(job); + + job.setTrackingController(JobStatus.TRACKING_TIME); + WorkSource ws = deriveWorkSource(job.getSourceUid(), job.getSourcePackageName()); + + // Only update alarms if the job would be ready with the relevant timing constraint + // satisfied. + if (job.hasTimingDelayConstraint() + && wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) { + maybeUpdateDelayAlarmLocked(job.getEarliestRunTime(), ws); + } + if (job.hasDeadlineConstraint() + && wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_DEADLINE)) { + maybeUpdateDeadlineAlarmLocked(job.getLatestRunTimeElapsed(), ws); + } + } + } + + /** + * When we stop tracking a job, we only need to update our alarms if the job we're no longer + * tracking was the one our alarms were based off of. + */ + @Override + public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob, + boolean forUpdate) { + if (job.clearTrackingController(JobStatus.TRACKING_TIME)) { + if (mTrackedJobs.remove(job)) { + checkExpiredDelaysAndResetAlarm(); + checkExpiredDeadlinesAndResetAlarm(); + } + } + } + + @Override + public void evaluateStateLocked(JobStatus job) { + final long nowElapsedMillis = sElapsedRealtimeClock.millis(); + + // Check deadline constraint first because if it's satisfied, we avoid a little bit of + // unnecessary processing of the timing delay. + if (job.hasDeadlineConstraint() + && !job.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE) + && job.getLatestRunTimeElapsed() <= mNextJobExpiredElapsedMillis) { + if (evaluateDeadlineConstraint(job, nowElapsedMillis)) { + checkExpiredDeadlinesAndResetAlarm(); + checkExpiredDelaysAndResetAlarm(); + } else { + final boolean isAlarmForJob = + job.getLatestRunTimeElapsed() == mNextJobExpiredElapsedMillis; + final boolean wouldBeReady = wouldBeReadyWithConstraintLocked( + job, JobStatus.CONSTRAINT_DEADLINE); + if ((isAlarmForJob && !wouldBeReady) || (!isAlarmForJob && wouldBeReady)) { + checkExpiredDeadlinesAndResetAlarm(); + } + } + } + if (job.hasTimingDelayConstraint() + && !job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY) + && job.getEarliestRunTime() <= mNextDelayExpiredElapsedMillis) { + if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) { + checkExpiredDelaysAndResetAlarm(); + } else { + final boolean isAlarmForJob = + job.getEarliestRunTime() == mNextDelayExpiredElapsedMillis; + final boolean wouldBeReady = wouldBeReadyWithConstraintLocked( + job, JobStatus.CONSTRAINT_TIMING_DELAY); + if ((isAlarmForJob && !wouldBeReady) || (!isAlarmForJob && wouldBeReady)) { + checkExpiredDelaysAndResetAlarm(); + } + } + } + } + + @Override + public void reevaluateStateLocked(int uid) { + checkExpiredDeadlinesAndResetAlarm(); + checkExpiredDelaysAndResetAlarm(); + } + + /** + * Determines whether this controller can stop tracking the given job. + * The controller is no longer interested in a job once its time constraint is satisfied, and + * the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle + * back and forth. + */ + private boolean canStopTrackingJobLocked(JobStatus job) { + return (!job.hasTimingDelayConstraint() + || job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) + && (!job.hasDeadlineConstraint() + || job.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE)); + } + + private void ensureAlarmServiceLocked() { + if (mAlarmService == null) { + mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + } + } + + /** + * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler + * if so, removing them from this list, and updating the alarm for the next expiry time. + */ + @VisibleForTesting + void checkExpiredDeadlinesAndResetAlarm() { + synchronized (mLock) { + long nextExpiryTime = Long.MAX_VALUE; + int nextExpiryUid = 0; + String nextExpiryPackageName = null; + final long nowElapsedMillis = sElapsedRealtimeClock.millis(); + + ListIterator<JobStatus> it = mTrackedJobs.listIterator(); + while (it.hasNext()) { + JobStatus job = it.next(); + if (!job.hasDeadlineConstraint()) { + continue; + } + + if (evaluateDeadlineConstraint(job, nowElapsedMillis)) { + if (job.isReady()) { + // If the job still isn't ready, there's no point trying to rush the + // Scheduler. + mStateChangedListener.onRunJobNow(job); + } + it.remove(); + } else { // Sorted by expiry time, so take the next one and stop. + if (!wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_DEADLINE)) { + if (DEBUG) { + Slog.i(TAG, + "Skipping " + job + " because deadline won't make it ready."); + } + continue; + } + nextExpiryTime = job.getLatestRunTimeElapsed(); + nextExpiryUid = job.getSourceUid(); + nextExpiryPackageName = job.getSourcePackageName(); + break; + } + } + setDeadlineExpiredAlarmLocked(nextExpiryTime, + deriveWorkSource(nextExpiryUid, nextExpiryPackageName)); + } + } + + /** @return true if the job's deadline constraint is satisfied */ + private boolean evaluateDeadlineConstraint(JobStatus job, long nowElapsedMillis) { + final long jobDeadline = job.getLatestRunTimeElapsed(); + + if (jobDeadline <= nowElapsedMillis) { + if (job.hasTimingDelayConstraint()) { + job.setTimingDelayConstraintSatisfied(true); + } + job.setDeadlineConstraintSatisfied(true); + return true; + } + return false; + } + + /** + * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of + * tracked jobs and marks them as ready as appropriate. + */ + @VisibleForTesting + void checkExpiredDelaysAndResetAlarm() { + synchronized (mLock) { + final long nowElapsedMillis = sElapsedRealtimeClock.millis(); + long nextDelayTime = Long.MAX_VALUE; + int nextDelayUid = 0; + String nextDelayPackageName = null; + boolean ready = false; + Iterator<JobStatus> it = mTrackedJobs.iterator(); + while (it.hasNext()) { + final JobStatus job = it.next(); + if (!job.hasTimingDelayConstraint()) { + continue; + } + if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) { + if (canStopTrackingJobLocked(job)) { + it.remove(); + } + if (job.isReady()) { + ready = true; + } + } else { + if (!wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) { + if (DEBUG) { + Slog.i(TAG, "Skipping " + job + " because delay won't make it ready."); + } + continue; + } + // If this job still doesn't have its delay constraint satisfied, + // then see if it is the next upcoming delay time for the alarm. + final long jobDelayTime = job.getEarliestRunTime(); + if (nextDelayTime > jobDelayTime) { + nextDelayTime = jobDelayTime; + nextDelayUid = job.getSourceUid(); + nextDelayPackageName = job.getSourcePackageName(); + } + } + } + if (ready) { + mStateChangedListener.onControllerStateChanged(); + } + setDelayExpiredAlarmLocked(nextDelayTime, + deriveWorkSource(nextDelayUid, nextDelayPackageName)); + } + } + + private WorkSource deriveWorkSource(int uid, @Nullable String packageName) { + if (mChainedAttributionEnabled) { + WorkSource ws = new WorkSource(); + ws.createWorkChain() + .addNode(uid, packageName) + .addNode(Process.SYSTEM_UID, "JobScheduler"); + return ws; + } else { + return packageName == null ? new WorkSource(uid) : new WorkSource(uid, packageName); + } + } + + /** @return true if the job's delay constraint is satisfied */ + private boolean evaluateTimingDelayConstraint(JobStatus job, long nowElapsedMillis) { + final long jobDelayTime = job.getEarliestRunTime(); + if (jobDelayTime <= nowElapsedMillis) { + job.setTimingDelayConstraintSatisfied(true); + return true; + } + return false; + } + + private void maybeUpdateDelayAlarmLocked(long delayExpiredElapsed, WorkSource ws) { + if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) { + setDelayExpiredAlarmLocked(delayExpiredElapsed, ws); + } + } + + private void maybeUpdateDeadlineAlarmLocked(long deadlineExpiredElapsed, WorkSource ws) { + if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) { + setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, ws); + } + } + + /** + * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's + * delay will expire. + * This alarm <b>will not</b> wake up the phone if + * {@link TcConstants#USE_NON_WAKEUP_ALARM_FOR_DELAY} is true. + */ + private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) { + alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); + if (mNextDelayExpiredElapsedMillis == alarmTimeElapsedMillis) { + return; + } + mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis; + final int alarmType = + mTcConstants.USE_NON_WAKEUP_ALARM_FOR_DELAY + ? AlarmManager.ELAPSED_REALTIME : AlarmManager.ELAPSED_REALTIME_WAKEUP; + updateAlarmWithListenerLocked(DELAY_TAG, alarmType, + mNextDelayExpiredListener, mNextDelayExpiredElapsedMillis, ws); + } + + /** + * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's + * deadline will expire. + * This alarm <b>will</b> wake up the phone. + */ + private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) { + alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); + if (mNextJobExpiredElapsedMillis == alarmTimeElapsedMillis) { + return; + } + mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis; + updateAlarmWithListenerLocked(DEADLINE_TAG, AlarmManager.ELAPSED_REALTIME_WAKEUP, + mDeadlineExpiredListener, mNextJobExpiredElapsedMillis, ws); + } + + private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) { + return Math.max(proposedAlarmTimeElapsedMillis, sElapsedRealtimeClock.millis()); + } + + private void updateAlarmWithListenerLocked(String tag, @AlarmManager.AlarmType int alarmType, + OnAlarmListener listener, long alarmTimeElapsed, WorkSource ws) { + ensureAlarmServiceLocked(); + if (alarmTimeElapsed == Long.MAX_VALUE) { + mAlarmService.cancel(listener); + } else { + if (DEBUG) { + Slog.d(TAG, "Setting " + tag + " for: " + alarmTimeElapsed); + } + mAlarmService.set(alarmType, alarmTimeElapsed, + AlarmManager.WINDOW_HEURISTIC, 0, tag, listener, null, ws); + } + } + + // Job/delay expiration alarm handling + + private final OnAlarmListener mDeadlineExpiredListener = new OnAlarmListener() { + @Override + public void onAlarm() { + if (DEBUG) { + Slog.d(TAG, "Deadline-expired alarm fired"); + } + checkExpiredDeadlinesAndResetAlarm(); + } + }; + + private final OnAlarmListener mNextDelayExpiredListener = new OnAlarmListener() { + @Override + public void onAlarm() { + if (DEBUG) { + Slog.d(TAG, "Delay-expired alarm fired"); + } + checkExpiredDelaysAndResetAlarm(); + } + }; + + @VisibleForTesting + class TcConstants extends ContentObserver { + private ContentResolver mResolver; + private final KeyValueListParser mParser = new KeyValueListParser(','); + + private static final String KEY_USE_NON_WAKEUP_ALARM_FOR_DELAY = + "use_non_wakeup_delay_alarm"; + + private static final boolean DEFAULT_USE_NON_WAKEUP_ALARM_FOR_DELAY = true; + + /** + * Whether or not TimeController should skip setting wakeup alarms for jobs that aren't + * ready now. + */ + public boolean USE_NON_WAKEUP_ALARM_FOR_DELAY = DEFAULT_USE_NON_WAKEUP_ALARM_FOR_DELAY; + + /** + * Creates a content observer. + * + * @param handler The handler to run {@link #onChange} on, or null if none. + */ + TcConstants(Handler handler) { + super(handler); + } + + private void start(ContentResolver resolver) { + mResolver = resolver; + mResolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.JOB_SCHEDULER_TIME_CONTROLLER_CONSTANTS), false, this); + onChange(true, null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + final String constants = Settings.Global.getString( + mResolver, Settings.Global.JOB_SCHEDULER_TIME_CONTROLLER_CONSTANTS); + + try { + mParser.setString(constants); + } catch (Exception e) { + // Failed to parse the settings string, log this and move on with defaults. + Slog.e(TAG, "Bad jobscheduler time controller settings", e); + } + + USE_NON_WAKEUP_ALARM_FOR_DELAY = mParser.getBoolean( + KEY_USE_NON_WAKEUP_ALARM_FOR_DELAY, DEFAULT_USE_NON_WAKEUP_ALARM_FOR_DELAY); + // Intentionally not calling checkExpiredDelaysAndResetAlarm() here. There's no need to + // iterate through the entire list again for this constant change. The next delay alarm + // that is set will make use of the new constant value. + } + + private void dump(IndentingPrintWriter pw) { + pw.println(); + pw.println("TimeController:"); + pw.increaseIndent(); + pw.printPair(KEY_USE_NON_WAKEUP_ALARM_FOR_DELAY, + USE_NON_WAKEUP_ALARM_FOR_DELAY).println(); + pw.decreaseIndent(); + } + + private void dump(ProtoOutputStream proto) { + final long tcToken = proto.start(ConstantsProto.TIME_CONTROLLER); + proto.write(ConstantsProto.TimeController.USE_NON_WAKEUP_ALARM_FOR_DELAY, + USE_NON_WAKEUP_ALARM_FOR_DELAY); + proto.end(tcToken); + } + } + + @VisibleForTesting + @NonNull + TcConstants getTcConstants() { + return mTcConstants; + } + + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + final long nowElapsed = sElapsedRealtimeClock.millis(); + pw.println("Elapsed clock: " + nowElapsed); + + pw.print("Next delay alarm in "); + TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw); + pw.println(); + pw.print("Next deadline alarm in "); + TimeUtils.formatDuration(mNextJobExpiredElapsedMillis, nowElapsed, pw); + pw.println(); + pw.println(); + + for (JobStatus ts : mTrackedJobs) { + if (!predicate.test(ts)) { + continue; + } + pw.print("#"); + ts.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, ts.getSourceUid()); + pw.print(": Delay="); + if (ts.hasTimingDelayConstraint()) { + TimeUtils.formatDuration(ts.getEarliestRunTime(), nowElapsed, pw); + } else { + pw.print("N/A"); + } + pw.print(", Deadline="); + if (ts.hasDeadlineConstraint()) { + TimeUtils.formatDuration(ts.getLatestRunTimeElapsed(), nowElapsed, pw); + } else { + pw.print("N/A"); + } + pw.println(); + } + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.TIME); + + final long nowElapsed = sElapsedRealtimeClock.millis(); + proto.write(StateControllerProto.TimeController.NOW_ELAPSED_REALTIME, nowElapsed); + proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DELAY_ALARM_MS, + mNextDelayExpiredElapsedMillis - nowElapsed); + proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DEADLINE_ALARM_MS, + mNextJobExpiredElapsedMillis - nowElapsed); + + for (JobStatus ts : mTrackedJobs) { + if (!predicate.test(ts)) { + continue; + } + final long tsToken = proto.start(StateControllerProto.TimeController.TRACKED_JOBS); + ts.writeToShortProto(proto, StateControllerProto.TimeController.TrackedJob.INFO); + + proto.write(StateControllerProto.TimeController.TrackedJob.HAS_TIMING_DELAY_CONSTRAINT, + ts.hasTimingDelayConstraint()); + proto.write(StateControllerProto.TimeController.TrackedJob.DELAY_TIME_REMAINING_MS, + ts.getEarliestRunTime() - nowElapsed); + + proto.write(StateControllerProto.TimeController.TrackedJob.HAS_DEADLINE_CONSTRAINT, + ts.hasDeadlineConstraint()); + proto.write(StateControllerProto.TimeController.TrackedJob.TIME_REMAINING_UNTIL_DEADLINE_MS, + ts.getLatestRunTimeElapsed() - nowElapsed); + + proto.end(tsToken); + } + + proto.end(mToken); + proto.end(token); + } + + @Override + public void dumpConstants(IndentingPrintWriter pw) { + mTcConstants.dump(pw); + } + + @Override + public void dumpConstants(ProtoOutputStream proto) { + mTcConstants.dump(proto); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java new file mode 100644 index 000000000000..1e5b84d55a02 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers.idle; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.server.am.ActivityManagerService; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; + +import java.io.PrintWriter; + +public final class CarIdlenessTracker extends BroadcastReceiver implements IdlenessTracker { + private static final String TAG = "JobScheduler.CarIdlenessTracker"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + public static final String ACTION_GARAGE_MODE_ON = + "com.android.server.jobscheduler.GARAGE_MODE_ON"; + public static final String ACTION_GARAGE_MODE_OFF = + "com.android.server.jobscheduler.GARAGE_MODE_OFF"; + + public static final String ACTION_FORCE_IDLE = "com.android.server.jobscheduler.FORCE_IDLE"; + public static final String ACTION_UNFORCE_IDLE = "com.android.server.jobscheduler.UNFORCE_IDLE"; + + // After construction, mutations of idle/screen-on state will only happen + // on the main looper thread, either in onReceive() or in an alarm callback. + private boolean mIdle; + private boolean mGarageModeOn; + private boolean mForced; + private IdlenessListener mIdleListener; + + public CarIdlenessTracker() { + // At boot we presume that the user has just "interacted" with the + // device in some meaningful way. + mIdle = false; + mGarageModeOn = false; + mForced = false; + } + + @Override + public boolean isIdle() { + return mIdle; + } + + @Override + public void startTracking(Context context, IdlenessListener listener) { + mIdleListener = listener; + + IntentFilter filter = new IntentFilter(); + + // Screen state + filter.addAction(Intent.ACTION_SCREEN_ON); + + // State of GarageMode + filter.addAction(ACTION_GARAGE_MODE_ON); + filter.addAction(ACTION_GARAGE_MODE_OFF); + + // Debugging/instrumentation + filter.addAction(ACTION_FORCE_IDLE); + filter.addAction(ACTION_UNFORCE_IDLE); + filter.addAction(ActivityManagerService.ACTION_TRIGGER_IDLE); + + context.registerReceiver(this, filter); + } + + @Override + public void dump(PrintWriter pw) { + pw.print(" mIdle: "); pw.println(mIdle); + pw.print(" mGarageModeOn: "); pw.println(mGarageModeOn); + } + + @Override + public void dump(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + final long ciToken = proto.start( + StateControllerProto.IdleController.IdlenessTracker.CAR_IDLENESS_TRACKER); + + proto.write(StateControllerProto.IdleController.IdlenessTracker.CarIdlenessTracker.IS_IDLE, + mIdle); + proto.write( + StateControllerProto.IdleController.IdlenessTracker.CarIdlenessTracker.IS_GARAGE_MODE_ON, + mGarageModeOn); + + proto.end(ciToken); + proto.end(token); + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + logIfDebug("Received action: " + action); + + // Check for forced actions + if (action.equals(ACTION_FORCE_IDLE)) { + logIfDebug("Forcing idle..."); + setForceIdleState(true); + } else if (action.equals(ACTION_UNFORCE_IDLE)) { + logIfDebug("Unforcing idle..."); + setForceIdleState(false); + } else if (action.equals(Intent.ACTION_SCREEN_ON)) { + logIfDebug("Screen is on..."); + handleScreenOn(); + } else if (action.equals(ACTION_GARAGE_MODE_ON)) { + logIfDebug("GarageMode is on..."); + mGarageModeOn = true; + updateIdlenessState(); + } else if (action.equals(ACTION_GARAGE_MODE_OFF)) { + logIfDebug("GarageMode is off..."); + mGarageModeOn = false; + updateIdlenessState(); + } else if (action.equals(ActivityManagerService.ACTION_TRIGGER_IDLE)) { + if (!mGarageModeOn) { + logIfDebug("Idle trigger fired..."); + triggerIdlenessOnce(); + } else { + logIfDebug("TRIGGER_IDLE received but not changing state; idle=" + + mIdle + " screen=" + mGarageModeOn); + } + } + } + + private void setForceIdleState(boolean forced) { + mForced = forced; + updateIdlenessState(); + } + + private void updateIdlenessState() { + final boolean newState = (mForced || mGarageModeOn); + if (mIdle != newState) { + // State of idleness changed. Notifying idleness controller + logIfDebug("Device idleness changed. New idle=" + newState); + mIdle = newState; + mIdleListener.reportNewIdleState(mIdle); + } else { + // Nothing changed, device idleness is in the same state as new state + logIfDebug("Device idleness is the same. Current idle=" + newState); + } + } + + private void triggerIdlenessOnce() { + // This is simply triggering idleness once until some constraint will switch it back off + if (mIdle) { + // Already in idle state. Nothing to do + logIfDebug("Device is already idle"); + } else { + // Going idle once + logIfDebug("Device is going idle once"); + mIdle = true; + mIdleListener.reportNewIdleState(mIdle); + } + } + + private void handleScreenOn() { + if (mForced || mGarageModeOn) { + // Even though screen is on, the device remains idle + logIfDebug("Screen is on, but device cannot exit idle"); + } else if (mIdle) { + // Exiting idle + logIfDebug("Device is exiting idle"); + mIdle = false; + } else { + // Already in non-idle state. Nothing to do + logIfDebug("Device is already non-idle"); + } + } + + private static void logIfDebug(String msg) { + if (DEBUG) { + Slog.v(TAG, msg); + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java new file mode 100644 index 000000000000..e2c8f649fdb7 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers.idle; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.app.AlarmManager; +import android.app.UiModeManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.PowerManager; +import android.util.Log; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.server.am.ActivityManagerService; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; + +import java.io.PrintWriter; + +public final class DeviceIdlenessTracker extends BroadcastReceiver implements IdlenessTracker { + private static final String TAG = "JobScheduler.DeviceIdlenessTracker"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private AlarmManager mAlarm; + private PowerManager mPowerManager; + + // After construction, mutations of idle/screen-on state will only happen + // on the main looper thread, either in onReceive() or in an alarm callback. + private long mInactivityIdleThreshold; + private long mIdleWindowSlop; + private boolean mIdle; + private boolean mScreenOn; + private boolean mDockIdle; + private boolean mInCarMode; + private IdlenessListener mIdleListener; + + private AlarmManager.OnAlarmListener mIdleAlarmListener = () -> { + handleIdleTrigger(); + }; + + public DeviceIdlenessTracker() { + // At boot we presume that the user has just "interacted" with the + // device in some meaningful way. + mIdle = false; + mScreenOn = true; + mDockIdle = false; + mInCarMode = false; + } + + @Override + public boolean isIdle() { + return mIdle; + } + + @Override + public void startTracking(Context context, IdlenessListener listener) { + mIdleListener = listener; + mInactivityIdleThreshold = context.getResources().getInteger( + com.android.internal.R.integer.config_jobSchedulerInactivityIdleThreshold); + mIdleWindowSlop = context.getResources().getInteger( + com.android.internal.R.integer.config_jobSchedulerIdleWindowSlop); + mAlarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + mPowerManager = context.getSystemService(PowerManager.class); + + IntentFilter filter = new IntentFilter(); + + // Screen state + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + + // Dreaming state + filter.addAction(Intent.ACTION_DREAMING_STARTED); + filter.addAction(Intent.ACTION_DREAMING_STOPPED); + + // Debugging/instrumentation + filter.addAction(ActivityManagerService.ACTION_TRIGGER_IDLE); + + // Wireless charging dock state + filter.addAction(Intent.ACTION_DOCK_IDLE); + filter.addAction(Intent.ACTION_DOCK_ACTIVE); + + // Car mode + filter.addAction(UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED); + filter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED); + + context.registerReceiver(this, filter); + } + + @Override + public void dump(PrintWriter pw) { + pw.print(" mIdle: "); pw.println(mIdle); + pw.print(" mScreenOn: "); pw.println(mScreenOn); + pw.print(" mDockIdle: "); pw.println(mDockIdle); + pw.print(" mInCarMode: "); + pw.println(mInCarMode); + } + + @Override + public void dump(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + final long diToken = proto.start( + StateControllerProto.IdleController.IdlenessTracker.DEVICE_IDLENESS_TRACKER); + + proto.write(StateControllerProto.IdleController.IdlenessTracker.DeviceIdlenessTracker.IS_IDLE, + mIdle); + proto.write( + StateControllerProto.IdleController.IdlenessTracker.DeviceIdlenessTracker.IS_SCREEN_ON, + mScreenOn); + proto.write( + StateControllerProto.IdleController.IdlenessTracker.DeviceIdlenessTracker.IS_DOCK_IDLE, + mDockIdle); + proto.write( + StateControllerProto.IdleController.IdlenessTracker.DeviceIdlenessTracker.IN_CAR_MODE, + mInCarMode); + + proto.end(diToken); + proto.end(token); + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (DEBUG) { + Slog.v(TAG, "Received action: " + action); + } + switch (action) { + case Intent.ACTION_DOCK_ACTIVE: + if (!mScreenOn) { + // Ignore this intent during screen off + return; + } + // Intentional fallthrough + case Intent.ACTION_DREAMING_STOPPED: + if (!mPowerManager.isInteractive()) { + // Ignore this intent if the device isn't interactive. + return; + } + // Intentional fallthrough + case Intent.ACTION_SCREEN_ON: + mScreenOn = true; + mDockIdle = false; + if (DEBUG) { + Slog.v(TAG, "exiting idle"); + } + cancelIdlenessCheck(); + if (mIdle) { + mIdle = false; + mIdleListener.reportNewIdleState(mIdle); + } + break; + case Intent.ACTION_SCREEN_OFF: + case Intent.ACTION_DREAMING_STARTED: + case Intent.ACTION_DOCK_IDLE: + // when the screen goes off or dreaming starts or wireless charging dock in idle, + // we schedule the alarm that will tell us when we have decided the device is + // truly idle. + if (action.equals(Intent.ACTION_DOCK_IDLE)) { + if (!mScreenOn) { + // Ignore this intent during screen off + return; + } else { + mDockIdle = true; + } + } else { + mScreenOn = false; + mDockIdle = false; + } + maybeScheduleIdlenessCheck(action); + break; + case UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED: + mInCarMode = true; + cancelIdlenessCheck(); + if (mIdle) { + mIdle = false; + mIdleListener.reportNewIdleState(mIdle); + } + break; + case UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED: + mInCarMode = false; + maybeScheduleIdlenessCheck(action); + break; + case ActivityManagerService.ACTION_TRIGGER_IDLE: + handleIdleTrigger(); + break; + } + } + + private void maybeScheduleIdlenessCheck(String reason) { + if ((!mScreenOn || mDockIdle) && !mInCarMode) { + final long nowElapsed = sElapsedRealtimeClock.millis(); + final long when = nowElapsed + mInactivityIdleThreshold; + if (DEBUG) { + Slog.v(TAG, "Scheduling idle : " + reason + " now:" + nowElapsed + " when=" + when); + } + mAlarm.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP, + when, mIdleWindowSlop, "JS idleness", mIdleAlarmListener, null); + } + } + + private void cancelIdlenessCheck() { + mAlarm.cancel(mIdleAlarmListener); + } + + private void handleIdleTrigger() { + // idle time starts now. Do not set mIdle if screen is on. + if (!mIdle && (!mScreenOn || mDockIdle) && !mInCarMode) { + if (DEBUG) { + Slog.v(TAG, "Idle trigger fired @ " + sElapsedRealtimeClock.millis()); + } + mIdle = true; + mIdleListener.reportNewIdleState(mIdle); + } else { + if (DEBUG) { + Slog.v(TAG, "TRIGGER_IDLE received but not changing state; idle=" + + mIdle + " screen=" + mScreenOn + " car=" + mInCarMode); + } + } + } +}
\ No newline at end of file diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessListener.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessListener.java new file mode 100644 index 000000000000..7ffd7cd3e2e0 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessListener.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers.idle; + +/** + * Interface through which an IdlenessTracker informs the job scheduler of + * changes in the device's inactivity state. + */ +public interface IdlenessListener { + /** + * Tell the job scheduler that the device's idle state has changed. + * + * @param deviceIsIdle {@code true} to indicate that the device is now considered + * to be idle; {@code false} to indicate that the device is now being interacted with, + * so jobs with idle constraints should not be run. + */ + void reportNewIdleState(boolean deviceIsIdle); +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java new file mode 100644 index 000000000000..cdab7e538ca5 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.controllers.idle; + +import android.content.Context; +import android.util.proto.ProtoOutputStream; + +import java.io.PrintWriter; + +public interface IdlenessTracker { + /** + * One-time initialization: this method is called once, after construction of + * the IdlenessTracker instance. This is when the tracker should actually begin + * monitoring whatever signals it consumes in deciding when the device is in a + * non-interacting state. When the idle state changes thereafter, the given + * listener must be called to report the new state. + */ + void startTracking(Context context, IdlenessListener listener); + + /** + * Report whether the device is currently considered "idle" for purposes of + * running scheduled jobs with idleness constraints. + * + * @return {@code true} if the job scheduler should consider idleness + * constraints to be currently satisfied; {@code false} otherwise. + */ + boolean isIdle(); + + /** + * Dump useful information about tracked idleness-related state in plaintext. + */ + void dump(PrintWriter pw); + + /** + * Dump useful information about tracked idleness-related state to proto. + */ + void dump(ProtoOutputStream proto, long fieldId); +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java new file mode 100644 index 000000000000..e180c55e1bf2 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.restrictions; + +import android.app.job.JobInfo; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.controllers.JobStatus; + +/** + * Used by {@link JobSchedulerService} to impose additional restrictions regarding whether jobs + * should be scheduled or not based on the state of the system/device. + * Every restriction is associated with exactly one reason (from {@link + * android.app.job.JobParameters#JOB_STOP_REASON_CODES}), which could be retrieved using {@link + * #getReason()}. + * Note, that this is not taken into account for the jobs that have priority + * {@link JobInfo#PRIORITY_FOREGROUND_APP} or higher. + */ +public abstract class JobRestriction { + + final JobSchedulerService mService; + private final int mReason; + + JobRestriction(JobSchedulerService service, int reason) { + mService = service; + mReason = reason; + } + + /** + * Called when the system boot phase has reached + * {@link com.android.server.SystemService#PHASE_SYSTEM_SERVICES_READY}. + */ + public void onSystemServicesReady() { + } + + /** + * Called by {@link JobSchedulerService} to check if it may proceed with scheduling the job (in + * case all constraints are satisfied and all other {@link JobRestriction}s are fine with it) + * + * @param job to be checked + * @return false if the {@link JobSchedulerService} should not schedule this job at the moment, + * true - otherwise + */ + public abstract boolean isJobRestricted(JobStatus job); + + /** Dump any internal constants the Restriction may have. */ + public abstract void dumpConstants(IndentingPrintWriter pw); + + /** Dump any internal constants the Restriction may have. */ + public abstract void dumpConstants(ProtoOutputStream proto); + + /** @return reason code for the Restriction. */ + public final int getReason() { + return mReason; + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java new file mode 100644 index 000000000000..aa7696df6dbd --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job.restrictions; + +import android.app.job.JobParameters; +import android.os.PowerManager; +import android.os.PowerManager.OnThermalStatusChangedListener; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobSchedulerServiceDumpProto; +import com.android.server.job.controllers.JobStatus; + +public class ThermalStatusRestriction extends JobRestriction { + private static final String TAG = "ThermalStatusRestriction"; + + private volatile boolean mIsThermalRestricted = false; + + private PowerManager mPowerManager; + + public ThermalStatusRestriction(JobSchedulerService service) { + super(service, JobParameters.REASON_DEVICE_THERMAL); + } + + @Override + public void onSystemServicesReady() { + mPowerManager = mService.getContext().getSystemService(PowerManager.class); + // Use MainExecutor + mPowerManager.addThermalStatusListener(new OnThermalStatusChangedListener() { + @Override + public void onThermalStatusChanged(int status) { + // This is called on the main thread. Do not do any slow operations in it. + // mService.onControllerStateChanged() will just post a message, which is okay. + final boolean shouldBeActive = status >= PowerManager.THERMAL_STATUS_SEVERE; + if (mIsThermalRestricted == shouldBeActive) { + return; + } + mIsThermalRestricted = shouldBeActive; + mService.onControllerStateChanged(); + } + }); + } + + @Override + public boolean isJobRestricted(JobStatus job) { + return mIsThermalRestricted && job.hasConnectivityConstraint(); + } + + @Override + public void dumpConstants(IndentingPrintWriter pw) { + pw.print("In thermal throttling?: "); + pw.print(mIsThermalRestricted); + } + + @Override + public void dumpConstants(ProtoOutputStream proto) { + proto.write(JobSchedulerServiceDumpProto.IN_THERMAL, mIsThermalRestricted); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java new file mode 100644 index 000000000000..70155ee84720 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java @@ -0,0 +1,820 @@ +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.android.server.usage; + +import static android.app.usage.UsageStatsManager.REASON_MAIN_DEFAULT; +import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER; +import static android.app.usage.UsageStatsManager.REASON_MAIN_MASK; +import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED; +import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE; +import static android.app.usage.UsageStatsManager.REASON_SUB_MASK; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; + +import static com.android.server.usage.AppStandbyController.isUserUsage; + +import android.app.usage.AppStandbyInfo; +import android.app.usage.UsageStatsManager; +import android.os.SystemClock; +import android.util.ArrayMap; +import android.util.AtomicFile; +import android.util.Slog; +import android.util.SparseArray; +import android.util.TimeUtils; +import android.util.Xml; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.CollectionUtils; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.IndentingPrintWriter; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Keeps track of recent active state changes in apps. + * Access should be guarded by a lock by the caller. + */ +public class AppIdleHistory { + + private static final String TAG = "AppIdleHistory"; + + private static final boolean DEBUG = AppStandbyController.DEBUG; + + // History for all users and all packages + private SparseArray<ArrayMap<String,AppUsageHistory>> mIdleHistory = new SparseArray<>(); + private static final long ONE_MINUTE = 60 * 1000; + + private static final int STANDBY_BUCKET_UNKNOWN = -1; + + /** + * The bucket beyond which apps are considered idle. Any apps in this bucket or lower are + * considered idle while those in higher buckets are not considered idle. + */ + static final int IDLE_BUCKET_CUTOFF = STANDBY_BUCKET_RARE; + + @VisibleForTesting + static final String APP_IDLE_FILENAME = "app_idle_stats.xml"; + private static final String TAG_PACKAGES = "packages"; + private static final String TAG_PACKAGE = "package"; + private static final String ATTR_NAME = "name"; + // Screen on timebase time when app was last used + private static final String ATTR_SCREEN_IDLE = "screenIdleTime"; + // Elapsed timebase time when app was last used + private static final String ATTR_ELAPSED_IDLE = "elapsedIdleTime"; + // Elapsed timebase time when app was last used by the user + private static final String ATTR_LAST_USED_BY_USER_ELAPSED = "lastUsedByUserElapsedTime"; + // Elapsed timebase time when the app bucket was last predicted externally + private static final String ATTR_LAST_PREDICTED_TIME = "lastPredictedTime"; + // The standby bucket for the app + private static final String ATTR_CURRENT_BUCKET = "appLimitBucket"; + // The reason the app was put in the above bucket + private static final String ATTR_BUCKETING_REASON = "bucketReason"; + // The last time a job was run for this app + private static final String ATTR_LAST_RUN_JOB_TIME = "lastJobRunTime"; + // The time when the forced active state can be overridden. + private static final String ATTR_BUCKET_ACTIVE_TIMEOUT_TIME = "activeTimeoutTime"; + // The time when the forced working_set state can be overridden. + private static final String ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME = "workingSetTimeoutTime"; + // Elapsed timebase time when the app was last marked for restriction. + private static final String ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED = + "lastRestrictionAttemptElapsedTime"; + // Reason why the app was last marked for restriction. + private static final String ATTR_LAST_RESTRICTION_ATTEMPT_REASON = + "lastRestrictionAttemptReason"; + + // device on time = mElapsedDuration + (timeNow - mElapsedSnapshot) + private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration + private long mElapsedDuration; // Total device on duration since device was "born" + + // screen on time = mScreenOnDuration + (timeNow - mScreenOnSnapshot) + private long mScreenOnSnapshot; // Elapsed time snapshot when last write of mScreenOnDuration + private long mScreenOnDuration; // Total screen on duration since device was "born" + + private final File mStorageDir; + + private boolean mScreenOn; + + static class AppUsageHistory { + // Last used time (including system usage), using elapsed timebase + long lastUsedElapsedTime; + // Last time the user used the app, using elapsed timebase + long lastUsedByUserElapsedTime; + // Last used time using screen_on timebase + long lastUsedScreenTime; + // Last predicted time using elapsed timebase + long lastPredictedTime; + // Last predicted bucket + @UsageStatsManager.StandbyBuckets + int lastPredictedBucket = STANDBY_BUCKET_UNKNOWN; + // Standby bucket + @UsageStatsManager.StandbyBuckets + int currentBucket; + // Reason for setting the standby bucket. The value here is a combination of + // one of UsageStatsManager.REASON_MAIN_* and one (or none) of + // UsageStatsManager.REASON_SUB_*. Also see REASON_MAIN_MASK and REASON_SUB_MASK. + int bucketingReason; + // In-memory only, last bucket for which the listeners were informed + int lastInformedBucket; + // The last time a job was run for this app, using elapsed timebase + long lastJobRunTime; + // When should the bucket active state timeout, in elapsed timebase, if greater than + // lastUsedElapsedTime. + // This is used to keep the app in a high bucket regardless of other timeouts and + // predictions. + long bucketActiveTimeoutTime; + // If there's a forced working_set state, this is when it times out. This can be sitting + // under any active state timeout, so that it becomes applicable after the active state + // timeout expires. + long bucketWorkingSetTimeoutTime; + // The last time an agent attempted to put the app into the RESTRICTED bucket. + long lastRestrictAttemptElapsedTime; + // The last reason the app was marked to be put into the RESTRICTED bucket. + int lastRestrictReason; + } + + AppIdleHistory(File storageDir, long elapsedRealtime) { + mElapsedSnapshot = elapsedRealtime; + mScreenOnSnapshot = elapsedRealtime; + mStorageDir = storageDir; + readScreenOnTime(); + } + + public void updateDisplay(boolean screenOn, long elapsedRealtime) { + if (screenOn == mScreenOn) return; + + mScreenOn = screenOn; + if (mScreenOn) { + mScreenOnSnapshot = elapsedRealtime; + } else { + mScreenOnDuration += elapsedRealtime - mScreenOnSnapshot; + mElapsedDuration += elapsedRealtime - mElapsedSnapshot; + mElapsedSnapshot = elapsedRealtime; + } + if (DEBUG) Slog.d(TAG, "mScreenOnSnapshot=" + mScreenOnSnapshot + + ", mScreenOnDuration=" + mScreenOnDuration + + ", mScreenOn=" + mScreenOn); + } + + public long getScreenOnTime(long elapsedRealtime) { + long screenOnTime = mScreenOnDuration; + if (mScreenOn) { + screenOnTime += elapsedRealtime - mScreenOnSnapshot; + } + return screenOnTime; + } + + @VisibleForTesting + File getScreenOnTimeFile() { + return new File(mStorageDir, "screen_on_time"); + } + + private void readScreenOnTime() { + File screenOnTimeFile = getScreenOnTimeFile(); + if (screenOnTimeFile.exists()) { + try { + BufferedReader reader = new BufferedReader(new FileReader(screenOnTimeFile)); + mScreenOnDuration = Long.parseLong(reader.readLine()); + mElapsedDuration = Long.parseLong(reader.readLine()); + reader.close(); + } catch (IOException | NumberFormatException e) { + } + } else { + writeScreenOnTime(); + } + } + + private void writeScreenOnTime() { + AtomicFile screenOnTimeFile = new AtomicFile(getScreenOnTimeFile()); + FileOutputStream fos = null; + try { + fos = screenOnTimeFile.startWrite(); + fos.write((Long.toString(mScreenOnDuration) + "\n" + + Long.toString(mElapsedDuration) + "\n").getBytes()); + screenOnTimeFile.finishWrite(fos); + } catch (IOException ioe) { + screenOnTimeFile.failWrite(fos); + } + } + + /** + * To be called periodically to keep track of elapsed time when app idle times are written + */ + public void writeAppIdleDurations() { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + // Only bump up and snapshot the elapsed time. Don't change screen on duration. + mElapsedDuration += elapsedRealtime - mElapsedSnapshot; + mElapsedSnapshot = elapsedRealtime; + writeScreenOnTime(); + } + + /** + * Mark the app as used and update the bucket if necessary. If there is a timeout specified + * that's in the future, then the usage event is temporary and keeps the app in the specified + * bucket at least until the timeout is reached. This can be used to keep the app in an + * elevated bucket for a while until some important task gets to run. + * @param appUsageHistory the usage record for the app being updated + * @param packageName name of the app being updated, for logging purposes + * @param newBucket the bucket to set the app to + * @param usageReason the sub-reason for usage, one of REASON_SUB_USAGE_* + * @param elapsedRealtime mark as used time if non-zero + * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used + * with bucket values of ACTIVE and WORKING_SET. + * @return {@code appUsageHistory} + */ + AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName, int userId, + int newBucket, int usageReason, long elapsedRealtime, long timeout) { + int bucketingReason = REASON_MAIN_USAGE | usageReason; + final boolean isUserUsage = isUserUsage(bucketingReason); + + if (appUsageHistory.currentBucket == STANDBY_BUCKET_RESTRICTED && !isUserUsage) { + // Only user usage should bring an app out of the RESTRICTED bucket. + newBucket = STANDBY_BUCKET_RESTRICTED; + bucketingReason = appUsageHistory.bucketingReason; + } else { + // Set the timeout if applicable + if (timeout > elapsedRealtime) { + // Convert to elapsed timebase + final long timeoutTime = mElapsedDuration + (timeout - mElapsedSnapshot); + if (newBucket == STANDBY_BUCKET_ACTIVE) { + appUsageHistory.bucketActiveTimeoutTime = Math.max(timeoutTime, + appUsageHistory.bucketActiveTimeoutTime); + } else if (newBucket == STANDBY_BUCKET_WORKING_SET) { + appUsageHistory.bucketWorkingSetTimeoutTime = Math.max(timeoutTime, + appUsageHistory.bucketWorkingSetTimeoutTime); + } else { + throw new IllegalArgumentException("Cannot set a timeout on bucket=" + + newBucket); + } + } + } + + if (elapsedRealtime != 0) { + appUsageHistory.lastUsedElapsedTime = mElapsedDuration + + (elapsedRealtime - mElapsedSnapshot); + if (isUserUsage) { + appUsageHistory.lastUsedByUserElapsedTime = appUsageHistory.lastUsedElapsedTime; + } + appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime); + } + + if (appUsageHistory.currentBucket > newBucket) { + appUsageHistory.currentBucket = newBucket; + logAppStandbyBucketChanged(packageName, userId, newBucket, bucketingReason); + } + appUsageHistory.bucketingReason = bucketingReason; + + return appUsageHistory; + } + + /** + * Mark the app as used and update the bucket if necessary. If there is a timeout specified + * that's in the future, then the usage event is temporary and keeps the app in the specified + * bucket at least until the timeout is reached. This can be used to keep the app in an + * elevated bucket for a while until some important task gets to run. + * @param packageName + * @param userId + * @param newBucket the bucket to set the app to + * @param usageReason sub reason for usage + * @param nowElapsed mark as used time if non-zero + * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used + * with bucket values of ACTIVE and WORKING_SET. + * @return + */ + public AppUsageHistory reportUsage(String packageName, int userId, int newBucket, + int usageReason, long nowElapsed, long timeout) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory history = getPackageHistory(userHistory, packageName, nowElapsed, true); + return reportUsage(history, packageName, userId, newBucket, usageReason, nowElapsed, + timeout); + } + + private ArrayMap<String, AppUsageHistory> getUserHistory(int userId) { + ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId); + if (userHistory == null) { + userHistory = new ArrayMap<>(); + mIdleHistory.put(userId, userHistory); + readAppIdleTimes(userId, userHistory); + } + return userHistory; + } + + private AppUsageHistory getPackageHistory(ArrayMap<String, AppUsageHistory> userHistory, + String packageName, long elapsedRealtime, boolean create) { + AppUsageHistory appUsageHistory = userHistory.get(packageName); + if (appUsageHistory == null && create) { + appUsageHistory = new AppUsageHistory(); + appUsageHistory.lastUsedElapsedTime = getElapsedTime(elapsedRealtime); + appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime); + appUsageHistory.lastPredictedTime = getElapsedTime(0); + appUsageHistory.currentBucket = STANDBY_BUCKET_NEVER; + appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT; + appUsageHistory.lastInformedBucket = -1; + appUsageHistory.lastJobRunTime = Long.MIN_VALUE; // long long time ago + userHistory.put(packageName, appUsageHistory); + } + return appUsageHistory; + } + + public void onUserRemoved(int userId) { + mIdleHistory.remove(userId); + } + + public boolean isIdle(String packageName, int userId, long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, true); + return appUsageHistory.currentBucket >= IDLE_BUCKET_CUTOFF; + } + + public AppUsageHistory getAppUsageHistory(String packageName, int userId, + long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, true); + return appUsageHistory; + } + + public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, + int bucket, int reason) { + setAppStandbyBucket(packageName, userId, elapsedRealtime, bucket, reason, false); + } + + public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, + int bucket, int reason, boolean resetTimeout) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, true); + final boolean changed = appUsageHistory.currentBucket != bucket; + appUsageHistory.currentBucket = bucket; + appUsageHistory.bucketingReason = reason; + + final long elapsed = getElapsedTime(elapsedRealtime); + + if ((reason & REASON_MAIN_MASK) == REASON_MAIN_PREDICTED) { + appUsageHistory.lastPredictedTime = elapsed; + appUsageHistory.lastPredictedBucket = bucket; + } + if (resetTimeout) { + appUsageHistory.bucketActiveTimeoutTime = elapsed; + appUsageHistory.bucketWorkingSetTimeoutTime = elapsed; + } + if (changed) { + logAppStandbyBucketChanged(packageName, userId, bucket, reason); + } + } + + /** + * Update the prediction for the app but don't change the actual bucket + * @param app The app for which the prediction was made + * @param elapsedTimeAdjusted The elapsed time in the elapsed duration timebase + * @param bucket The predicted bucket + */ + public void updateLastPrediction(AppUsageHistory app, long elapsedTimeAdjusted, int bucket) { + app.lastPredictedTime = elapsedTimeAdjusted; + app.lastPredictedBucket = bucket; + } + + /** + * Marks the last time a job was run, with the given elapsedRealtime. The time stored is + * based on the elapsed timebase. + * @param packageName + * @param userId + * @param elapsedRealtime + */ + public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, true); + appUsageHistory.lastJobRunTime = getElapsedTime(elapsedRealtime); + } + + /** + * Notes an attempt to put the app in the {@link UsageStatsManager#STANDBY_BUCKET_RESTRICTED} + * bucket. + * + * @param packageName The package name of the app that is being restricted + * @param userId The ID of the user in which the app is being restricted + * @param elapsedRealtime The time the attempt was made, in the (unadjusted) elapsed realtime + * timebase + * @param reason The reason for the restriction attempt + */ + void noteRestrictionAttempt(String packageName, int userId, long elapsedRealtime, int reason) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, true); + appUsageHistory.lastRestrictAttemptElapsedTime = getElapsedTime(elapsedRealtime); + appUsageHistory.lastRestrictReason = reason; + } + + /** + * Returns the time since the last job was run for this app. This can be larger than the + * current elapsedRealtime, in case it happened before boot or a really large value if no jobs + * were ever run. + * @param packageName + * @param userId + * @param elapsedRealtime + * @return + */ + public long getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, false); + // Don't adjust the default, else it'll wrap around to a positive value + if (appUsageHistory == null || appUsageHistory.lastJobRunTime == Long.MIN_VALUE) { + return Long.MAX_VALUE; + } + return getElapsedTime(elapsedRealtime) - appUsageHistory.lastJobRunTime; + } + + public int getAppStandbyBucket(String packageName, int userId, long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, false); + return appUsageHistory == null ? STANDBY_BUCKET_NEVER : appUsageHistory.currentBucket; + } + + public ArrayList<AppStandbyInfo> getAppStandbyBuckets(int userId, boolean appIdleEnabled) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + int size = userHistory.size(); + ArrayList<AppStandbyInfo> buckets = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + buckets.add(new AppStandbyInfo(userHistory.keyAt(i), + appIdleEnabled ? userHistory.valueAt(i).currentBucket : STANDBY_BUCKET_ACTIVE)); + } + return buckets; + } + + public int getAppStandbyReason(String packageName, int userId, long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, false); + return appUsageHistory != null ? appUsageHistory.bucketingReason : 0; + } + + public long getElapsedTime(long elapsedRealtime) { + return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration); + } + + /* Returns the new standby bucket the app is assigned to */ + public int setIdle(String packageName, int userId, boolean idle, long elapsedRealtime) { + final int newBucket; + final int reason; + if (idle) { + newBucket = IDLE_BUCKET_CUTOFF; + reason = REASON_MAIN_FORCED_BY_USER; + } else { + newBucket = STANDBY_BUCKET_ACTIVE; + // This is to pretend that the app was just used, don't freeze the state anymore. + reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION; + } + setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket, reason, false); + + return newBucket; + } + + public void clearUsage(String packageName, int userId) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + userHistory.remove(packageName); + } + + boolean shouldInformListeners(String packageName, int userId, + long elapsedRealtime, int bucket) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, + elapsedRealtime, true); + if (appUsageHistory.lastInformedBucket != bucket) { + appUsageHistory.lastInformedBucket = bucket; + return true; + } + return false; + } + + /** + * Returns the index in the arrays of screenTimeThresholds and elapsedTimeThresholds + * that corresponds to how long since the app was used. + * @param packageName + * @param userId + * @param elapsedRealtime current time + * @param screenTimeThresholds Array of screen times, in ascending order, first one is 0 + * @param elapsedTimeThresholds Array of elapsed time, in ascending order, first one is 0 + * @return The index whose values the app's used time exceeds (in both arrays) + */ + int getThresholdIndex(String packageName, int userId, long elapsedRealtime, + long[] screenTimeThresholds, long[] elapsedTimeThresholds) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, + elapsedRealtime, false); + // If we don't have any state for the app, assume never used + if (appUsageHistory == null) return screenTimeThresholds.length - 1; + + long screenOnDelta = getScreenOnTime(elapsedRealtime) - appUsageHistory.lastUsedScreenTime; + long elapsedDelta = getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedElapsedTime; + + if (DEBUG) Slog.d(TAG, packageName + + " lastUsedScreen=" + appUsageHistory.lastUsedScreenTime + + " lastUsedElapsed=" + appUsageHistory.lastUsedElapsedTime); + if (DEBUG) Slog.d(TAG, packageName + " screenOn=" + screenOnDelta + + ", elapsed=" + elapsedDelta); + for (int i = screenTimeThresholds.length - 1; i >= 0; i--) { + if (screenOnDelta >= screenTimeThresholds[i] + && elapsedDelta >= elapsedTimeThresholds[i]) { + return i; + } + } + return 0; + } + + /** + * Log a standby bucket change to statsd, and also logcat if debug logging is enabled. + */ + private void logAppStandbyBucketChanged(String packageName, int userId, int bucket, + int reason) { + FrameworkStatsLog.write( + FrameworkStatsLog.APP_STANDBY_BUCKET_CHANGED, + packageName, userId, bucket, + (reason & REASON_MAIN_MASK), (reason & REASON_SUB_MASK)); + if (DEBUG) { + Slog.d(TAG, "Moved " + packageName + " to bucket=" + bucket + + ", reason=0x0" + Integer.toHexString(reason)); + } + } + + @VisibleForTesting + File getUserFile(int userId) { + return new File(new File(new File(mStorageDir, "users"), + Integer.toString(userId)), APP_IDLE_FILENAME); + } + + /** + * Check if App Idle File exists on disk + * @param userId + * @return true if file exists + */ + public boolean userFileExists(int userId) { + return getUserFile(userId).exists(); + } + + private void readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory) { + FileInputStream fis = null; + try { + AtomicFile appIdleFile = new AtomicFile(getUserFile(userId)); + fis = appIdleFile.openRead(); + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(fis, StandardCharsets.UTF_8.name()); + + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + // Skip + } + + if (type != XmlPullParser.START_TAG) { + Slog.e(TAG, "Unable to read app idle file for user " + userId); + return; + } + if (!parser.getName().equals(TAG_PACKAGES)) { + return; + } + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type == XmlPullParser.START_TAG) { + final String name = parser.getName(); + if (name.equals(TAG_PACKAGE)) { + final String packageName = parser.getAttributeValue(null, ATTR_NAME); + AppUsageHistory appUsageHistory = new AppUsageHistory(); + appUsageHistory.lastUsedElapsedTime = + Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE)); + appUsageHistory.lastUsedByUserElapsedTime = getLongValue(parser, + ATTR_LAST_USED_BY_USER_ELAPSED, + appUsageHistory.lastUsedElapsedTime); + appUsageHistory.lastUsedScreenTime = + Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE)); + appUsageHistory.lastPredictedTime = getLongValue(parser, + ATTR_LAST_PREDICTED_TIME, 0L); + String currentBucketString = parser.getAttributeValue(null, + ATTR_CURRENT_BUCKET); + appUsageHistory.currentBucket = currentBucketString == null + ? STANDBY_BUCKET_ACTIVE + : Integer.parseInt(currentBucketString); + String bucketingReason = + parser.getAttributeValue(null, ATTR_BUCKETING_REASON); + appUsageHistory.lastJobRunTime = getLongValue(parser, + ATTR_LAST_RUN_JOB_TIME, Long.MIN_VALUE); + appUsageHistory.bucketActiveTimeoutTime = getLongValue(parser, + ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, 0L); + appUsageHistory.bucketWorkingSetTimeoutTime = getLongValue(parser, + ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, 0L); + appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT; + if (bucketingReason != null) { + try { + appUsageHistory.bucketingReason = + Integer.parseInt(bucketingReason, 16); + } catch (NumberFormatException nfe) { + Slog.wtf(TAG, "Unable to read bucketing reason", nfe); + } + } + appUsageHistory.lastRestrictAttemptElapsedTime = + getLongValue(parser, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED, 0); + String lastRestrictReason = parser.getAttributeValue( + null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON); + if (lastRestrictReason != null) { + try { + appUsageHistory.lastRestrictReason = + Integer.parseInt(lastRestrictReason, 16); + } catch (NumberFormatException nfe) { + Slog.wtf(TAG, "Unable to read last restrict reason", nfe); + } + } + appUsageHistory.lastInformedBucket = -1; + userHistory.put(packageName, appUsageHistory); + } + } + } + } catch (IOException | XmlPullParserException e) { + Slog.e(TAG, "Unable to read app idle file for user " + userId, e); + } finally { + IoUtils.closeQuietly(fis); + } + } + + private long getLongValue(XmlPullParser parser, String attrName, long defValue) { + String value = parser.getAttributeValue(null, attrName); + if (value == null) return defValue; + return Long.parseLong(value); + } + + + public void writeAppIdleTimes() { + final int size = mIdleHistory.size(); + for (int i = 0; i < size; i++) { + writeAppIdleTimes(mIdleHistory.keyAt(i)); + } + } + + public void writeAppIdleTimes(int userId) { + FileOutputStream fos = null; + AtomicFile appIdleFile = new AtomicFile(getUserFile(userId)); + try { + fos = appIdleFile.startWrite(); + final BufferedOutputStream bos = new BufferedOutputStream(fos); + + FastXmlSerializer xml = new FastXmlSerializer(); + xml.setOutput(bos, StandardCharsets.UTF_8.name()); + xml.startDocument(null, true); + xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + + xml.startTag(null, TAG_PACKAGES); + + ArrayMap<String,AppUsageHistory> userHistory = getUserHistory(userId); + final int N = userHistory.size(); + for (int i = 0; i < N; i++) { + String packageName = userHistory.keyAt(i); + // Skip any unexpected null package names + if (packageName == null) { + Slog.w(TAG, "Skipping App Idle write for unexpected null package"); + continue; + } + AppUsageHistory history = userHistory.valueAt(i); + xml.startTag(null, TAG_PACKAGE); + xml.attribute(null, ATTR_NAME, packageName); + xml.attribute(null, ATTR_ELAPSED_IDLE, + Long.toString(history.lastUsedElapsedTime)); + xml.attribute(null, ATTR_LAST_USED_BY_USER_ELAPSED, + Long.toString(history.lastUsedByUserElapsedTime)); + xml.attribute(null, ATTR_SCREEN_IDLE, + Long.toString(history.lastUsedScreenTime)); + xml.attribute(null, ATTR_LAST_PREDICTED_TIME, + Long.toString(history.lastPredictedTime)); + xml.attribute(null, ATTR_CURRENT_BUCKET, + Integer.toString(history.currentBucket)); + xml.attribute(null, ATTR_BUCKETING_REASON, + Integer.toHexString(history.bucketingReason)); + if (history.bucketActiveTimeoutTime > 0) { + xml.attribute(null, ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, Long.toString(history + .bucketActiveTimeoutTime)); + } + if (history.bucketWorkingSetTimeoutTime > 0) { + xml.attribute(null, ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, Long.toString(history + .bucketWorkingSetTimeoutTime)); + } + if (history.lastJobRunTime != Long.MIN_VALUE) { + xml.attribute(null, ATTR_LAST_RUN_JOB_TIME, Long.toString(history + .lastJobRunTime)); + } + if (history.lastRestrictAttemptElapsedTime > 0) { + xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED, + Long.toString(history.lastRestrictAttemptElapsedTime)); + } + xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON, + Integer.toHexString(history.lastRestrictReason)); + xml.endTag(null, TAG_PACKAGE); + } + + xml.endTag(null, TAG_PACKAGES); + xml.endDocument(); + appIdleFile.finishWrite(fos); + } catch (Exception e) { + appIdleFile.failWrite(fos); + Slog.e(TAG, "Error writing app idle file for user " + userId, e); + } + } + + public void dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs) { + final int numUsers = userIds.length; + for (int i = 0; i < numUsers; i++) { + idpw.println(); + dumpUser(idpw, userIds[i], pkgs); + } + } + + private void dumpUser(IndentingPrintWriter idpw, int userId, List<String> pkgs) { + idpw.print("User "); + idpw.print(userId); + idpw.println(" App Standby States:"); + idpw.increaseIndent(); + ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long totalElapsedTime = getElapsedTime(elapsedRealtime); + final long screenOnTime = getScreenOnTime(elapsedRealtime); + if (userHistory == null) return; + final int P = userHistory.size(); + for (int p = 0; p < P; p++) { + final String packageName = userHistory.keyAt(p); + final AppUsageHistory appUsageHistory = userHistory.valueAt(p); + if (!CollectionUtils.isEmpty(pkgs) && !pkgs.contains(packageName)) { + continue; + } + idpw.print("package=" + packageName); + idpw.print(" u=" + userId); + idpw.print(" bucket=" + appUsageHistory.currentBucket + + " reason=" + + UsageStatsManager.reasonToString(appUsageHistory.bucketingReason)); + idpw.print(" used="); + TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedElapsedTime, idpw); + idpw.print(" usedByUser="); + TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedByUserElapsedTime, + idpw); + idpw.print(" usedScr="); + TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw); + idpw.print(" lastPred="); + TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastPredictedTime, idpw); + idpw.print(" activeLeft="); + TimeUtils.formatDuration(appUsageHistory.bucketActiveTimeoutTime - totalElapsedTime, + idpw); + idpw.print(" wsLeft="); + TimeUtils.formatDuration(appUsageHistory.bucketWorkingSetTimeoutTime - totalElapsedTime, + idpw); + idpw.print(" lastJob="); + TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw); + if (appUsageHistory.lastRestrictAttemptElapsedTime > 0) { + idpw.print(" lastRestrictAttempt="); + TimeUtils.formatDuration( + totalElapsedTime - appUsageHistory.lastRestrictAttemptElapsedTime, idpw); + idpw.print(" lastRestrictReason=" + + UsageStatsManager.reasonToString(appUsageHistory.lastRestrictReason)); + } + idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n")); + idpw.println(); + } + idpw.println(); + idpw.print("totalElapsedTime="); + TimeUtils.formatDuration(getElapsedTime(elapsedRealtime), idpw); + idpw.println(); + idpw.print("totalScreenOnTime="); + TimeUtils.formatDuration(getScreenOnTime(elapsedRealtime), idpw); + idpw.println(); + idpw.decreaseIndent(); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java new file mode 100644 index 000000000000..0cadbfd23dba --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -0,0 +1,2432 @@ +/** + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.android.server.usage; + +import static android.app.usage.UsageStatsManager.REASON_MAIN_DEFAULT; +import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_SYSTEM; +import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER; +import static android.app.usage.UsageStatsManager.REASON_MAIN_MASK; +import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED; +import static android.app.usage.UsageStatsManager.REASON_MAIN_TIMEOUT; +import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE; +import static android.app.usage.UsageStatsManager.REASON_SUB_DEFAULT_APP_UPDATE; +import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY; +import static android.app.usage.UsageStatsManager.REASON_SUB_MASK; +import static android.app.usage.UsageStatsManager.REASON_SUB_PREDICTED_RESTORED; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_ACTIVE_TIMEOUT; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_DOZE; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_NON_DOZE; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_EXEMPTED_SYNC_START; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_FOREGROUND_SERVICE_START; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_MOVE_TO_BACKGROUND; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_MOVE_TO_FOREGROUND; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_NOTIFICATION_SEEN; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SLICE_PINNED; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SLICE_PINNED_PRIV; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SYNC_ADAPTER; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SYSTEM_INTERACTION; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SYSTEM_UPDATE; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_UNEXEMPTED_SYNC_SCHEDULED; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; + +import static com.android.server.SystemService.PHASE_BOOT_COMPLETED; +import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.ActivityManager; +import android.app.AppGlobals; +import android.app.usage.AppStandbyInfo; +import android.app.usage.UsageEvents; +import android.app.usage.UsageStatsManager.StandbyBuckets; +import android.app.usage.UsageStatsManager.SystemForcedReasons; +import android.appwidget.AppWidgetManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.CrossProfileAppsInternal; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ParceledListSlice; +import android.database.ContentObserver; +import android.hardware.display.DisplayManager; +import android.net.NetworkScoreManager; +import android.os.BatteryManager; +import android.os.BatteryStats; +import android.os.Build; +import android.os.Environment; +import android.os.Handler; +import android.os.IDeviceIdleController; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.Trace; +import android.os.UserHandle; +import android.provider.Settings.Global; +import android.telephony.TelephonyManager; +import android.util.ArraySet; +import android.util.KeyValueListParser; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.TimeUtils; +import android.view.Display; +import android.widget.Toast; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IBatteryStats; +import com.android.internal.os.SomeArgs; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.ConcurrentUtils; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.LocalServices; +import com.android.server.pm.parsing.pkg.AndroidPackage; +import com.android.server.usage.AppIdleHistory.AppUsageHistory; + +import java.io.File; +import java.io.PrintWriter; +import java.time.Duration; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +/** + * Manages the standby state of an app, listening to various events. + * + * Unit test: + atest com.android.server.usage.AppStandbyControllerTests + */ +public class AppStandbyController implements AppStandbyInternal { + + private static final String TAG = "AppStandbyController"; + // Do not submit with true. + static final boolean DEBUG = false; + + static final boolean COMPRESS_TIME = false; + private static final long ONE_MINUTE = 60 * 1000; + private static final long ONE_HOUR = ONE_MINUTE * 60; + private static final long ONE_DAY = ONE_HOUR * 24; + + /** + * The minimum amount of time the screen must have been on before an app can time out from its + * current bucket to the next bucket. + */ + private static final long[] SCREEN_TIME_THRESHOLDS = { + 0, + 0, + COMPRESS_TIME ? 2 * ONE_MINUTE : 1 * ONE_HOUR, + COMPRESS_TIME ? 4 * ONE_MINUTE : 2 * ONE_HOUR, + COMPRESS_TIME ? 8 * ONE_MINUTE : 6 * ONE_HOUR + }; + + /** The minimum allowed values for each index in {@link #SCREEN_TIME_THRESHOLDS}. */ + private static final long[] MINIMUM_SCREEN_TIME_THRESHOLDS = COMPRESS_TIME + ? new long[SCREEN_TIME_THRESHOLDS.length] + : new long[]{ + 0, + 0, + 0, + 30 * ONE_MINUTE, + ONE_HOUR + }; + + /** + * The minimum amount of elapsed time that must have passed before an app can time out from its + * current bucket to the next bucket. + */ + private static final long[] ELAPSED_TIME_THRESHOLDS = { + 0, + COMPRESS_TIME ? 1 * ONE_MINUTE : 12 * ONE_HOUR, + COMPRESS_TIME ? 4 * ONE_MINUTE : 24 * ONE_HOUR, + COMPRESS_TIME ? 16 * ONE_MINUTE : 48 * ONE_HOUR, + COMPRESS_TIME ? 32 * ONE_MINUTE : 30 * ONE_DAY + }; + + /** The minimum allowed values for each index in {@link #ELAPSED_TIME_THRESHOLDS}. */ + private static final long[] MINIMUM_ELAPSED_TIME_THRESHOLDS = COMPRESS_TIME + ? new long[ELAPSED_TIME_THRESHOLDS.length] + : new long[]{ + 0, + ONE_HOUR, + ONE_HOUR, + 2 * ONE_HOUR, + 4 * ONE_DAY + }; + + private static final int[] THRESHOLD_BUCKETS = { + STANDBY_BUCKET_ACTIVE, + STANDBY_BUCKET_WORKING_SET, + STANDBY_BUCKET_FREQUENT, + STANDBY_BUCKET_RARE, + STANDBY_BUCKET_RESTRICTED + }; + + /** Default expiration time for bucket prediction. After this, use thresholds to downgrade. */ + private static final long DEFAULT_PREDICTION_TIMEOUT = 12 * ONE_HOUR; + + /** + * Indicates the maximum wait time for admin data to be available; + */ + private static final long WAIT_FOR_ADMIN_DATA_TIMEOUT_MS = 10_000; + + private static final int HEADLESS_APP_CHECK_FLAGS = + PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.GET_ACTIVITIES | PackageManager.MATCH_DISABLED_COMPONENTS; + + // To name the lock for stack traces + static class Lock {} + + /** Lock to protect the app's standby state. Required for calls into AppIdleHistory */ + private final Object mAppIdleLock = new Lock(); + + /** Keeps the history and state for each app. */ + @GuardedBy("mAppIdleLock") + private AppIdleHistory mAppIdleHistory; + + @GuardedBy("mPackageAccessListeners") + private final ArrayList<AppIdleStateChangeListener> mPackageAccessListeners = new ArrayList<>(); + + /** Whether we've queried the list of carrier privileged apps. */ + @GuardedBy("mAppIdleLock") + private boolean mHaveCarrierPrivilegedApps; + + /** List of carrier-privileged apps that should be excluded from standby */ + @GuardedBy("mAppIdleLock") + private List<String> mCarrierPrivilegedApps; + + @GuardedBy("mActiveAdminApps") + private final SparseArray<Set<String>> mActiveAdminApps = new SparseArray<>(); + + /** + * Set of system apps that are headless (don't have any declared activities, enabled or + * disabled). Presence in this map indicates that the app is a headless system app. + */ + @GuardedBy("mHeadlessSystemApps") + private final ArraySet<String> mHeadlessSystemApps = new ArraySet<>(); + + private final CountDownLatch mAdminDataAvailableLatch = new CountDownLatch(1); + + // Cache the active network scorer queried from the network scorer service + private volatile String mCachedNetworkScorer = null; + // The last time the network scorer service was queried + private volatile long mCachedNetworkScorerAtMillis = 0L; + // How long before querying the network scorer again. During this time, subsequent queries will + // get the cached value + private static final long NETWORK_SCORER_CACHE_DURATION_MILLIS = 5000L; + + // Messages for the handler + static final int MSG_INFORM_LISTENERS = 3; + static final int MSG_FORCE_IDLE_STATE = 4; + static final int MSG_CHECK_IDLE_STATES = 5; + static final int MSG_REPORT_CONTENT_PROVIDER_USAGE = 8; + static final int MSG_PAROLE_STATE_CHANGED = 9; + static final int MSG_ONE_TIME_CHECK_IDLE_STATES = 10; + /** Check the state of one app: arg1 = userId, arg2 = uid, obj = (String) packageName */ + static final int MSG_CHECK_PACKAGE_IDLE_STATE = 11; + static final int MSG_REPORT_SYNC_SCHEDULED = 12; + static final int MSG_REPORT_EXEMPTED_SYNC_START = 13; + + long mCheckIdleIntervalMillis; + /** + * The minimum amount of time the screen must have been on before an app can time out from its + * current bucket to the next bucket. + */ + long[] mAppStandbyScreenThresholds = SCREEN_TIME_THRESHOLDS; + /** + * The minimum amount of elapsed time that must have passed before an app can time out from its + * current bucket to the next bucket. + */ + long[] mAppStandbyElapsedThresholds = ELAPSED_TIME_THRESHOLDS; + /** Minimum time a strong usage event should keep the bucket elevated. */ + long mStrongUsageTimeoutMillis; + /** Minimum time a notification seen event should keep the bucket elevated. */ + long mNotificationSeenTimeoutMillis; + /** Minimum time a system update event should keep the buckets elevated. */ + long mSystemUpdateUsageTimeoutMillis; + /** Maximum time to wait for a prediction before using simple timeouts to downgrade buckets. */ + long mPredictionTimeoutMillis; + /** Maximum time a sync adapter associated with a CP should keep the buckets elevated. */ + long mSyncAdapterTimeoutMillis; + /** + * Maximum time an exempted sync should keep the buckets elevated, when sync is scheduled in + * non-doze + */ + long mExemptedSyncScheduledNonDozeTimeoutMillis; + /** + * Maximum time an exempted sync should keep the buckets elevated, when sync is scheduled in + * doze + */ + long mExemptedSyncScheduledDozeTimeoutMillis; + /** + * Maximum time an exempted sync should keep the buckets elevated, when sync is started. + */ + long mExemptedSyncStartTimeoutMillis; + /** + * Maximum time an unexempted sync should keep the buckets elevated, when sync is scheduled + */ + long mUnexemptedSyncScheduledTimeoutMillis; + /** Maximum time a system interaction should keep the buckets elevated. */ + long mSystemInteractionTimeoutMillis; + /** + * Maximum time a foreground service start should keep the buckets elevated if the service + * start is the first usage of the app + */ + long mInitialForegroundServiceStartTimeoutMillis; + /** + * User usage that would elevate an app's standby bucket will also elevate the standby bucket of + * cross profile connected apps. Explicit standby bucket setting via + * {@link #setAppStandbyBucket(String, int, int, int, int)} will not be propagated. + */ + boolean mLinkCrossProfileApps; + /** + * Whether we should allow apps into the + * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket or not. + * If false, any attempts to put an app into the bucket will put the app into the + * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RARE} bucket instead. + */ + private boolean mAllowRestrictedBucket; + + private volatile boolean mAppIdleEnabled; + private boolean mIsCharging; + private boolean mSystemServicesReady = false; + // There was a system update, defaults need to be initialized after services are ready + private boolean mPendingInitializeDefaults; + + private volatile boolean mPendingOneTimeCheckIdleStates; + + private final AppStandbyHandler mHandler; + private final Context mContext; + + private AppWidgetManager mAppWidgetManager; + private PackageManager mPackageManager; + Injector mInjector; + + static final ArrayList<StandbyUpdateRecord> sStandbyUpdatePool = new ArrayList<>(4); + + public static class StandbyUpdateRecord { + // Identity of the app whose standby state has changed + String packageName; + int userId; + + // What the standby bucket the app is now in + int bucket; + + // Whether the bucket change is because the user has started interacting with the app + boolean isUserInteraction; + + // Reason for bucket change + int reason; + + StandbyUpdateRecord(String pkgName, int userId, int bucket, int reason, + boolean isInteraction) { + this.packageName = pkgName; + this.userId = userId; + this.bucket = bucket; + this.reason = reason; + this.isUserInteraction = isInteraction; + } + + public static StandbyUpdateRecord obtain(String pkgName, int userId, + int bucket, int reason, boolean isInteraction) { + synchronized (sStandbyUpdatePool) { + final int size = sStandbyUpdatePool.size(); + if (size < 1) { + return new StandbyUpdateRecord(pkgName, userId, bucket, reason, isInteraction); + } + StandbyUpdateRecord r = sStandbyUpdatePool.remove(size - 1); + r.packageName = pkgName; + r.userId = userId; + r.bucket = bucket; + r.reason = reason; + r.isUserInteraction = isInteraction; + return r; + } + } + + public void recycle() { + synchronized (sStandbyUpdatePool) { + sStandbyUpdatePool.add(this); + } + } + } + + public AppStandbyController(Context context, Looper looper) { + this(new Injector(context, looper)); + } + + AppStandbyController(Injector injector) { + mInjector = injector; + mContext = mInjector.getContext(); + mHandler = new AppStandbyHandler(mInjector.getLooper()); + mPackageManager = mContext.getPackageManager(); + + DeviceStateReceiver deviceStateReceiver = new DeviceStateReceiver(); + IntentFilter deviceStates = new IntentFilter(BatteryManager.ACTION_CHARGING); + deviceStates.addAction(BatteryManager.ACTION_DISCHARGING); + deviceStates.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); + mContext.registerReceiver(deviceStateReceiver, deviceStates); + + synchronized (mAppIdleLock) { + mAppIdleHistory = new AppIdleHistory(mInjector.getDataSystemDirectory(), + mInjector.elapsedRealtime()); + } + + IntentFilter packageFilter = new IntentFilter(); + packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); + packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + packageFilter.addDataScheme("package"); + + mContext.registerReceiverAsUser(new PackageReceiver(), UserHandle.ALL, packageFilter, + null, mHandler); + } + + @VisibleForTesting + void setAppIdleEnabled(boolean enabled) { + synchronized (mAppIdleLock) { + if (mAppIdleEnabled != enabled) { + final boolean oldParoleState = isInParole(); + mAppIdleEnabled = enabled; + if (isInParole() != oldParoleState) { + postParoleStateChanged(); + } + } + } + + } + + @Override + public boolean isAppIdleEnabled() { + return mAppIdleEnabled; + } + + @Override + public void onBootPhase(int phase) { + mInjector.onBootPhase(phase); + if (phase == PHASE_SYSTEM_SERVICES_READY) { + Slog.d(TAG, "Setting app idle enabled state"); + // Observe changes to the threshold + SettingsObserver settingsObserver = new SettingsObserver(mHandler); + settingsObserver.registerObserver(); + settingsObserver.updateSettings(); + + mAppWidgetManager = mContext.getSystemService(AppWidgetManager.class); + + mInjector.registerDisplayListener(mDisplayListener, mHandler); + synchronized (mAppIdleLock) { + mAppIdleHistory.updateDisplay(isDisplayOn(), mInjector.elapsedRealtime()); + } + + mSystemServicesReady = true; + + // Offload to handler thread to avoid boot time impact. + mHandler.post(AppStandbyController.this::updatePowerWhitelistCache); + + boolean userFileExists; + synchronized (mAppIdleLock) { + userFileExists = mAppIdleHistory.userFileExists(UserHandle.USER_SYSTEM); + } + + if (mPendingInitializeDefaults || !userFileExists) { + initializeDefaultsForSystemApps(UserHandle.USER_SYSTEM); + } + + if (mPendingOneTimeCheckIdleStates) { + postOneTimeCheckIdleStates(); + } + } else if (phase == PHASE_BOOT_COMPLETED) { + setChargingState(mInjector.isCharging()); + + // Offload to handler thread after boot completed to avoid boot time impact. This means + // that headless system apps may be put in a lower bucket until boot has completed. + mHandler.post(this::loadHeadlessSystemAppCache); + } + } + + private void reportContentProviderUsage(String authority, String providerPkgName, int userId) { + if (!mAppIdleEnabled) return; + + // Get sync adapters for the authority + String[] packages = ContentResolver.getSyncAdapterPackagesForAuthorityAsUser( + authority, userId); + final long elapsedRealtime = mInjector.elapsedRealtime(); + for (String packageName: packages) { + // Only force the sync adapters to active if the provider is not in the same package and + // the sync adapter is a system package. + try { + PackageInfo pi = mPackageManager.getPackageInfoAsUser( + packageName, PackageManager.MATCH_SYSTEM_ONLY, userId); + if (pi == null || pi.applicationInfo == null) { + continue; + } + if (!packageName.equals(providerPkgName)) { + final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName, + userId); + synchronized (mAppIdleLock) { + reportNoninteractiveUsageCrossUserLocked(packageName, userId, + STANDBY_BUCKET_ACTIVE, REASON_SUB_USAGE_SYNC_ADAPTER, + elapsedRealtime, mSyncAdapterTimeoutMillis, linkedProfiles); + } + } + } catch (PackageManager.NameNotFoundException e) { + // Shouldn't happen + } + } + } + + private void reportExemptedSyncScheduled(String packageName, int userId) { + if (!mAppIdleEnabled) return; + + final int bucketToPromote; + final int usageReason; + final long durationMillis; + + if (!mInjector.isDeviceIdleMode()) { + // Not dozing. + bucketToPromote = STANDBY_BUCKET_ACTIVE; + usageReason = REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_NON_DOZE; + durationMillis = mExemptedSyncScheduledNonDozeTimeoutMillis; + } else { + // Dozing. + bucketToPromote = STANDBY_BUCKET_WORKING_SET; + usageReason = REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_DOZE; + durationMillis = mExemptedSyncScheduledDozeTimeoutMillis; + } + + final long elapsedRealtime = mInjector.elapsedRealtime(); + final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName, userId); + synchronized (mAppIdleLock) { + reportNoninteractiveUsageCrossUserLocked(packageName, userId, bucketToPromote, + usageReason, elapsedRealtime, durationMillis, linkedProfiles); + } + } + + private void reportUnexemptedSyncScheduled(String packageName, int userId) { + if (!mAppIdleEnabled) return; + + final long elapsedRealtime = mInjector.elapsedRealtime(); + synchronized (mAppIdleLock) { + final int currentBucket = + mAppIdleHistory.getAppStandbyBucket(packageName, userId, elapsedRealtime); + if (currentBucket == STANDBY_BUCKET_NEVER) { + final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName, userId); + // Bring the app out of the never bucket + reportNoninteractiveUsageCrossUserLocked(packageName, userId, + STANDBY_BUCKET_WORKING_SET, REASON_SUB_USAGE_UNEXEMPTED_SYNC_SCHEDULED, + elapsedRealtime, mUnexemptedSyncScheduledTimeoutMillis, linkedProfiles); + } + } + } + + private void reportExemptedSyncStart(String packageName, int userId) { + if (!mAppIdleEnabled) return; + + final long elapsedRealtime = mInjector.elapsedRealtime(); + final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName, userId); + synchronized (mAppIdleLock) { + reportNoninteractiveUsageCrossUserLocked(packageName, userId, STANDBY_BUCKET_ACTIVE, + REASON_SUB_USAGE_EXEMPTED_SYNC_START, elapsedRealtime, + mExemptedSyncStartTimeoutMillis, linkedProfiles); + } + } + + /** + * Helper method to report indirect user usage of an app and handle reporting the usage + * against cross profile connected apps. <br> + * Use {@link #reportNoninteractiveUsageLocked(String, int, int, int, long, long)} if + * cross profile connected apps do not need to be handled. + */ + private void reportNoninteractiveUsageCrossUserLocked(String packageName, int userId, + int bucket, int subReason, long elapsedRealtime, long nextCheckDelay, + List<UserHandle> otherProfiles) { + reportNoninteractiveUsageLocked(packageName, userId, bucket, subReason, elapsedRealtime, + nextCheckDelay); + final int size = otherProfiles.size(); + for (int profileIndex = 0; profileIndex < size; profileIndex++) { + final int otherUserId = otherProfiles.get(profileIndex).getIdentifier(); + reportNoninteractiveUsageLocked(packageName, otherUserId, bucket, subReason, + elapsedRealtime, nextCheckDelay); + } + } + + /** + * Helper method to report indirect user usage of an app. <br> + * Use + * {@link #reportNoninteractiveUsageCrossUserLocked(String, int, int, int, long, long, List)} + * if cross profile connected apps need to be handled. + */ + private void reportNoninteractiveUsageLocked(String packageName, int userId, int bucket, + int subReason, long elapsedRealtime, long nextCheckDelay) { + final AppUsageHistory appUsage = mAppIdleHistory.reportUsage(packageName, userId, bucket, + subReason, 0, elapsedRealtime + nextCheckDelay); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, packageName), + nextCheckDelay); + maybeInformListeners(packageName, userId, elapsedRealtime, appUsage.currentBucket, + appUsage.bucketingReason, false); + } + + @VisibleForTesting + void setChargingState(boolean isCharging) { + synchronized (mAppIdleLock) { + if (mIsCharging != isCharging) { + if (DEBUG) Slog.d(TAG, "Setting mIsCharging to " + isCharging); + mIsCharging = isCharging; + postParoleStateChanged(); + } + } + } + + @Override + public boolean isInParole() { + return !mAppIdleEnabled || mIsCharging; + } + + private void postParoleStateChanged() { + if (DEBUG) Slog.d(TAG, "Posting MSG_PAROLE_STATE_CHANGED"); + mHandler.removeMessages(MSG_PAROLE_STATE_CHANGED); + mHandler.sendEmptyMessage(MSG_PAROLE_STATE_CHANGED); + } + + @Override + public void postCheckIdleStates(int userId) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_CHECK_IDLE_STATES, userId, 0)); + } + + @Override + public void postOneTimeCheckIdleStates() { + if (mInjector.getBootPhase() < PHASE_SYSTEM_SERVICES_READY) { + // Not booted yet; wait for it! + mPendingOneTimeCheckIdleStates = true; + } else { + mHandler.sendEmptyMessage(MSG_ONE_TIME_CHECK_IDLE_STATES); + mPendingOneTimeCheckIdleStates = false; + } + } + + @VisibleForTesting + boolean checkIdleStates(int checkUserId) { + if (!mAppIdleEnabled) { + return false; + } + + final int[] runningUserIds; + try { + runningUserIds = mInjector.getRunningUserIds(); + if (checkUserId != UserHandle.USER_ALL + && !ArrayUtils.contains(runningUserIds, checkUserId)) { + return false; + } + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + + final long elapsedRealtime = mInjector.elapsedRealtime(); + for (int i = 0; i < runningUserIds.length; i++) { + final int userId = runningUserIds[i]; + if (checkUserId != UserHandle.USER_ALL && checkUserId != userId) { + continue; + } + if (DEBUG) { + Slog.d(TAG, "Checking idle state for user " + userId); + } + List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser( + PackageManager.MATCH_DISABLED_COMPONENTS, + userId); + final int packageCount = packages.size(); + for (int p = 0; p < packageCount; p++) { + final PackageInfo pi = packages.get(p); + final String packageName = pi.packageName; + checkAndUpdateStandbyState(packageName, userId, pi.applicationInfo.uid, + elapsedRealtime); + } + } + if (DEBUG) { + Slog.d(TAG, "checkIdleStates took " + + (mInjector.elapsedRealtime() - elapsedRealtime)); + } + return true; + } + + /** Check if we need to update the standby state of a specific app. */ + private void checkAndUpdateStandbyState(String packageName, @UserIdInt int userId, + int uid, long elapsedRealtime) { + if (uid <= 0) { + try { + uid = mPackageManager.getPackageUidAsUser(packageName, userId); + } catch (PackageManager.NameNotFoundException e) { + // Not a valid package for this user, nothing to do + // TODO: Remove any history of removed packages + return; + } + } + final int minBucket = getAppMinBucket(packageName, + UserHandle.getAppId(uid), + userId); + if (DEBUG) { + Slog.d(TAG, " Checking idle state for " + packageName + + " minBucket=" + minBucket); + } + if (minBucket <= STANDBY_BUCKET_ACTIVE) { + // No extra processing needed for ACTIVE or higher since apps can't drop into lower + // buckets. + synchronized (mAppIdleLock) { + mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, + minBucket, REASON_MAIN_DEFAULT); + } + maybeInformListeners(packageName, userId, elapsedRealtime, + minBucket, REASON_MAIN_DEFAULT, false); + } else { + synchronized (mAppIdleLock) { + final AppIdleHistory.AppUsageHistory app = + mAppIdleHistory.getAppUsageHistory(packageName, + userId, elapsedRealtime); + int reason = app.bucketingReason; + final int oldMainReason = reason & REASON_MAIN_MASK; + + // If the bucket was forced by the user/developer, leave it alone. + // A usage event will be the only way to bring it out of this forced state + if (oldMainReason == REASON_MAIN_FORCED_BY_USER) { + return; + } + final int oldBucket = app.currentBucket; + if (oldBucket == STANDBY_BUCKET_NEVER) { + // None of this should bring an app out of the NEVER bucket. + return; + } + int newBucket = Math.max(oldBucket, STANDBY_BUCKET_ACTIVE); // Undo EXEMPTED + boolean predictionLate = predictionTimedOut(app, elapsedRealtime); + // Compute age-based bucket + if (oldMainReason == REASON_MAIN_DEFAULT + || oldMainReason == REASON_MAIN_USAGE + || oldMainReason == REASON_MAIN_TIMEOUT + || predictionLate) { + + if (!predictionLate && app.lastPredictedBucket >= STANDBY_BUCKET_ACTIVE + && app.lastPredictedBucket <= STANDBY_BUCKET_RARE) { + newBucket = app.lastPredictedBucket; + reason = REASON_MAIN_PREDICTED | REASON_SUB_PREDICTED_RESTORED; + if (DEBUG) { + Slog.d(TAG, "Restored predicted newBucket = " + newBucket); + } + } else { + newBucket = getBucketForLocked(packageName, userId, + elapsedRealtime); + if (DEBUG) { + Slog.d(TAG, "Evaluated AOSP newBucket = " + newBucket); + } + reason = REASON_MAIN_TIMEOUT; + } + } + + // Check if the app is within one of the timeouts for forced bucket elevation + final long elapsedTimeAdjusted = mAppIdleHistory.getElapsedTime(elapsedRealtime); + if (newBucket >= STANDBY_BUCKET_ACTIVE + && app.bucketActiveTimeoutTime > elapsedTimeAdjusted) { + newBucket = STANDBY_BUCKET_ACTIVE; + reason = app.bucketingReason; + if (DEBUG) { + Slog.d(TAG, " Keeping at ACTIVE due to min timeout"); + } + } else if (newBucket >= STANDBY_BUCKET_WORKING_SET + && app.bucketWorkingSetTimeoutTime > elapsedTimeAdjusted) { + newBucket = STANDBY_BUCKET_WORKING_SET; + // If it was already there, keep the reason, else assume timeout to WS + reason = (newBucket == oldBucket) + ? app.bucketingReason + : REASON_MAIN_USAGE | REASON_SUB_USAGE_ACTIVE_TIMEOUT; + if (DEBUG) { + Slog.d(TAG, " Keeping at WORKING_SET due to min timeout"); + } + } + + if (app.lastRestrictAttemptElapsedTime > app.lastUsedByUserElapsedTime + && elapsedTimeAdjusted - app.lastUsedByUserElapsedTime + >= mInjector.getAutoRestrictedBucketDelayMs()) { + newBucket = STANDBY_BUCKET_RESTRICTED; + reason = app.lastRestrictReason; + if (DEBUG) { + Slog.d(TAG, "Bringing down to RESTRICTED due to timeout"); + } + } + if (newBucket == STANDBY_BUCKET_RESTRICTED && !mAllowRestrictedBucket) { + newBucket = STANDBY_BUCKET_RARE; + // Leave the reason alone. + if (DEBUG) { + Slog.d(TAG, "Bringing up from RESTRICTED to RARE due to off switch"); + } + } + if (newBucket > minBucket) { + newBucket = minBucket; + // Leave the reason alone. + if (DEBUG) { + Slog.d(TAG, "Bringing up from " + newBucket + " to " + minBucket + + " due to min bucketing"); + } + } + if (DEBUG) { + Slog.d(TAG, " Old bucket=" + oldBucket + + ", newBucket=" + newBucket); + } + if (oldBucket != newBucket || predictionLate) { + mAppIdleHistory.setAppStandbyBucket(packageName, userId, + elapsedRealtime, newBucket, reason); + maybeInformListeners(packageName, userId, elapsedRealtime, + newBucket, reason, false); + } + } + } + } + + /** Returns true if there hasn't been a prediction for the app in a while. */ + private boolean predictionTimedOut(AppIdleHistory.AppUsageHistory app, long elapsedRealtime) { + return app.lastPredictedTime > 0 + && mAppIdleHistory.getElapsedTime(elapsedRealtime) + - app.lastPredictedTime > mPredictionTimeoutMillis; + } + + /** Inform listeners if the bucket has changed since it was last reported to listeners */ + private void maybeInformListeners(String packageName, int userId, + long elapsedRealtime, int bucket, int reason, boolean userStartedInteracting) { + synchronized (mAppIdleLock) { + if (mAppIdleHistory.shouldInformListeners(packageName, userId, + elapsedRealtime, bucket)) { + final StandbyUpdateRecord r = StandbyUpdateRecord.obtain(packageName, userId, + bucket, reason, userStartedInteracting); + if (DEBUG) Slog.d(TAG, "Standby bucket for " + packageName + "=" + bucket); + mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, r)); + } + } + } + + /** + * Evaluates next bucket based on time since last used and the bucketing thresholds. + * @param packageName the app + * @param userId the user + * @param elapsedRealtime as the name suggests, current elapsed time + * @return the bucket for the app, based on time since last used + */ + @GuardedBy("mAppIdleLock") + @StandbyBuckets + private int getBucketForLocked(String packageName, int userId, + long elapsedRealtime) { + int bucketIndex = mAppIdleHistory.getThresholdIndex(packageName, userId, + elapsedRealtime, mAppStandbyScreenThresholds, mAppStandbyElapsedThresholds); + return THRESHOLD_BUCKETS[bucketIndex]; + } + + private void notifyBatteryStats(String packageName, int userId, boolean idle) { + try { + final int uid = mPackageManager.getPackageUidAsUser(packageName, + PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); + if (idle) { + mInjector.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE, + packageName, uid); + } else { + mInjector.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE, + packageName, uid); + } + } catch (PackageManager.NameNotFoundException | RemoteException e) { + } + } + + @Override + public void reportEvent(UsageEvents.Event event, int userId) { + if (!mAppIdleEnabled) return; + final int eventType = event.getEventType(); + if ((eventType == UsageEvents.Event.ACTIVITY_RESUMED + || eventType == UsageEvents.Event.ACTIVITY_PAUSED + || eventType == UsageEvents.Event.SYSTEM_INTERACTION + || eventType == UsageEvents.Event.USER_INTERACTION + || eventType == UsageEvents.Event.NOTIFICATION_SEEN + || eventType == UsageEvents.Event.SLICE_PINNED + || eventType == UsageEvents.Event.SLICE_PINNED_PRIV + || eventType == UsageEvents.Event.FOREGROUND_SERVICE_START)) { + final String pkg = event.getPackageName(); + final List<UserHandle> linkedProfiles = getCrossProfileTargets(pkg, userId); + synchronized (mAppIdleLock) { + final long elapsedRealtime = mInjector.elapsedRealtime(); + reportEventLocked(pkg, eventType, elapsedRealtime, userId); + + final int size = linkedProfiles.size(); + for (int profileIndex = 0; profileIndex < size; profileIndex++) { + final int linkedUserId = linkedProfiles.get(profileIndex).getIdentifier(); + reportEventLocked(pkg, eventType, elapsedRealtime, linkedUserId); + } + } + } + } + + private void reportEventLocked(String pkg, int eventType, long elapsedRealtime, int userId) { + // TODO: Ideally this should call isAppIdleFiltered() to avoid calling back + // about apps that are on some kind of whitelist anyway. + final boolean previouslyIdle = mAppIdleHistory.isIdle( + pkg, userId, elapsedRealtime); + + final AppUsageHistory appHistory = mAppIdleHistory.getAppUsageHistory( + pkg, userId, elapsedRealtime); + final int prevBucket = appHistory.currentBucket; + final int prevBucketReason = appHistory.bucketingReason; + final long nextCheckDelay; + final int subReason = usageEventToSubReason(eventType); + final int reason = REASON_MAIN_USAGE | subReason; + if (eventType == UsageEvents.Event.NOTIFICATION_SEEN + || eventType == UsageEvents.Event.SLICE_PINNED) { + // Mild usage elevates to WORKING_SET but doesn't change usage time. + mAppIdleHistory.reportUsage(appHistory, pkg, userId, + STANDBY_BUCKET_WORKING_SET, subReason, + 0, elapsedRealtime + mNotificationSeenTimeoutMillis); + nextCheckDelay = mNotificationSeenTimeoutMillis; + } else if (eventType == UsageEvents.Event.SYSTEM_INTERACTION) { + mAppIdleHistory.reportUsage(appHistory, pkg, userId, + STANDBY_BUCKET_ACTIVE, subReason, + 0, elapsedRealtime + mSystemInteractionTimeoutMillis); + nextCheckDelay = mSystemInteractionTimeoutMillis; + } else if (eventType == UsageEvents.Event.FOREGROUND_SERVICE_START) { + // Only elevate bucket if this is the first usage of the app + if (prevBucket != STANDBY_BUCKET_NEVER) return; + mAppIdleHistory.reportUsage(appHistory, pkg, userId, + STANDBY_BUCKET_ACTIVE, subReason, + 0, elapsedRealtime + mInitialForegroundServiceStartTimeoutMillis); + nextCheckDelay = mInitialForegroundServiceStartTimeoutMillis; + } else { + mAppIdleHistory.reportUsage(appHistory, pkg, userId, + STANDBY_BUCKET_ACTIVE, subReason, + elapsedRealtime, elapsedRealtime + mStrongUsageTimeoutMillis); + nextCheckDelay = mStrongUsageTimeoutMillis; + } + if (appHistory.currentBucket != prevBucket) { + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkg), + nextCheckDelay); + final boolean userStartedInteracting = + appHistory.currentBucket == STANDBY_BUCKET_ACTIVE + && (prevBucketReason & REASON_MAIN_MASK) != REASON_MAIN_USAGE; + maybeInformListeners(pkg, userId, elapsedRealtime, + appHistory.currentBucket, reason, userStartedInteracting); + } + + if (previouslyIdle) { + notifyBatteryStats(pkg, userId, false); + } + } + + /** + * Note: don't call this with the lock held since it makes calls to other system services. + */ + private @NonNull List<UserHandle> getCrossProfileTargets(String pkg, int userId) { + synchronized (mAppIdleLock) { + if (!mLinkCrossProfileApps) return Collections.emptyList(); + } + return mInjector.getValidCrossProfileTargets(pkg, userId); + } + + private int usageEventToSubReason(int eventType) { + switch (eventType) { + case UsageEvents.Event.ACTIVITY_RESUMED: return REASON_SUB_USAGE_MOVE_TO_FOREGROUND; + case UsageEvents.Event.ACTIVITY_PAUSED: return REASON_SUB_USAGE_MOVE_TO_BACKGROUND; + case UsageEvents.Event.SYSTEM_INTERACTION: return REASON_SUB_USAGE_SYSTEM_INTERACTION; + case UsageEvents.Event.USER_INTERACTION: return REASON_SUB_USAGE_USER_INTERACTION; + case UsageEvents.Event.NOTIFICATION_SEEN: return REASON_SUB_USAGE_NOTIFICATION_SEEN; + case UsageEvents.Event.SLICE_PINNED: return REASON_SUB_USAGE_SLICE_PINNED; + case UsageEvents.Event.SLICE_PINNED_PRIV: return REASON_SUB_USAGE_SLICE_PINNED_PRIV; + case UsageEvents.Event.FOREGROUND_SERVICE_START: + return REASON_SUB_USAGE_FOREGROUND_SERVICE_START; + default: return 0; + } + } + + @VisibleForTesting + void forceIdleState(String packageName, int userId, boolean idle) { + if (!mAppIdleEnabled) return; + + final int appId = getAppId(packageName); + if (appId < 0) return; + final long elapsedRealtime = mInjector.elapsedRealtime(); + + final boolean previouslyIdle = isAppIdleFiltered(packageName, appId, + userId, elapsedRealtime); + final int standbyBucket; + synchronized (mAppIdleLock) { + standbyBucket = mAppIdleHistory.setIdle(packageName, userId, idle, elapsedRealtime); + } + final boolean stillIdle = isAppIdleFiltered(packageName, appId, + userId, elapsedRealtime); + // Inform listeners if necessary + if (previouslyIdle != stillIdle) { + maybeInformListeners(packageName, userId, elapsedRealtime, standbyBucket, + REASON_MAIN_FORCED_BY_USER, false); + if (!stillIdle) { + notifyBatteryStats(packageName, userId, idle); + } + } + } + + @Override + public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) { + synchronized (mAppIdleLock) { + mAppIdleHistory.setLastJobRunTime(packageName, userId, elapsedRealtime); + } + } + + @Override + public long getTimeSinceLastJobRun(String packageName, int userId) { + final long elapsedRealtime = mInjector.elapsedRealtime(); + synchronized (mAppIdleLock) { + return mAppIdleHistory.getTimeSinceLastJobRun(packageName, userId, elapsedRealtime); + } + } + + @Override + public void onUserRemoved(int userId) { + synchronized (mAppIdleLock) { + mAppIdleHistory.onUserRemoved(userId); + synchronized (mActiveAdminApps) { + mActiveAdminApps.remove(userId); + } + } + } + + private boolean isAppIdleUnfiltered(String packageName, int userId, long elapsedRealtime) { + synchronized (mAppIdleLock) { + return mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime); + } + } + + @Override + public void addListener(AppIdleStateChangeListener listener) { + synchronized (mPackageAccessListeners) { + if (!mPackageAccessListeners.contains(listener)) { + mPackageAccessListeners.add(listener); + } + } + } + + @Override + public void removeListener(AppIdleStateChangeListener listener) { + synchronized (mPackageAccessListeners) { + mPackageAccessListeners.remove(listener); + } + } + + @Override + public int getAppId(String packageName) { + try { + ApplicationInfo ai = mPackageManager.getApplicationInfo(packageName, + PackageManager.MATCH_ANY_USER + | PackageManager.MATCH_DISABLED_COMPONENTS); + return ai.uid; + } catch (PackageManager.NameNotFoundException re) { + return -1; + } + } + + @Override + public boolean isAppIdleFiltered(String packageName, int userId, long elapsedRealtime, + boolean shouldObfuscateInstantApps) { + if (shouldObfuscateInstantApps && + mInjector.isPackageEphemeral(userId, packageName)) { + return false; + } + return isAppIdleFiltered(packageName, getAppId(packageName), userId, elapsedRealtime); + } + + @StandbyBuckets + private int getAppMinBucket(String packageName, int userId) { + try { + final int uid = mPackageManager.getPackageUidAsUser(packageName, userId); + return getAppMinBucket(packageName, UserHandle.getAppId(uid), userId); + } catch (PackageManager.NameNotFoundException e) { + // Not a valid package for this user, nothing to do + return STANDBY_BUCKET_NEVER; + } + } + + /** + * Return the lowest bucket this app should ever enter. + */ + @StandbyBuckets + private int getAppMinBucket(String packageName, int appId, int userId) { + if (packageName == null) return STANDBY_BUCKET_NEVER; + // If not enabled at all, of course nobody is ever idle. + if (!mAppIdleEnabled) { + return STANDBY_BUCKET_EXEMPTED; + } + if (appId < Process.FIRST_APPLICATION_UID) { + // System uids never go idle. + return STANDBY_BUCKET_EXEMPTED; + } + if (packageName.equals("android")) { + // Nor does the framework (which should be redundant with the above, but for MR1 we will + // retain this for safety). + return STANDBY_BUCKET_EXEMPTED; + } + if (mSystemServicesReady) { + // We allow all whitelisted apps, including those that don't want to be whitelisted + // for idle mode, because app idle (aka app standby) is really not as big an issue + // for controlling who participates vs. doze mode. + if (mInjector.isNonIdleWhitelisted(packageName)) { + return STANDBY_BUCKET_EXEMPTED; + } + + if (isActiveDeviceAdmin(packageName, userId)) { + return STANDBY_BUCKET_EXEMPTED; + } + + if (isActiveNetworkScorer(packageName)) { + return STANDBY_BUCKET_EXEMPTED; + } + + if (mAppWidgetManager != null + && mInjector.isBoundWidgetPackage(mAppWidgetManager, packageName, userId)) { + return STANDBY_BUCKET_ACTIVE; + } + + if (isDeviceProvisioningPackage(packageName)) { + return STANDBY_BUCKET_EXEMPTED; + } + } + + // Check this last, as it can be the most expensive check + if (isCarrierApp(packageName)) { + return STANDBY_BUCKET_EXEMPTED; + } + + if (isHeadlessSystemApp(packageName)) { + return STANDBY_BUCKET_ACTIVE; + } + + return STANDBY_BUCKET_NEVER; + } + + private boolean isHeadlessSystemApp(String packageName) { + synchronized (mHeadlessSystemApps) { + return mHeadlessSystemApps.contains(packageName); + } + } + + @Override + public boolean isAppIdleFiltered(String packageName, int appId, int userId, + long elapsedRealtime) { + if (getAppMinBucket(packageName, appId, userId) < AppIdleHistory.IDLE_BUCKET_CUTOFF) { + return false; + } else { + synchronized (mAppIdleLock) { + if (!mAppIdleEnabled || mIsCharging) { + return false; + } + } + return isAppIdleUnfiltered(packageName, userId, elapsedRealtime); + } + } + + static boolean isUserUsage(int reason) { + if ((reason & REASON_MAIN_MASK) == REASON_MAIN_USAGE) { + final int subReason = reason & REASON_SUB_MASK; + return subReason == REASON_SUB_USAGE_USER_INTERACTION + || subReason == REASON_SUB_USAGE_MOVE_TO_FOREGROUND; + } + return false; + } + + @Override + public int[] getIdleUidsForUser(int userId) { + if (!mAppIdleEnabled) { + return new int[0]; + } + + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "getIdleUidsForUser"); + + final long elapsedRealtime = mInjector.elapsedRealtime(); + + List<ApplicationInfo> apps; + try { + ParceledListSlice<ApplicationInfo> slice = AppGlobals.getPackageManager() + .getInstalledApplications(/* flags= */ 0, userId); + if (slice == null) { + return new int[0]; + } + apps = slice.getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + // State of each uid. Key is the uid. Value lower 16 bits is the number of apps + // associated with that uid, upper 16 bits is the number of those apps that is idle. + SparseIntArray uidStates = new SparseIntArray(); + + // Now resolve all app state. Iterating over all apps, keeping track of how many + // we find for each uid and how many of those are idle. + for (int i = apps.size() - 1; i >= 0; i--) { + ApplicationInfo ai = apps.get(i); + + // Check whether this app is idle. + boolean idle = isAppIdleFiltered(ai.packageName, UserHandle.getAppId(ai.uid), + userId, elapsedRealtime); + + int index = uidStates.indexOfKey(ai.uid); + if (index < 0) { + uidStates.put(ai.uid, 1 + (idle ? 1<<16 : 0)); + } else { + int value = uidStates.valueAt(index); + uidStates.setValueAt(index, value + 1 + (idle ? 1<<16 : 0)); + } + } + + if (DEBUG) { + Slog.d(TAG, "getIdleUids took " + (mInjector.elapsedRealtime() - elapsedRealtime)); + } + int numIdle = 0; + for (int i = uidStates.size() - 1; i >= 0; i--) { + int value = uidStates.valueAt(i); + if ((value&0x7fff) == (value>>16)) { + numIdle++; + } + } + + int[] res = new int[numIdle]; + numIdle = 0; + for (int i = uidStates.size() - 1; i >= 0; i--) { + int value = uidStates.valueAt(i); + if ((value&0x7fff) == (value>>16)) { + res[numIdle] = uidStates.keyAt(i); + numIdle++; + } + } + + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + + return res; + } + + @Override + public void setAppIdleAsync(String packageName, boolean idle, int userId) { + if (packageName == null || !mAppIdleEnabled) return; + + mHandler.obtainMessage(MSG_FORCE_IDLE_STATE, userId, idle ? 1 : 0, packageName) + .sendToTarget(); + } + + @Override + @StandbyBuckets public int getAppStandbyBucket(String packageName, int userId, + long elapsedRealtime, boolean shouldObfuscateInstantApps) { + if (!mAppIdleEnabled || (shouldObfuscateInstantApps + && mInjector.isPackageEphemeral(userId, packageName))) { + return STANDBY_BUCKET_ACTIVE; + } + + synchronized (mAppIdleLock) { + return mAppIdleHistory.getAppStandbyBucket(packageName, userId, elapsedRealtime); + } + } + + @VisibleForTesting + int getAppStandbyBucketReason(String packageName, int userId, long elapsedRealtime) { + synchronized (mAppIdleLock) { + return mAppIdleHistory.getAppStandbyReason(packageName, userId, elapsedRealtime); + } + } + + @Override + public List<AppStandbyInfo> getAppStandbyBuckets(int userId) { + synchronized (mAppIdleLock) { + return mAppIdleHistory.getAppStandbyBuckets(userId, mAppIdleEnabled); + } + } + + @Override + public void restrictApp(@NonNull String packageName, int userId, + @SystemForcedReasons int restrictReason) { + // If the package is not installed, don't allow the bucket to be set. + if (!mInjector.isPackageInstalled(packageName, 0, userId)) { + Slog.e(TAG, "Tried to restrict uninstalled app: " + packageName); + return; + } + + final int reason = REASON_MAIN_FORCED_BY_SYSTEM | (REASON_SUB_MASK & restrictReason); + final long nowElapsed = mInjector.elapsedRealtime(); + final int bucket = mAllowRestrictedBucket ? STANDBY_BUCKET_RESTRICTED : STANDBY_BUCKET_RARE; + setAppStandbyBucket(packageName, userId, bucket, reason, nowElapsed, false); + } + + @Override + public void setAppStandbyBucket(@NonNull String packageName, int bucket, int userId, + int callingUid, int callingPid) { + setAppStandbyBuckets( + Collections.singletonList(new AppStandbyInfo(packageName, bucket)), + userId, callingUid, callingPid); + } + + @Override + public void setAppStandbyBuckets(@NonNull List<AppStandbyInfo> appBuckets, int userId, + int callingUid, int callingPid) { + userId = ActivityManager.handleIncomingUser( + callingPid, callingUid, userId, false, true, "setAppStandbyBucket", null); + final boolean shellCaller = callingUid == Process.ROOT_UID + || callingUid == Process.SHELL_UID; + final int reason; + // The Settings app runs in the system UID but in a separate process. Assume + // things coming from other processes are due to the user. + if ((UserHandle.isSameApp(callingUid, Process.SYSTEM_UID) && callingPid != Process.myPid()) + || shellCaller) { + reason = REASON_MAIN_FORCED_BY_USER; + } else if (UserHandle.isCore(callingUid)) { + reason = REASON_MAIN_FORCED_BY_SYSTEM; + } else { + reason = REASON_MAIN_PREDICTED; + } + final int packageFlags = PackageManager.MATCH_ANY_USER + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_DIRECT_BOOT_AWARE; + final int numApps = appBuckets.size(); + final long elapsedRealtime = mInjector.elapsedRealtime(); + for (int i = 0; i < numApps; ++i) { + final AppStandbyInfo bucketInfo = appBuckets.get(i); + final String packageName = bucketInfo.mPackageName; + final int bucket = bucketInfo.mStandbyBucket; + if (bucket < STANDBY_BUCKET_ACTIVE || bucket > STANDBY_BUCKET_NEVER) { + throw new IllegalArgumentException("Cannot set the standby bucket to " + bucket); + } + final int packageUid = mInjector.getPackageManagerInternal() + .getPackageUid(packageName, packageFlags, userId); + // Caller cannot set their own standby state + if (packageUid == callingUid) { + throw new IllegalArgumentException("Cannot set your own standby bucket"); + } + if (packageUid < 0) { + throw new IllegalArgumentException( + "Cannot set standby bucket for non existent package (" + packageName + ")"); + } + setAppStandbyBucket(packageName, userId, bucket, reason, elapsedRealtime, shellCaller); + } + } + + @VisibleForTesting + void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket, + int reason) { + setAppStandbyBucket( + packageName, userId, newBucket, reason, mInjector.elapsedRealtime(), false); + } + + private void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket, + int reason, long elapsedRealtime, boolean resetTimeout) { + if (!mAppIdleEnabled) return; + + synchronized (mAppIdleLock) { + // If the package is not installed, don't allow the bucket to be set. + if (!mInjector.isPackageInstalled(packageName, 0, userId)) { + Slog.e(TAG, "Tried to set bucket of uninstalled app: " + packageName); + return; + } + if (newBucket == STANDBY_BUCKET_RESTRICTED && !mAllowRestrictedBucket) { + newBucket = STANDBY_BUCKET_RARE; + } + AppIdleHistory.AppUsageHistory app = mAppIdleHistory.getAppUsageHistory(packageName, + userId, elapsedRealtime); + boolean predicted = (reason & REASON_MAIN_MASK) == REASON_MAIN_PREDICTED; + + // Don't allow changing bucket if higher than ACTIVE + if (app.currentBucket < STANDBY_BUCKET_ACTIVE) return; + + // Don't allow prediction to change from/to NEVER. + if ((app.currentBucket == STANDBY_BUCKET_NEVER || newBucket == STANDBY_BUCKET_NEVER) + && predicted) { + return; + } + + final boolean wasForcedBySystem = + (app.bucketingReason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_SYSTEM; + + // If the bucket was forced, don't allow prediction to override + if (predicted + && ((app.bucketingReason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_USER + || wasForcedBySystem)) { + return; + } + + final boolean isForcedBySystem = + (reason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_SYSTEM; + + if (app.currentBucket == newBucket && wasForcedBySystem && isForcedBySystem) { + mAppIdleHistory + .noteRestrictionAttempt(packageName, userId, elapsedRealtime, reason); + // Keep track of all restricting reasons + reason = REASON_MAIN_FORCED_BY_SYSTEM + | (app.bucketingReason & REASON_SUB_MASK) + | (reason & REASON_SUB_MASK); + mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, + newBucket, reason, resetTimeout); + return; + } + + final boolean isForcedByUser = + (reason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_USER; + + if (app.currentBucket == STANDBY_BUCKET_RESTRICTED) { + if ((app.bucketingReason & REASON_MAIN_MASK) == REASON_MAIN_TIMEOUT) { + if (predicted && newBucket >= STANDBY_BUCKET_RARE) { + // Predicting into RARE or below means we don't expect the user to use the + // app anytime soon, so don't elevate it from RESTRICTED. + return; + } + } else if (!isUserUsage(reason) && !isForcedByUser) { + // If the current bucket is RESTRICTED, only user force or usage should bring + // it out, unless the app was put into the bucket due to timing out. + return; + } + } + + if (newBucket == STANDBY_BUCKET_RESTRICTED) { + mAppIdleHistory + .noteRestrictionAttempt(packageName, userId, elapsedRealtime, reason); + + if (isForcedByUser) { + // Only user force can bypass the delay restriction. If the user forced the + // app into the RESTRICTED bucket, then a toast confirming the action + // shouldn't be surprising. + if (Build.IS_DEBUGGABLE) { + Toast.makeText(mContext, + // Since AppStandbyController sits low in the lock hierarchy, + // make sure not to call out with the lock held. + mHandler.getLooper(), + mContext.getResources().getString( + R.string.as_app_forced_to_restricted_bucket, packageName), + Toast.LENGTH_SHORT) + .show(); + } else { + Slog.i(TAG, packageName + " restricted by user"); + } + } else { + final long timeUntilRestrictPossibleMs = app.lastUsedByUserElapsedTime + + mInjector.getAutoRestrictedBucketDelayMs() - elapsedRealtime; + if (timeUntilRestrictPossibleMs > 0) { + Slog.w(TAG, "Tried to restrict recently used app: " + packageName + + " due to " + reason); + mHandler.sendMessageDelayed( + mHandler.obtainMessage( + MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, packageName), + timeUntilRestrictPossibleMs); + return; + } + } + } + + // If the bucket is required to stay in a higher state for a specified duration, don't + // override unless the duration has passed + if (predicted) { + // Check if the app is within one of the timeouts for forced bucket elevation + final long elapsedTimeAdjusted = mAppIdleHistory.getElapsedTime(elapsedRealtime); + // In case of not using the prediction, just keep track of it for applying after + // ACTIVE or WORKING_SET timeout. + mAppIdleHistory.updateLastPrediction(app, elapsedTimeAdjusted, newBucket); + + if (newBucket > STANDBY_BUCKET_ACTIVE + && app.bucketActiveTimeoutTime > elapsedTimeAdjusted) { + newBucket = STANDBY_BUCKET_ACTIVE; + reason = app.bucketingReason; + if (DEBUG) { + Slog.d(TAG, " Keeping at ACTIVE due to min timeout"); + } + } else if (newBucket > STANDBY_BUCKET_WORKING_SET + && app.bucketWorkingSetTimeoutTime > elapsedTimeAdjusted) { + newBucket = STANDBY_BUCKET_WORKING_SET; + if (app.currentBucket != newBucket) { + reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_ACTIVE_TIMEOUT; + } else { + reason = app.bucketingReason; + } + if (DEBUG) { + Slog.d(TAG, " Keeping at WORKING_SET due to min timeout"); + } + } else if (newBucket == STANDBY_BUCKET_RARE + && mAllowRestrictedBucket + && getBucketForLocked(packageName, userId, elapsedRealtime) + == STANDBY_BUCKET_RESTRICTED) { + // Prediction doesn't think the app will be used anytime soon and + // it's been long enough that it could just time out into restricted, + // so time it out there instead. Using TIMEOUT will allow prediction + // to raise the bucket when it needs to. + newBucket = STANDBY_BUCKET_RESTRICTED; + reason = REASON_MAIN_TIMEOUT; + if (DEBUG) { + Slog.d(TAG, + "Prediction to RARE overridden by timeout into RESTRICTED"); + } + } + } + + // Make sure we don't put the app in a lower bucket than it's supposed to be in. + newBucket = Math.min(newBucket, getAppMinBucket(packageName, userId)); + mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket, + reason, resetTimeout); + } + maybeInformListeners(packageName, userId, elapsedRealtime, newBucket, reason, false); + } + + @VisibleForTesting + boolean isActiveDeviceAdmin(String packageName, int userId) { + synchronized (mActiveAdminApps) { + final Set<String> adminPkgs = mActiveAdminApps.get(userId); + return adminPkgs != null && adminPkgs.contains(packageName); + } + } + + @Override + public void addActiveDeviceAdmin(String adminPkg, int userId) { + synchronized (mActiveAdminApps) { + Set<String> adminPkgs = mActiveAdminApps.get(userId); + if (adminPkgs == null) { + adminPkgs = new ArraySet<>(); + mActiveAdminApps.put(userId, adminPkgs); + } + adminPkgs.add(adminPkg); + } + } + + @Override + public void setActiveAdminApps(Set<String> adminPkgs, int userId) { + synchronized (mActiveAdminApps) { + if (adminPkgs == null) { + mActiveAdminApps.remove(userId); + } else { + mActiveAdminApps.put(userId, adminPkgs); + } + } + } + + @Override + public void onAdminDataAvailable() { + mAdminDataAvailableLatch.countDown(); + } + + /** + * This will only ever be called once - during device boot. + */ + private void waitForAdminData() { + if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) { + ConcurrentUtils.waitForCountDownNoInterrupt(mAdminDataAvailableLatch, + WAIT_FOR_ADMIN_DATA_TIMEOUT_MS, "Wait for admin data"); + } + } + + @VisibleForTesting + Set<String> getActiveAdminAppsForTest(int userId) { + synchronized (mActiveAdminApps) { + return mActiveAdminApps.get(userId); + } + } + + /** + * Returns {@code true} if the supplied package is the device provisioning app. Otherwise, + * returns {@code false}. + */ + private boolean isDeviceProvisioningPackage(String packageName) { + String deviceProvisioningPackage = mContext.getResources().getString( + com.android.internal.R.string.config_deviceProvisioningPackage); + return deviceProvisioningPackage != null && deviceProvisioningPackage.equals(packageName); + } + + private boolean isCarrierApp(String packageName) { + synchronized (mAppIdleLock) { + if (!mHaveCarrierPrivilegedApps) { + fetchCarrierPrivilegedAppsLocked(); + } + if (mCarrierPrivilegedApps != null) { + return mCarrierPrivilegedApps.contains(packageName); + } + return false; + } + } + + @Override + public void clearCarrierPrivilegedApps() { + if (DEBUG) { + Slog.i(TAG, "Clearing carrier privileged apps list"); + } + synchronized (mAppIdleLock) { + mHaveCarrierPrivilegedApps = false; + mCarrierPrivilegedApps = null; // Need to be refetched. + } + } + + @GuardedBy("mAppIdleLock") + private void fetchCarrierPrivilegedAppsLocked() { + TelephonyManager telephonyManager = + mContext.getSystemService(TelephonyManager.class); + mCarrierPrivilegedApps = + telephonyManager.getCarrierPrivilegedPackagesForAllActiveSubscriptions(); + mHaveCarrierPrivilegedApps = true; + if (DEBUG) { + Slog.d(TAG, "apps with carrier privilege " + mCarrierPrivilegedApps); + } + } + + private boolean isActiveNetworkScorer(String packageName) { + // Validity of network scorer cache is limited to a few seconds. Fetch it again + // if longer since query. + // This is a temporary optimization until there's a callback mechanism for changes to network scorer. + final long now = SystemClock.elapsedRealtime(); + if (mCachedNetworkScorer == null + || mCachedNetworkScorerAtMillis < now - NETWORK_SCORER_CACHE_DURATION_MILLIS) { + mCachedNetworkScorer = mInjector.getActiveNetworkScorer(); + mCachedNetworkScorerAtMillis = now; + } + return packageName != null && packageName.equals(mCachedNetworkScorer); + } + + private void informListeners(String packageName, int userId, int bucket, int reason, + boolean userInteraction) { + final boolean idle = bucket >= STANDBY_BUCKET_RARE; + synchronized (mPackageAccessListeners) { + for (AppIdleStateChangeListener listener : mPackageAccessListeners) { + listener.onAppIdleStateChanged(packageName, userId, idle, bucket, reason); + if (userInteraction) { + listener.onUserInteractionStarted(packageName, userId); + } + } + } + } + + private void informParoleStateChanged() { + final boolean paroled = isInParole(); + synchronized (mPackageAccessListeners) { + for (AppIdleStateChangeListener listener : mPackageAccessListeners) { + listener.onParoleStateChanged(paroled); + } + } + } + + + @Override + public void flushToDisk() { + synchronized (mAppIdleLock) { + mAppIdleHistory.writeAppIdleTimes(); + mAppIdleHistory.writeAppIdleDurations(); + } + } + + private boolean isDisplayOn() { + return mInjector.isDefaultDisplayOn(); + } + + @VisibleForTesting + void clearAppIdleForPackage(String packageName, int userId) { + synchronized (mAppIdleLock) { + mAppIdleHistory.clearUsage(packageName, userId); + } + } + + /** + * Remove an app from the {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} + * bucket if it was forced into the bucket by the system because it was buggy. + */ + @VisibleForTesting + void maybeUnrestrictBuggyApp(String packageName, int userId) { + synchronized (mAppIdleLock) { + final long elapsedRealtime = mInjector.elapsedRealtime(); + final AppIdleHistory.AppUsageHistory app = + mAppIdleHistory.getAppUsageHistory(packageName, userId, elapsedRealtime); + if (app.currentBucket != STANDBY_BUCKET_RESTRICTED + || (app.bucketingReason & REASON_MAIN_MASK) != REASON_MAIN_FORCED_BY_SYSTEM) { + return; + } + + final int newBucket; + final int newReason; + if ((app.bucketingReason & REASON_SUB_MASK) == REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY) { + // If bugginess was the only reason the app should be restricted, then lift it out. + newBucket = STANDBY_BUCKET_RARE; + newReason = REASON_MAIN_DEFAULT | REASON_SUB_DEFAULT_APP_UPDATE; + } else { + // There's another reason the app was restricted. Remove the buggy bit and call + // it a day. + newBucket = STANDBY_BUCKET_RESTRICTED; + newReason = app.bucketingReason & ~REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY; + } + mAppIdleHistory.setAppStandbyBucket( + packageName, userId, elapsedRealtime, newBucket, newReason); + } + } + + private void updatePowerWhitelistCache() { + if (mInjector.getBootPhase() < PHASE_SYSTEM_SERVICES_READY) { + return; + } + mInjector.updatePowerWhitelistCache(); + postCheckIdleStates(UserHandle.USER_ALL); + } + + private class PackageReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + final String pkgName = intent.getData().getSchemeSpecificPart(); + final int userId = getSendingUserId(); + if (Intent.ACTION_PACKAGE_ADDED.equals(action) + || Intent.ACTION_PACKAGE_CHANGED.equals(action)) { + clearCarrierPrivilegedApps(); + // ACTION_PACKAGE_ADDED is called even for system app downgrades. + evaluateSystemAppException(pkgName, userId); + mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkgName) + .sendToTarget(); + } + if ((Intent.ACTION_PACKAGE_REMOVED.equals(action) || + Intent.ACTION_PACKAGE_ADDED.equals(action))) { + if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + maybeUnrestrictBuggyApp(pkgName, userId); + } else { + clearAppIdleForPackage(pkgName, userId); + } + } + } + } + + private void evaluateSystemAppException(String packageName, int userId) { + if (!mSystemServicesReady) { + // The app will be evaluated in when services are ready. + return; + } + try { + PackageInfo pi = mPackageManager.getPackageInfoAsUser( + packageName, HEADLESS_APP_CHECK_FLAGS, userId); + evaluateSystemAppException(pi); + } catch (PackageManager.NameNotFoundException e) { + synchronized (mHeadlessSystemApps) { + mHeadlessSystemApps.remove(packageName); + } + } + } + + /** Returns true if the exception status changed. */ + private boolean evaluateSystemAppException(@Nullable PackageInfo pkgInfo) { + if (pkgInfo == null || pkgInfo.applicationInfo == null + || (!pkgInfo.applicationInfo.isSystemApp() + && !pkgInfo.applicationInfo.isUpdatedSystemApp())) { + return false; + } + synchronized (mHeadlessSystemApps) { + if (pkgInfo.activities == null || pkgInfo.activities.length == 0) { + // Headless system app. + return mHeadlessSystemApps.add(pkgInfo.packageName); + } else { + return mHeadlessSystemApps.remove(pkgInfo.packageName); + } + } + } + + /** Call on a system version update to temporarily reset system app buckets. */ + @Override + public void initializeDefaultsForSystemApps(int userId) { + if (!mSystemServicesReady) { + // Do it later, since SettingsProvider wasn't queried yet for app_standby_enabled + mPendingInitializeDefaults = true; + return; + } + Slog.d(TAG, "Initializing defaults for system apps on user " + userId + ", " + + "appIdleEnabled=" + mAppIdleEnabled); + final long elapsedRealtime = mInjector.elapsedRealtime(); + List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser( + PackageManager.MATCH_DISABLED_COMPONENTS, + userId); + final int packageCount = packages.size(); + synchronized (mAppIdleLock) { + for (int i = 0; i < packageCount; i++) { + final PackageInfo pi = packages.get(i); + String packageName = pi.packageName; + if (pi.applicationInfo != null && pi.applicationInfo.isSystemApp()) { + // Mark app as used for 2 hours. After that it can timeout to whatever the + // past usage pattern was. + mAppIdleHistory.reportUsage(packageName, userId, STANDBY_BUCKET_ACTIVE, + REASON_SUB_USAGE_SYSTEM_UPDATE, 0, + elapsedRealtime + mSystemUpdateUsageTimeoutMillis); + } + } + // Immediately persist defaults to disk + mAppIdleHistory.writeAppIdleTimes(userId); + } + } + + /** Call on system boot to get the initial set of headless system apps. */ + private void loadHeadlessSystemAppCache() { + Slog.d(TAG, "Loading headless system app cache. appIdleEnabled=" + mAppIdleEnabled); + final List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser( + HEADLESS_APP_CHECK_FLAGS, UserHandle.USER_SYSTEM); + final int packageCount = packages.size(); + for (int i = 0; i < packageCount; i++) { + PackageInfo pkgInfo = packages.get(i); + if (pkgInfo != null && evaluateSystemAppException(pkgInfo)) { + mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, + UserHandle.USER_SYSTEM, -1, pkgInfo.packageName) + .sendToTarget(); + } + } + } + + @Override + public void postReportContentProviderUsage(String name, String packageName, int userId) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = name; + args.arg2 = packageName; + args.arg3 = userId; + mHandler.obtainMessage(MSG_REPORT_CONTENT_PROVIDER_USAGE, args) + .sendToTarget(); + } + + @Override + public void postReportSyncScheduled(String packageName, int userId, boolean exempted) { + mHandler.obtainMessage(MSG_REPORT_SYNC_SCHEDULED, userId, exempted ? 1 : 0, packageName) + .sendToTarget(); + } + + @Override + public void postReportExemptedSyncStart(String packageName, int userId) { + mHandler.obtainMessage(MSG_REPORT_EXEMPTED_SYNC_START, userId, 0, packageName) + .sendToTarget(); + } + + @Override + public void dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs) { + synchronized (mAppIdleLock) { + mAppIdleHistory.dumpUsers(idpw, userIds, pkgs); + } + } + + @Override + public void dumpState(String[] args, PrintWriter pw) { + synchronized (mAppIdleLock) { + pw.println("Carrier privileged apps (have=" + mHaveCarrierPrivilegedApps + + "): " + mCarrierPrivilegedApps); + } + + pw.println(); + pw.println("Settings:"); + + pw.print(" mCheckIdleIntervalMillis="); + TimeUtils.formatDuration(mCheckIdleIntervalMillis, pw); + pw.println(); + + pw.print(" mStrongUsageTimeoutMillis="); + TimeUtils.formatDuration(mStrongUsageTimeoutMillis, pw); + pw.println(); + pw.print(" mNotificationSeenTimeoutMillis="); + TimeUtils.formatDuration(mNotificationSeenTimeoutMillis, pw); + pw.println(); + pw.print(" mSyncAdapterTimeoutMillis="); + TimeUtils.formatDuration(mSyncAdapterTimeoutMillis, pw); + pw.println(); + pw.print(" mSystemInteractionTimeoutMillis="); + TimeUtils.formatDuration(mSystemInteractionTimeoutMillis, pw); + pw.println(); + pw.print(" mInitialForegroundServiceStartTimeoutMillis="); + TimeUtils.formatDuration(mInitialForegroundServiceStartTimeoutMillis, pw); + pw.println(); + + pw.print(" mPredictionTimeoutMillis="); + TimeUtils.formatDuration(mPredictionTimeoutMillis, pw); + pw.println(); + + pw.print(" mExemptedSyncScheduledNonDozeTimeoutMillis="); + TimeUtils.formatDuration(mExemptedSyncScheduledNonDozeTimeoutMillis, pw); + pw.println(); + pw.print(" mExemptedSyncScheduledDozeTimeoutMillis="); + TimeUtils.formatDuration(mExemptedSyncScheduledDozeTimeoutMillis, pw); + pw.println(); + pw.print(" mExemptedSyncStartTimeoutMillis="); + TimeUtils.formatDuration(mExemptedSyncStartTimeoutMillis, pw); + pw.println(); + pw.print(" mUnexemptedSyncScheduledTimeoutMillis="); + TimeUtils.formatDuration(mUnexemptedSyncScheduledTimeoutMillis, pw); + pw.println(); + + pw.print(" mSystemUpdateUsageTimeoutMillis="); + TimeUtils.formatDuration(mSystemUpdateUsageTimeoutMillis, pw); + pw.println(); + + pw.println(); + pw.print("mAppIdleEnabled="); pw.print(mAppIdleEnabled); + pw.print(" mAllowRestrictedBucket="); + pw.print(mAllowRestrictedBucket); + pw.print(" mIsCharging="); + pw.print(mIsCharging); + pw.println(); + pw.print("mScreenThresholds="); pw.println(Arrays.toString(mAppStandbyScreenThresholds)); + pw.print("mElapsedThresholds="); pw.println(Arrays.toString(mAppStandbyElapsedThresholds)); + pw.println(); + + pw.println("mHeadlessSystemApps=["); + synchronized (mHeadlessSystemApps) { + for (int i = mHeadlessSystemApps.size() - 1; i >= 0; --i) { + pw.print(" "); + pw.print(mHeadlessSystemApps.valueAt(i)); + pw.println(","); + } + } + pw.println("]"); + pw.println(); + + mInjector.dump(pw); + } + + /** + * Injector for interaction with external code. Override methods to provide a mock + * implementation for tests. + * onBootPhase() must be called with at least the PHASE_SYSTEM_SERVICES_READY + */ + static class Injector { + + private final Context mContext; + private final Looper mLooper; + private IBatteryStats mBatteryStats; + private BatteryManager mBatteryManager; + private PackageManagerInternal mPackageManagerInternal; + private DisplayManager mDisplayManager; + private PowerManager mPowerManager; + private IDeviceIdleController mDeviceIdleController; + private CrossProfileAppsInternal mCrossProfileAppsInternal; + int mBootPhase; + /** + * The minimum amount of time required since the last user interaction before an app can be + * automatically placed in the RESTRICTED bucket. + */ + long mAutoRestrictedBucketDelayMs = ONE_DAY; + /** + * Cached set of apps that are power whitelisted, including those not whitelisted from idle. + */ + @GuardedBy("mPowerWhitelistedApps") + private final ArraySet<String> mPowerWhitelistedApps = new ArraySet<>(); + + Injector(Context context, Looper looper) { + mContext = context; + mLooper = looper; + } + + Context getContext() { + return mContext; + } + + Looper getLooper() { + return mLooper; + } + + void onBootPhase(int phase) { + if (phase == PHASE_SYSTEM_SERVICES_READY) { + mDeviceIdleController = IDeviceIdleController.Stub.asInterface( + ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); + mBatteryStats = IBatteryStats.Stub.asInterface( + ServiceManager.getService(BatteryStats.SERVICE_NAME)); + mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); + mDisplayManager = (DisplayManager) mContext.getSystemService( + Context.DISPLAY_SERVICE); + mPowerManager = mContext.getSystemService(PowerManager.class); + mBatteryManager = mContext.getSystemService(BatteryManager.class); + mCrossProfileAppsInternal = LocalServices.getService( + CrossProfileAppsInternal.class); + + final ActivityManager activityManager = + (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + if (activityManager.isLowRamDevice() || ActivityManager.isSmallBatteryDevice()) { + mAutoRestrictedBucketDelayMs = 12 * ONE_HOUR; + } + } + mBootPhase = phase; + } + + int getBootPhase() { + return mBootPhase; + } + + /** + * Returns the elapsed realtime since the device started. Override this + * to control the clock. + * @return elapsed realtime + */ + long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + + long currentTimeMillis() { + return System.currentTimeMillis(); + } + + boolean isAppIdleEnabled() { + final boolean buildFlag = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_enableAutoPowerModes); + final boolean runtimeFlag = Global.getInt(mContext.getContentResolver(), + Global.APP_STANDBY_ENABLED, 1) == 1 + && Global.getInt(mContext.getContentResolver(), + Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED, 1) == 1; + return buildFlag && runtimeFlag; + } + + boolean isCharging() { + return mBatteryManager.isCharging(); + } + + boolean isNonIdleWhitelisted(String packageName) { + if (mBootPhase < PHASE_SYSTEM_SERVICES_READY) { + return false; + } + synchronized (mPowerWhitelistedApps) { + return mPowerWhitelistedApps.contains(packageName); + } + } + + void updatePowerWhitelistCache() { + try { + // Don't call out to DeviceIdleController with the lock held. + final String[] whitelistedPkgs = + mDeviceIdleController.getFullPowerWhitelistExceptIdle(); + synchronized (mPowerWhitelistedApps) { + mPowerWhitelistedApps.clear(); + final int len = whitelistedPkgs.length; + for (int i = 0; i < len; ++i) { + mPowerWhitelistedApps.add(whitelistedPkgs[i]); + } + } + } catch (RemoteException e) { + // Should not happen. + Slog.wtf(TAG, "Failed to get power whitelist", e); + } + } + + boolean isRestrictedBucketEnabled() { + return Global.getInt(mContext.getContentResolver(), + Global.ENABLE_RESTRICTED_BUCKET, + Global.DEFAULT_ENABLE_RESTRICTED_BUCKET) == 1; + } + + File getDataSystemDirectory() { + return Environment.getDataSystemDirectory(); + } + + /** + * Return the minimum amount of time that must have passed since the last user usage before + * an app can be automatically put into the + * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket. + */ + long getAutoRestrictedBucketDelayMs() { + return mAutoRestrictedBucketDelayMs; + } + + void noteEvent(int event, String packageName, int uid) throws RemoteException { + mBatteryStats.noteEvent(event, packageName, uid); + } + + PackageManagerInternal getPackageManagerInternal() { + return mPackageManagerInternal; + } + + boolean isPackageEphemeral(int userId, String packageName) { + return mPackageManagerInternal.isPackageEphemeral(userId, packageName); + } + + boolean isPackageInstalled(String packageName, int flags, int userId) { + return mPackageManagerInternal.getPackageUid(packageName, flags, userId) >= 0; + } + + int[] getRunningUserIds() throws RemoteException { + return ActivityManager.getService().getRunningUserIds(); + } + + boolean isDefaultDisplayOn() { + return mDisplayManager + .getDisplay(Display.DEFAULT_DISPLAY).getState() == Display.STATE_ON; + } + + void registerDisplayListener(DisplayManager.DisplayListener listener, Handler handler) { + mDisplayManager.registerDisplayListener(listener, handler); + } + + String getActiveNetworkScorer() { + NetworkScoreManager nsm = (NetworkScoreManager) mContext.getSystemService( + Context.NETWORK_SCORE_SERVICE); + return nsm.getActiveScorerPackage(); + } + + public boolean isBoundWidgetPackage(AppWidgetManager appWidgetManager, String packageName, + int userId) { + return appWidgetManager.isBoundWidgetPackage(packageName, userId); + } + + String getAppIdleSettings() { + return Global.getString(mContext.getContentResolver(), + Global.APP_IDLE_CONSTANTS); + } + + /** Whether the device is in doze or not. */ + public boolean isDeviceIdleMode() { + return mPowerManager.isDeviceIdleMode(); + } + + public List<UserHandle> getValidCrossProfileTargets(String pkg, int userId) { + final int uid = mPackageManagerInternal.getPackageUidInternal(pkg, 0, userId); + final AndroidPackage aPkg = mPackageManagerInternal.getPackage(uid); + if (uid < 0 + || aPkg == null + || !aPkg.isCrossProfile() + || !mCrossProfileAppsInternal + .verifyUidHasInteractAcrossProfilePermission(pkg, uid)) { + if (uid >= 0 && aPkg == null) { + Slog.wtf(TAG, "Null package retrieved for UID " + uid); + } + return Collections.emptyList(); + } + return mCrossProfileAppsInternal.getTargetUserProfiles(pkg, userId); + } + + void dump(PrintWriter pw) { + pw.println("mPowerWhitelistedApps=["); + synchronized (mPowerWhitelistedApps) { + for (int i = mPowerWhitelistedApps.size() - 1; i >= 0; --i) { + pw.print(" "); + pw.print(mPowerWhitelistedApps.valueAt(i)); + pw.println(","); + } + } + pw.println("]"); + pw.println(); + } + } + + class AppStandbyHandler extends Handler { + + AppStandbyHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_INFORM_LISTENERS: + StandbyUpdateRecord r = (StandbyUpdateRecord) msg.obj; + informListeners(r.packageName, r.userId, r.bucket, r.reason, + r.isUserInteraction); + r.recycle(); + break; + + case MSG_FORCE_IDLE_STATE: + forceIdleState((String) msg.obj, msg.arg1, msg.arg2 == 1); + break; + + case MSG_CHECK_IDLE_STATES: + if (checkIdleStates(msg.arg1) && mAppIdleEnabled) { + mHandler.sendMessageDelayed(mHandler.obtainMessage( + MSG_CHECK_IDLE_STATES, msg.arg1, 0), + mCheckIdleIntervalMillis); + } + break; + + case MSG_ONE_TIME_CHECK_IDLE_STATES: + mHandler.removeMessages(MSG_ONE_TIME_CHECK_IDLE_STATES); + waitForAdminData(); + checkIdleStates(UserHandle.USER_ALL); + break; + + case MSG_REPORT_CONTENT_PROVIDER_USAGE: + SomeArgs args = (SomeArgs) msg.obj; + reportContentProviderUsage((String) args.arg1, // authority name + (String) args.arg2, // package name + (int) args.arg3); // userId + args.recycle(); + break; + + case MSG_PAROLE_STATE_CHANGED: + if (DEBUG) Slog.d(TAG, "Parole state: " + isInParole()); + informParoleStateChanged(); + break; + + case MSG_CHECK_PACKAGE_IDLE_STATE: + checkAndUpdateStandbyState((String) msg.obj, msg.arg1, msg.arg2, + mInjector.elapsedRealtime()); + break; + + case MSG_REPORT_SYNC_SCHEDULED: + final boolean exempted = msg.arg2 > 0 ? true : false; + if (exempted) { + reportExemptedSyncScheduled((String) msg.obj, msg.arg1); + } else { + reportUnexemptedSyncScheduled((String) msg.obj, msg.arg1); + } + break; + + case MSG_REPORT_EXEMPTED_SYNC_START: + reportExemptedSyncStart((String) msg.obj, msg.arg1); + break; + + default: + super.handleMessage(msg); + break; + + } + } + }; + + private class DeviceStateReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case BatteryManager.ACTION_CHARGING: + setChargingState(true); + break; + case BatteryManager.ACTION_DISCHARGING: + setChargingState(false); + break; + case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED: + if (mSystemServicesReady) { + mHandler.post(AppStandbyController.this::updatePowerWhitelistCache); + } + break; + } + } + } + + private final DisplayManager.DisplayListener mDisplayListener + = new DisplayManager.DisplayListener() { + + @Override public void onDisplayAdded(int displayId) { + } + + @Override public void onDisplayRemoved(int displayId) { + } + + @Override public void onDisplayChanged(int displayId) { + if (displayId == Display.DEFAULT_DISPLAY) { + final boolean displayOn = isDisplayOn(); + synchronized (mAppIdleLock) { + mAppIdleHistory.updateDisplay(displayOn, mInjector.elapsedRealtime()); + } + } + } + }; + + /** + * Observe settings changes for {@link Global#APP_IDLE_CONSTANTS}. + */ + private class SettingsObserver extends ContentObserver { + private static final String KEY_SCREEN_TIME_THRESHOLDS = "screen_thresholds"; + private static final String KEY_ELAPSED_TIME_THRESHOLDS = "elapsed_thresholds"; + private static final String KEY_STRONG_USAGE_HOLD_DURATION = "strong_usage_duration"; + private static final String KEY_NOTIFICATION_SEEN_HOLD_DURATION = + "notification_seen_duration"; + private static final String KEY_SYSTEM_UPDATE_HOLD_DURATION = + "system_update_usage_duration"; + private static final String KEY_PREDICTION_TIMEOUT = "prediction_timeout"; + private static final String KEY_SYNC_ADAPTER_HOLD_DURATION = "sync_adapter_duration"; + private static final String KEY_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_HOLD_DURATION = + "exempted_sync_scheduled_nd_duration"; + private static final String KEY_EXEMPTED_SYNC_SCHEDULED_DOZE_HOLD_DURATION = + "exempted_sync_scheduled_d_duration"; + private static final String KEY_EXEMPTED_SYNC_START_HOLD_DURATION = + "exempted_sync_start_duration"; + private static final String KEY_UNEXEMPTED_SYNC_SCHEDULED_HOLD_DURATION = + "unexempted_sync_scheduled_duration"; + private static final String KEY_SYSTEM_INTERACTION_HOLD_DURATION = + "system_interaction_duration"; + private static final String KEY_INITIAL_FOREGROUND_SERVICE_START_HOLD_DURATION = + "initial_foreground_service_start_duration"; + private static final String KEY_AUTO_RESTRICTED_BUCKET_DELAY_MS = + "auto_restricted_bucket_delay_ms"; + private static final String KEY_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS = + "cross_profile_apps_share_standby_buckets"; + public static final long DEFAULT_STRONG_USAGE_TIMEOUT = 1 * ONE_HOUR; + public static final long DEFAULT_NOTIFICATION_TIMEOUT = 12 * ONE_HOUR; + public static final long DEFAULT_SYSTEM_UPDATE_TIMEOUT = 2 * ONE_HOUR; + public static final long DEFAULT_SYSTEM_INTERACTION_TIMEOUT = 10 * ONE_MINUTE; + public static final long DEFAULT_SYNC_ADAPTER_TIMEOUT = 10 * ONE_MINUTE; + public static final long DEFAULT_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_TIMEOUT = 10 * ONE_MINUTE; + public static final long DEFAULT_EXEMPTED_SYNC_SCHEDULED_DOZE_TIMEOUT = 4 * ONE_HOUR; + public static final long DEFAULT_EXEMPTED_SYNC_START_TIMEOUT = 10 * ONE_MINUTE; + public static final long DEFAULT_UNEXEMPTED_SYNC_SCHEDULED_TIMEOUT = 10 * ONE_MINUTE; + public static final long DEFAULT_INITIAL_FOREGROUND_SERVICE_START_TIMEOUT = 30 * ONE_MINUTE; + public static final long DEFAULT_AUTO_RESTRICTED_BUCKET_DELAY_MS = ONE_DAY; + public static final boolean DEFAULT_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS = true; + + private final KeyValueListParser mParser = new KeyValueListParser(','); + + SettingsObserver(Handler handler) { + super(handler); + } + + void registerObserver() { + final ContentResolver cr = mContext.getContentResolver(); + cr.registerContentObserver(Global.getUriFor(Global.APP_IDLE_CONSTANTS), false, this); + cr.registerContentObserver(Global.getUriFor(Global.APP_STANDBY_ENABLED), false, this); + cr.registerContentObserver(Global.getUriFor(Global.ENABLE_RESTRICTED_BUCKET), + false, this); + cr.registerContentObserver(Global.getUriFor(Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED), + false, this); + } + + @Override + public void onChange(boolean selfChange) { + updateSettings(); + postOneTimeCheckIdleStates(); + } + + void updateSettings() { + if (DEBUG) { + Slog.d(TAG, + "appidle=" + Global.getString(mContext.getContentResolver(), + Global.APP_STANDBY_ENABLED)); + Slog.d(TAG, + "adaptivebat=" + Global.getString(mContext.getContentResolver(), + Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED)); + Slog.d(TAG, "appidleconstants=" + Global.getString( + mContext.getContentResolver(), + Global.APP_IDLE_CONSTANTS)); + } + + // Look at global settings for this. + // TODO: Maybe apply different thresholds for different users. + try { + mParser.setString(mInjector.getAppIdleSettings()); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Bad value for app idle settings: " + e.getMessage()); + // fallthrough, mParser is empty and all defaults will be returned. + } + + synchronized (mAppIdleLock) { + + String screenThresholdsValue = mParser.getString(KEY_SCREEN_TIME_THRESHOLDS, null); + mAppStandbyScreenThresholds = parseLongArray(screenThresholdsValue, + SCREEN_TIME_THRESHOLDS, MINIMUM_SCREEN_TIME_THRESHOLDS); + + String elapsedThresholdsValue = mParser.getString(KEY_ELAPSED_TIME_THRESHOLDS, + null); + mAppStandbyElapsedThresholds = parseLongArray(elapsedThresholdsValue, + ELAPSED_TIME_THRESHOLDS, MINIMUM_ELAPSED_TIME_THRESHOLDS); + mCheckIdleIntervalMillis = Math.min(mAppStandbyElapsedThresholds[1] / 4, + COMPRESS_TIME ? ONE_MINUTE : 4 * 60 * ONE_MINUTE); // 4 hours + mStrongUsageTimeoutMillis = mParser.getDurationMillis( + KEY_STRONG_USAGE_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE : DEFAULT_STRONG_USAGE_TIMEOUT); + mNotificationSeenTimeoutMillis = mParser.getDurationMillis( + KEY_NOTIFICATION_SEEN_HOLD_DURATION, + COMPRESS_TIME ? 12 * ONE_MINUTE : DEFAULT_NOTIFICATION_TIMEOUT); + mSystemUpdateUsageTimeoutMillis = mParser.getDurationMillis( + KEY_SYSTEM_UPDATE_HOLD_DURATION, + COMPRESS_TIME ? 2 * ONE_MINUTE : DEFAULT_SYSTEM_UPDATE_TIMEOUT); + mPredictionTimeoutMillis = mParser.getDurationMillis( + KEY_PREDICTION_TIMEOUT, + COMPRESS_TIME ? 10 * ONE_MINUTE : DEFAULT_PREDICTION_TIMEOUT); + mSyncAdapterTimeoutMillis = mParser.getDurationMillis( + KEY_SYNC_ADAPTER_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE : DEFAULT_SYNC_ADAPTER_TIMEOUT); + + mExemptedSyncScheduledNonDozeTimeoutMillis = mParser.getDurationMillis( + KEY_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_HOLD_DURATION, + COMPRESS_TIME ? (ONE_MINUTE / 2) + : DEFAULT_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_TIMEOUT); + + mExemptedSyncScheduledDozeTimeoutMillis = mParser.getDurationMillis( + KEY_EXEMPTED_SYNC_SCHEDULED_DOZE_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE + : DEFAULT_EXEMPTED_SYNC_SCHEDULED_DOZE_TIMEOUT); + + mExemptedSyncStartTimeoutMillis = mParser.getDurationMillis( + KEY_EXEMPTED_SYNC_START_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE + : DEFAULT_EXEMPTED_SYNC_START_TIMEOUT); + + mUnexemptedSyncScheduledTimeoutMillis = mParser.getDurationMillis( + KEY_UNEXEMPTED_SYNC_SCHEDULED_HOLD_DURATION, + COMPRESS_TIME + ? ONE_MINUTE : DEFAULT_UNEXEMPTED_SYNC_SCHEDULED_TIMEOUT); + + mSystemInteractionTimeoutMillis = mParser.getDurationMillis( + KEY_SYSTEM_INTERACTION_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE : DEFAULT_SYSTEM_INTERACTION_TIMEOUT); + + mInitialForegroundServiceStartTimeoutMillis = mParser.getDurationMillis( + KEY_INITIAL_FOREGROUND_SERVICE_START_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE : + DEFAULT_INITIAL_FOREGROUND_SERVICE_START_TIMEOUT); + + mInjector.mAutoRestrictedBucketDelayMs = Math.max( + COMPRESS_TIME ? ONE_MINUTE : 2 * ONE_HOUR, + mParser.getDurationMillis(KEY_AUTO_RESTRICTED_BUCKET_DELAY_MS, + COMPRESS_TIME + ? ONE_MINUTE : DEFAULT_AUTO_RESTRICTED_BUCKET_DELAY_MS)); + + mLinkCrossProfileApps = mParser.getBoolean( + KEY_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS, + DEFAULT_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS); + + mAllowRestrictedBucket = mInjector.isRestrictedBucketEnabled(); + } + + // Check if app_idle_enabled has changed. Do this after getting the rest of the settings + // in case we need to change something based on the new values. + setAppIdleEnabled(mInjector.isAppIdleEnabled()); + } + + long[] parseLongArray(String values, long[] defaults, long[] minValues) { + if (values == null) return defaults; + if (values.isEmpty()) { + // Reset to defaults + return defaults; + } else { + String[] thresholds = values.split("/"); + if (thresholds.length == THRESHOLD_BUCKETS.length) { + if (minValues.length != THRESHOLD_BUCKETS.length) { + Slog.wtf(TAG, "minValues array is the wrong size"); + // Use zeroes as the minimums. + minValues = new long[THRESHOLD_BUCKETS.length]; + } + long[] array = new long[THRESHOLD_BUCKETS.length]; + for (int i = 0; i < THRESHOLD_BUCKETS.length; i++) { + try { + if (thresholds[i].startsWith("P") || thresholds[i].startsWith("p")) { + array[i] = Math.max(minValues[i], + Duration.parse(thresholds[i]).toMillis()); + } else { + array[i] = Math.max(minValues[i], Long.parseLong(thresholds[i])); + } + } catch (NumberFormatException|DateTimeParseException e) { + return defaults; + } + } + return array; + } else { + return defaults; + } + } + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING new file mode 100644 index 000000000000..c5dc51cc9c24 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING @@ -0,0 +1,31 @@ +{ + "presubmit": [ + { + "name": "CtsUsageStatsTestCases", + "options": [ + {"include-filter": "android.app.usage.cts.UsageStatsTest"}, + {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.FlakyTest"} + ] + }, + { + "name": "FrameworksServicesTests", + "options": [ + {"include-filter": "com.android.server.usage"}, + {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.FlakyTest"} + ] + } + ], + "postsubmit": [ + { + "name": "CtsUsageStatsTestCases" + }, + { + "name": "FrameworksServicesTests", + "options": [ + {"include-filter": "com.android.server.usage"} + ] + } + ] +}
\ No newline at end of file diff --git a/apex/media/OWNERS b/apex/media/OWNERS new file mode 100644 index 000000000000..9b853c5dd7d8 --- /dev/null +++ b/apex/media/OWNERS @@ -0,0 +1,4 @@ +andrewlewis@google.com +aquilescanta@google.com +marcone@google.com +sungsoo@google.com diff --git a/apex/media/aidl/Android.bp b/apex/media/aidl/Android.bp new file mode 100644 index 000000000000..409a04897f56 --- /dev/null +++ b/apex/media/aidl/Android.bp @@ -0,0 +1,35 @@ +// +// Copyright 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +filegroup { + name: "stable-mediasession2-aidl-srcs", + srcs: ["stable/**/*.aidl"], + path: "stable", +} + +filegroup { + name: "private-mediasession2-aidl-srcs", + srcs: ["private/**/I*.aidl"], + path: "private", +} + +filegroup { + name: "mediasession2-aidl-srcs", + srcs: [ + ":private-mediasession2-aidl-srcs", + ":stable-mediasession2-aidl-srcs", + ], +} diff --git a/apex/media/aidl/private/android/media/Controller2Link.aidl b/apex/media/aidl/private/android/media/Controller2Link.aidl new file mode 100644 index 000000000000..64edafcb11fc --- /dev/null +++ b/apex/media/aidl/private/android/media/Controller2Link.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +parcelable Controller2Link; diff --git a/apex/media/aidl/private/android/media/IMediaController2.aidl b/apex/media/aidl/private/android/media/IMediaController2.aidl new file mode 100644 index 000000000000..42c6e70529ec --- /dev/null +++ b/apex/media/aidl/private/android/media/IMediaController2.aidl @@ -0,0 +1,39 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.os.Bundle; +import android.os.ResultReceiver; +import android.media.Session2Command; + +/** + * Interface from MediaSession2 to MediaController2. + * <p> + * Keep this interface oneway. Otherwise a malicious app may implement fake version of this, + * and holds calls from session to make session owner(s) frozen. + * @hide + */ + // Code for AML only +oneway interface IMediaController2 { + void notifyConnected(int seq, in Bundle connectionResult) = 0; + void notifyDisconnected(int seq) = 1; + void notifyPlaybackActiveChanged(int seq, boolean playbackActive) = 2; + void sendSessionCommand(int seq, in Session2Command command, in Bundle args, + in ResultReceiver resultReceiver) = 3; + void cancelSessionCommand(int seq) = 4; + // Next Id : 5 +} diff --git a/apex/media/aidl/private/android/media/IMediaSession2.aidl b/apex/media/aidl/private/android/media/IMediaSession2.aidl new file mode 100644 index 000000000000..26e717b39afc --- /dev/null +++ b/apex/media/aidl/private/android/media/IMediaSession2.aidl @@ -0,0 +1,39 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.os.Bundle; +import android.os.ResultReceiver; +import android.media.Controller2Link; +import android.media.Session2Command; + +/** + * Interface from MediaController2 to MediaSession2. + * <p> + * Keep this interface oneway. Otherwise a malicious app may implement fake version of this, + * and holds calls from session to make session owner(s) frozen. + * @hide + */ + // Code for AML only +oneway interface IMediaSession2 { + void connect(in Controller2Link caller, int seq, in Bundle connectionRequest) = 0; + void disconnect(in Controller2Link caller, int seq) = 1; + void sendSessionCommand(in Controller2Link caller, int seq, in Session2Command sessionCommand, + in Bundle args, in ResultReceiver resultReceiver) = 2; + void cancelSessionCommand(in Controller2Link caller, int seq) = 3; + // Next Id : 4 +} diff --git a/apex/media/aidl/private/android/media/IMediaSession2Service.aidl b/apex/media/aidl/private/android/media/IMediaSession2Service.aidl new file mode 100644 index 000000000000..10ac1be0a36e --- /dev/null +++ b/apex/media/aidl/private/android/media/IMediaSession2Service.aidl @@ -0,0 +1,32 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.os.Bundle; +import android.media.Controller2Link; + +/** + * Interface from MediaController2 to MediaSession2Service. + * <p> + * Keep this interface oneway. Otherwise a malicious app may implement fake version of this, + * and holds calls from controller to make controller owner(s) frozen. + * @hide + */ +oneway interface IMediaSession2Service { + void connect(in Controller2Link caller, int seq, in Bundle connectionRequest) = 0; + // Next Id : 1 +} diff --git a/apex/media/aidl/private/android/media/Session2Command.aidl b/apex/media/aidl/private/android/media/Session2Command.aidl new file mode 100644 index 000000000000..43a7b123ed29 --- /dev/null +++ b/apex/media/aidl/private/android/media/Session2Command.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +parcelable Session2Command; diff --git a/apex/media/aidl/stable/android/media/Session2Token.aidl b/apex/media/aidl/stable/android/media/Session2Token.aidl new file mode 100644 index 000000000000..c5980e9e77fd --- /dev/null +++ b/apex/media/aidl/stable/android/media/Session2Token.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +parcelable Session2Token; diff --git a/apex/media/framework/Android.bp b/apex/media/framework/Android.bp new file mode 100644 index 000000000000..ce4b030467a7 --- /dev/null +++ b/apex/media/framework/Android.bp @@ -0,0 +1,110 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +java_library { + name: "updatable-media", + + srcs: [ + ":updatable-media-srcs", + ], + + permitted_packages: [ + "android.media", + ], + + optimize: { + enabled: true, + shrink: true, + proguard_flags_files: ["updatable-media-proguard.flags"], + }, + + installable: true, + + sdk_version: "module_current", + libs: [ + "framework_media_annotation", + ], + + static_libs: [ + "exoplayer2-extractor" + ], + jarjar_rules: "jarjar_rules.txt", + + plugins: ["java_api_finder"], + + hostdex: true, // for hiddenapi check + apex_available: [ + "com.android.media", + "test_com.android.media", + ], + min_sdk_version: "29", +} + +filegroup { + name: "updatable-media-srcs", + srcs: [ + ":mediaparser-srcs", + ":mediasession2-java-srcs", + ":mediasession2-aidl-srcs", + ], +} + +filegroup { + name: "mediasession2-java-srcs", + srcs: [ + "java/android/media/Controller2Link.java", + "java/android/media/MediaConstants.java", + "java/android/media/MediaController2.java", + "java/android/media/MediaSession2.java", + "java/android/media/MediaSession2Service.java", + "java/android/media/Session2Command.java", + "java/android/media/Session2CommandGroup.java", + "java/android/media/Session2Link.java", + "java/android/media/Session2Token.java", + ], + path: "java", +} + +filegroup { + name: "mediaparser-srcs", + srcs: [ + "java/android/media/MediaParser.java" + ], + path: "java", +} + +java_sdk_library { + name: "framework-media", + defaults: ["framework-module-defaults"], + + // This is only used to define the APIs for updatable-media. + api_only: true, + + srcs: [ + ":updatable-media-srcs", + ], + + libs: [ + "framework_media_annotation", + ], + impl_library_visibility: ["//frameworks/av/apex:__subpackages__"], +} + + +java_library { + name: "framework_media_annotation", + srcs: [":framework-media-annotation-srcs"], + installable: false, + sdk_version: "core_current", +} diff --git a/apex/media/framework/TEST_MAPPING b/apex/media/framework/TEST_MAPPING new file mode 100644 index 000000000000..ec2d2e2e756c --- /dev/null +++ b/apex/media/framework/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "CtsMediaParserTestCases" + } + ] +} diff --git a/apex/media/framework/api/current.txt b/apex/media/framework/api/current.txt new file mode 100644 index 000000000000..0cc8e52f6411 --- /dev/null +++ b/apex/media/framework/api/current.txt @@ -0,0 +1,226 @@ +// Signature format: 2.0 +package android.media { + + public class MediaController2 implements java.lang.AutoCloseable { + method public void cancelSessionCommand(@NonNull Object); + method public void close(); + method @Nullable public android.media.Session2Token getConnectedToken(); + method public boolean isPlaybackActive(); + method @NonNull public Object sendSessionCommand(@NonNull android.media.Session2Command, @Nullable android.os.Bundle); + } + + public static final class MediaController2.Builder { + ctor public MediaController2.Builder(@NonNull android.content.Context, @NonNull android.media.Session2Token); + method @NonNull public android.media.MediaController2 build(); + method @NonNull public android.media.MediaController2.Builder setConnectionHints(@NonNull android.os.Bundle); + method @NonNull public android.media.MediaController2.Builder setControllerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaController2.ControllerCallback); + } + + public abstract static class MediaController2.ControllerCallback { + ctor public MediaController2.ControllerCallback(); + method public void onCommandResult(@NonNull android.media.MediaController2, @NonNull Object, @NonNull android.media.Session2Command, @NonNull android.media.Session2Command.Result); + method public void onConnected(@NonNull android.media.MediaController2, @NonNull android.media.Session2CommandGroup); + method public void onDisconnected(@NonNull android.media.MediaController2); + method public void onPlaybackActiveChanged(@NonNull android.media.MediaController2, boolean); + method @Nullable public android.media.Session2Command.Result onSessionCommand(@NonNull android.media.MediaController2, @NonNull android.media.Session2Command, @Nullable android.os.Bundle); + } + + public final class MediaParser { + method public boolean advance(@NonNull android.media.MediaParser.SeekableInputReader) throws java.io.IOException; + method @NonNull public static android.media.MediaParser create(@NonNull android.media.MediaParser.OutputConsumer, @NonNull java.lang.String...); + method @NonNull public static android.media.MediaParser createByName(@NonNull String, @NonNull android.media.MediaParser.OutputConsumer); + method @NonNull public String getParserName(); + method @NonNull public static java.util.List<java.lang.String> getParserNames(@NonNull android.media.MediaFormat); + method public void release(); + method public void seek(@NonNull android.media.MediaParser.SeekPoint); + method @NonNull public android.media.MediaParser setParameter(@NonNull String, @NonNull Object); + method public boolean supportsParameter(@NonNull String); + field public static final String PARAMETER_ADTS_ENABLE_CBR_SEEKING = "android.media.mediaparser.adts.enableCbrSeeking"; + field public static final String PARAMETER_AMR_ENABLE_CBR_SEEKING = "android.media.mediaparser.amr.enableCbrSeeking"; + field public static final String PARAMETER_FLAC_DISABLE_ID3 = "android.media.mediaparser.flac.disableId3"; + field public static final String PARAMETER_MATROSKA_DISABLE_CUES_SEEKING = "android.media.mediaparser.matroska.disableCuesSeeking"; + field public static final String PARAMETER_MP3_DISABLE_ID3 = "android.media.mediaparser.mp3.disableId3"; + field public static final String PARAMETER_MP3_ENABLE_CBR_SEEKING = "android.media.mediaparser.mp3.enableCbrSeeking"; + field public static final String PARAMETER_MP3_ENABLE_INDEX_SEEKING = "android.media.mediaparser.mp3.enableIndexSeeking"; + field public static final String PARAMETER_MP4_IGNORE_EDIT_LISTS = "android.media.mediaparser.mp4.ignoreEditLists"; + field public static final String PARAMETER_MP4_IGNORE_TFDT_BOX = "android.media.mediaparser.mp4.ignoreTfdtBox"; + field public static final String PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES = "android.media.mediaparser.mp4.treatVideoFramesAsKeyframes"; + field public static final String PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES = "android.media.mediaparser.ts.allowNonIdrAvcKeyframes"; + field public static final String PARAMETER_TS_DETECT_ACCESS_UNITS = "android.media.mediaparser.ts.ignoreDetectAccessUnits"; + field public static final String PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS = "android.media.mediaparser.ts.enableHdmvDtsAudioStreams"; + field public static final String PARAMETER_TS_IGNORE_AAC_STREAM = "android.media.mediaparser.ts.ignoreAacStream"; + field public static final String PARAMETER_TS_IGNORE_AVC_STREAM = "android.media.mediaparser.ts.ignoreAvcStream"; + field public static final String PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM = "android.media.mediaparser.ts.ignoreSpliceInfoStream"; + field public static final String PARAMETER_TS_MODE = "android.media.mediaparser.ts.mode"; + field public static final String PARSER_NAME_AC3 = "android.media.mediaparser.Ac3Parser"; + field public static final String PARSER_NAME_AC4 = "android.media.mediaparser.Ac4Parser"; + field public static final String PARSER_NAME_ADTS = "android.media.mediaparser.AdtsParser"; + field public static final String PARSER_NAME_AMR = "android.media.mediaparser.AmrParser"; + field public static final String PARSER_NAME_FLAC = "android.media.mediaparser.FlacParser"; + field public static final String PARSER_NAME_FLV = "android.media.mediaparser.FlvParser"; + field public static final String PARSER_NAME_FMP4 = "android.media.mediaparser.FragmentedMp4Parser"; + field public static final String PARSER_NAME_MATROSKA = "android.media.mediaparser.MatroskaParser"; + field public static final String PARSER_NAME_MP3 = "android.media.mediaparser.Mp3Parser"; + field public static final String PARSER_NAME_MP4 = "android.media.mediaparser.Mp4Parser"; + field public static final String PARSER_NAME_OGG = "android.media.mediaparser.OggParser"; + field public static final String PARSER_NAME_PS = "android.media.mediaparser.PsParser"; + field public static final String PARSER_NAME_TS = "android.media.mediaparser.TsParser"; + field public static final String PARSER_NAME_UNKNOWN = "android.media.mediaparser.UNKNOWN"; + field public static final String PARSER_NAME_WAV = "android.media.mediaparser.WavParser"; + field public static final int SAMPLE_FLAG_DECODE_ONLY = -2147483648; // 0x80000000 + field public static final int SAMPLE_FLAG_ENCRYPTED = 1073741824; // 0x40000000 + field public static final int SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA = 268435456; // 0x10000000 + field public static final int SAMPLE_FLAG_KEY_FRAME = 1; // 0x1 + field public static final int SAMPLE_FLAG_LAST_SAMPLE = 536870912; // 0x20000000 + } + + public static interface MediaParser.InputReader { + method public long getLength(); + method public long getPosition(); + method public int read(@NonNull byte[], int, int) throws java.io.IOException; + } + + public static interface MediaParser.OutputConsumer { + method public void onSampleCompleted(int, long, int, int, int, @Nullable android.media.MediaCodec.CryptoInfo); + method public void onSampleDataFound(int, @NonNull android.media.MediaParser.InputReader) throws java.io.IOException; + method public void onSeekMapFound(@NonNull android.media.MediaParser.SeekMap); + method public void onTrackCountFound(int); + method public void onTrackDataFound(int, @NonNull android.media.MediaParser.TrackData); + } + + public static final class MediaParser.ParsingException extends java.io.IOException { + } + + public static final class MediaParser.SeekMap { + method public long getDurationMicros(); + method @NonNull public android.util.Pair<android.media.MediaParser.SeekPoint,android.media.MediaParser.SeekPoint> getSeekPoints(long); + method public boolean isSeekable(); + field public static final int UNKNOWN_DURATION = -2147483648; // 0x80000000 + } + + public static final class MediaParser.SeekPoint { + field @NonNull public static final android.media.MediaParser.SeekPoint START; + field public final long position; + field public final long timeMicros; + } + + public static interface MediaParser.SeekableInputReader extends android.media.MediaParser.InputReader { + method public void seekToPosition(long); + } + + public static final class MediaParser.TrackData { + field @Nullable public final android.media.DrmInitData drmInitData; + field @NonNull public final android.media.MediaFormat mediaFormat; + } + + public static final class MediaParser.UnrecognizedInputFormatException extends java.io.IOException { + } + + public class MediaSession2 implements java.lang.AutoCloseable { + method public void broadcastSessionCommand(@NonNull android.media.Session2Command, @Nullable android.os.Bundle); + method public void cancelSessionCommand(@NonNull android.media.MediaSession2.ControllerInfo, @NonNull Object); + method public void close(); + method @NonNull public java.util.List<android.media.MediaSession2.ControllerInfo> getConnectedControllers(); + method @NonNull public String getId(); + method @NonNull public android.media.Session2Token getToken(); + method public boolean isPlaybackActive(); + method @NonNull public Object sendSessionCommand(@NonNull android.media.MediaSession2.ControllerInfo, @NonNull android.media.Session2Command, @Nullable android.os.Bundle); + method public void setPlaybackActive(boolean); + } + + public static final class MediaSession2.Builder { + ctor public MediaSession2.Builder(@NonNull android.content.Context); + method @NonNull public android.media.MediaSession2 build(); + method @NonNull public android.media.MediaSession2.Builder setExtras(@NonNull android.os.Bundle); + method @NonNull public android.media.MediaSession2.Builder setId(@NonNull String); + method @NonNull public android.media.MediaSession2.Builder setSessionActivity(@Nullable android.app.PendingIntent); + method @NonNull public android.media.MediaSession2.Builder setSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaSession2.SessionCallback); + } + + public static final class MediaSession2.ControllerInfo { + method @NonNull public android.os.Bundle getConnectionHints(); + method @NonNull public String getPackageName(); + method @NonNull public android.media.session.MediaSessionManager.RemoteUserInfo getRemoteUserInfo(); + method public int getUid(); + } + + public abstract static class MediaSession2.SessionCallback { + ctor public MediaSession2.SessionCallback(); + method public void onCommandResult(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo, @NonNull Object, @NonNull android.media.Session2Command, @NonNull android.media.Session2Command.Result); + method @Nullable public android.media.Session2CommandGroup onConnect(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo); + method public void onDisconnected(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo); + method public void onPostConnect(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo); + method @Nullable public android.media.Session2Command.Result onSessionCommand(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo, @NonNull android.media.Session2Command, @Nullable android.os.Bundle); + } + + public abstract class MediaSession2Service extends android.app.Service { + ctor public MediaSession2Service(); + method public final void addSession(@NonNull android.media.MediaSession2); + method @NonNull public final java.util.List<android.media.MediaSession2> getSessions(); + method @CallSuper @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent); + method @Nullable public abstract android.media.MediaSession2 onGetSession(@NonNull android.media.MediaSession2.ControllerInfo); + method @Nullable public abstract android.media.MediaSession2Service.MediaNotification onUpdateNotification(@NonNull android.media.MediaSession2); + method public final void removeSession(@NonNull android.media.MediaSession2); + field public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service"; + } + + public static class MediaSession2Service.MediaNotification { + ctor public MediaSession2Service.MediaNotification(int, @NonNull android.app.Notification); + method @NonNull public android.app.Notification getNotification(); + method public int getNotificationId(); + } + + public final class Session2Command implements android.os.Parcelable { + ctor public Session2Command(int); + ctor public Session2Command(@NonNull String, @Nullable android.os.Bundle); + method public int describeContents(); + method public int getCommandCode(); + method @Nullable public String getCustomAction(); + method @Nullable public android.os.Bundle getCustomExtras(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int COMMAND_CODE_CUSTOM = 0; // 0x0 + field @NonNull public static final android.os.Parcelable.Creator<android.media.Session2Command> CREATOR; + } + + public static final class Session2Command.Result { + ctor public Session2Command.Result(int, @Nullable android.os.Bundle); + method public int getResultCode(); + method @Nullable public android.os.Bundle getResultData(); + field public static final int RESULT_ERROR_UNKNOWN_ERROR = -1; // 0xffffffff + field public static final int RESULT_INFO_SKIPPED = 1; // 0x1 + field public static final int RESULT_SUCCESS = 0; // 0x0 + } + + public final class Session2CommandGroup implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.Set<android.media.Session2Command> getCommands(); + method public boolean hasCommand(@NonNull android.media.Session2Command); + method public boolean hasCommand(int); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.media.Session2CommandGroup> CREATOR; + } + + public static final class Session2CommandGroup.Builder { + ctor public Session2CommandGroup.Builder(); + ctor public Session2CommandGroup.Builder(@NonNull android.media.Session2CommandGroup); + method @NonNull public android.media.Session2CommandGroup.Builder addCommand(@NonNull android.media.Session2Command); + method @NonNull public android.media.Session2CommandGroup build(); + method @NonNull public android.media.Session2CommandGroup.Builder removeCommand(@NonNull android.media.Session2Command); + } + + public final class Session2Token implements android.os.Parcelable { + ctor public Session2Token(@NonNull android.content.Context, @NonNull android.content.ComponentName); + method public int describeContents(); + method @NonNull public android.os.Bundle getExtras(); + method @NonNull public String getPackageName(); + method @Nullable public String getServiceName(); + method public int getType(); + method public int getUid(); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.media.Session2Token> CREATOR; + field public static final int TYPE_SESSION = 0; // 0x0 + field public static final int TYPE_SESSION_SERVICE = 1; // 0x1 + } + +} + diff --git a/apex/media/framework/api/module-lib-current.txt b/apex/media/framework/api/module-lib-current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/media/framework/api/module-lib-current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/media/framework/api/module-lib-removed.txt b/apex/media/framework/api/module-lib-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/media/framework/api/module-lib-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/media/framework/api/removed.txt b/apex/media/framework/api/removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/media/framework/api/removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/media/framework/api/system-current.txt b/apex/media/framework/api/system-current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/media/framework/api/system-current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/media/framework/api/system-removed.txt b/apex/media/framework/api/system-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/media/framework/api/system-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/media/framework/jarjar_rules.txt b/apex/media/framework/jarjar_rules.txt new file mode 100644 index 000000000000..d89d9d3343d1 --- /dev/null +++ b/apex/media/framework/jarjar_rules.txt @@ -0,0 +1 @@ +rule com.google.android.exoplayer2.** android.media.internal.exo.@1 diff --git a/apex/media/framework/java/android/media/BufferingParams.java b/apex/media/framework/java/android/media/BufferingParams.java new file mode 100644 index 000000000000..04af02874bbd --- /dev/null +++ b/apex/media/framework/java/android/media/BufferingParams.java @@ -0,0 +1,188 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Structure for source buffering management params. + * + * Used by {@link MediaPlayer#getBufferingParams()} and + * {@link MediaPlayer#setBufferingParams(BufferingParams)} + * to control source buffering behavior. + * + * <p>There are two stages of source buffering in {@link MediaPlayer}: initial buffering + * (when {@link MediaPlayer} is being prepared) and rebuffering (when {@link MediaPlayer} + * is playing back source). {@link BufferingParams} includes corresponding marks for each + * stage of source buffering. The marks are time based (in milliseconds). + * + * <p>{@link MediaPlayer} source component has default marks which can be queried by + * calling {@link MediaPlayer#getBufferingParams()} before any change is made by + * {@link MediaPlayer#setBufferingParams()}. + * <ul> + * <li><strong>initial buffering:</strong> initialMarkMs is used when + * {@link MediaPlayer} is being prepared. When cached data amount exceeds this mark + * {@link MediaPlayer} is prepared. </li> + * <li><strong>rebuffering during playback:</strong> resumePlaybackMarkMs is used when + * {@link MediaPlayer} is playing back content. + * <ul> + * <li> {@link MediaPlayer} has internal mark, namely pausePlaybackMarkMs, to decide when + * to pause playback if cached data amount runs low. This internal mark varies based on + * type of data source. </li> + * <li> When cached data amount exceeds resumePlaybackMarkMs, {@link MediaPlayer} will + * resume playback if it has been paused due to low cached data amount. The internal mark + * pausePlaybackMarkMs shall be less than resumePlaybackMarkMs. </li> + * <li> {@link MediaPlayer} has internal mark, namely pauseRebufferingMarkMs, to decide + * when to pause rebuffering. Apparently, this internal mark shall be no less than + * resumePlaybackMarkMs. </li> + * <li> {@link MediaPlayer} has internal mark, namely resumeRebufferingMarkMs, to decide + * when to resume buffering. This internal mark varies based on type of data source. This + * mark shall be larger than pausePlaybackMarkMs, and less than pauseRebufferingMarkMs. + * </li> + * </ul> </li> + * </ul> + * <p>Users should use {@link Builder} to change {@link BufferingParams}. + * @hide + */ +public final class BufferingParams implements Parcelable { + private static final int BUFFERING_NO_MARK = -1; + + // params + private int mInitialMarkMs = BUFFERING_NO_MARK; + + private int mResumePlaybackMarkMs = BUFFERING_NO_MARK; + + private BufferingParams() { + } + + /** + * Return initial buffering mark in milliseconds. + * @return initial buffering mark in milliseconds + */ + public int getInitialMarkMs() { + return mInitialMarkMs; + } + + /** + * Return the mark in milliseconds for resuming playback. + * @return the mark for resuming playback in milliseconds + */ + public int getResumePlaybackMarkMs() { + return mResumePlaybackMarkMs; + } + + /** + * Builder class for {@link BufferingParams} objects. + * <p> Here is an example where <code>Builder</code> is used to define the + * {@link BufferingParams} to be used by a {@link MediaPlayer} instance: + * + * <pre class="prettyprint"> + * BufferingParams myParams = mediaplayer.getDefaultBufferingParams(); + * myParams = new BufferingParams.Builder(myParams) + * .setInitialMarkMs(10000) + * .setResumePlaybackMarkMs(15000) + * .build(); + * mediaplayer.setBufferingParams(myParams); + * </pre> + */ + public static class Builder { + private int mInitialMarkMs = BUFFERING_NO_MARK; + private int mResumePlaybackMarkMs = BUFFERING_NO_MARK; + + /** + * Constructs a new Builder with the defaults. + * By default, all marks are -1. + */ + public Builder() { + } + + /** + * Constructs a new Builder from a given {@link BufferingParams} instance + * @param bp the {@link BufferingParams} object whose data will be reused + * in the new Builder. + */ + public Builder(BufferingParams bp) { + mInitialMarkMs = bp.mInitialMarkMs; + mResumePlaybackMarkMs = bp.mResumePlaybackMarkMs; + } + + /** + * Combines all of the fields that have been set and return a new + * {@link BufferingParams} object. <code>IllegalStateException</code> will be + * thrown if there is conflict between fields. + * @return a new {@link BufferingParams} object + */ + public BufferingParams build() { + BufferingParams bp = new BufferingParams(); + bp.mInitialMarkMs = mInitialMarkMs; + bp.mResumePlaybackMarkMs = mResumePlaybackMarkMs; + + return bp; + } + + /** + * Sets the time based mark in milliseconds for initial buffering. + * @param markMs time based mark in milliseconds + * @return the same Builder instance. + */ + public Builder setInitialMarkMs(int markMs) { + mInitialMarkMs = markMs; + return this; + } + + /** + * Sets the time based mark in milliseconds for resuming playback. + * @param markMs time based mark in milliseconds for resuming playback + * @return the same Builder instance. + */ + public Builder setResumePlaybackMarkMs(int markMs) { + mResumePlaybackMarkMs = markMs; + return this; + } + } + + private BufferingParams(Parcel in) { + mInitialMarkMs = in.readInt(); + mResumePlaybackMarkMs = in.readInt(); + } + + public static final @android.annotation.NonNull Parcelable.Creator<BufferingParams> CREATOR = + new Parcelable.Creator<BufferingParams>() { + @Override + public BufferingParams createFromParcel(Parcel in) { + return new BufferingParams(in); + } + + @Override + public BufferingParams[] newArray(int size) { + return new BufferingParams[size]; + } + }; + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mInitialMarkMs); + dest.writeInt(mResumePlaybackMarkMs); + } +} diff --git a/apex/media/framework/java/android/media/Controller2Link.java b/apex/media/framework/java/android/media/Controller2Link.java new file mode 100644 index 000000000000..04185e79b0ad --- /dev/null +++ b/apex/media/framework/java/android/media/Controller2Link.java @@ -0,0 +1,216 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.os.Binder; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ResultReceiver; + +import java.util.Objects; + +/** + * Handles incoming commands from {@link MediaSession2} to both {@link MediaController2}. + * @hide + */ +// @SystemApi +public final class Controller2Link implements Parcelable { + private static final String TAG = "Controller2Link"; + private static final boolean DEBUG = MediaController2.DEBUG; + + public static final @android.annotation.NonNull Parcelable.Creator<Controller2Link> CREATOR = + new Parcelable.Creator<Controller2Link>() { + @Override + public Controller2Link createFromParcel(Parcel in) { + return new Controller2Link(in); + } + + @Override + public Controller2Link[] newArray(int size) { + return new Controller2Link[size]; + } + }; + + + private final MediaController2 mController; + private final IMediaController2 mIController; + + public Controller2Link(MediaController2 controller) { + mController = controller; + mIController = new Controller2Stub(); + } + + Controller2Link(Parcel in) { + mController = null; + mIController = IMediaController2.Stub.asInterface(in.readStrongBinder()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mIController.asBinder()); + } + + @Override + public int hashCode() { + return mIController.asBinder().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Controller2Link)) { + return false; + } + Controller2Link other = (Controller2Link) obj; + return Objects.equals(mIController.asBinder(), other.mIController.asBinder()); + } + + /** Interface method for IMediaController2.notifyConnected */ + public void notifyConnected(int seq, Bundle connectionResult) { + try { + mIController.notifyConnected(seq, connectionResult); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** Interface method for IMediaController2.notifyDisonnected */ + public void notifyDisconnected(int seq) { + try { + mIController.notifyDisconnected(seq); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** Interface method for IMediaController2.notifyPlaybackActiveChanged */ + public void notifyPlaybackActiveChanged(int seq, boolean playbackActive) { + try { + mIController.notifyPlaybackActiveChanged(seq, playbackActive); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** Interface method for IMediaController2.sendSessionCommand */ + public void sendSessionCommand(int seq, Session2Command command, Bundle args, + ResultReceiver resultReceiver) { + try { + mIController.sendSessionCommand(seq, command, args, resultReceiver); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** Interface method for IMediaController2.cancelSessionCommand */ + public void cancelSessionCommand(int seq) { + try { + mIController.cancelSessionCommand(seq); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** Stub implementation for IMediaController2.notifyConnected */ + public void onConnected(int seq, Bundle connectionResult) { + if (connectionResult == null) { + onDisconnected(seq); + return; + } + mController.onConnected(seq, connectionResult); + } + + /** Stub implementation for IMediaController2.notifyDisonnected */ + public void onDisconnected(int seq) { + mController.onDisconnected(seq); + } + + /** Stub implementation for IMediaController2.notifyPlaybackActiveChanged */ + public void onPlaybackActiveChanged(int seq, boolean playbackActive) { + mController.onPlaybackActiveChanged(seq, playbackActive); + } + + /** Stub implementation for IMediaController2.sendSessionCommand */ + public void onSessionCommand(int seq, Session2Command command, Bundle args, + ResultReceiver resultReceiver) { + mController.onSessionCommand(seq, command, args, resultReceiver); + } + + /** Stub implementation for IMediaController2.cancelSessionCommand */ + public void onCancelCommand(int seq) { + mController.onCancelCommand(seq); + } + + private class Controller2Stub extends IMediaController2.Stub { + @Override + public void notifyConnected(int seq, Bundle connectionResult) { + final long token = Binder.clearCallingIdentity(); + try { + Controller2Link.this.onConnected(seq, connectionResult); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void notifyDisconnected(int seq) { + final long token = Binder.clearCallingIdentity(); + try { + Controller2Link.this.onDisconnected(seq); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void notifyPlaybackActiveChanged(int seq, boolean playbackActive) { + final long token = Binder.clearCallingIdentity(); + try { + Controller2Link.this.onPlaybackActiveChanged(seq, playbackActive); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void sendSessionCommand(int seq, Session2Command command, Bundle args, + ResultReceiver resultReceiver) { + final long token = Binder.clearCallingIdentity(); + try { + Controller2Link.this.onSessionCommand(seq, command, args, resultReceiver); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void cancelSessionCommand(int seq) { + final long token = Binder.clearCallingIdentity(); + try { + Controller2Link.this.onCancelCommand(seq); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } +} diff --git a/apex/media/framework/java/android/media/DataSourceCallback.java b/apex/media/framework/java/android/media/DataSourceCallback.java new file mode 100644 index 000000000000..c297ecda249c --- /dev/null +++ b/apex/media/framework/java/android/media/DataSourceCallback.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.media; + +import android.annotation.NonNull; + +import java.io.Closeable; +import java.io.IOException; + +/** + * For supplying media data to the framework. Implement this if your app has + * special requirements for the way media data is obtained. + * + * <p class="note">Methods of this interface may be called on multiple different + * threads. There will be a thread synchronization point between each call to ensure that + * modifications to the state of your DataSourceCallback are visible to future calls. This means + * you don't need to do your own synchronization unless you're modifying the + * DataSourceCallback from another thread while it's being used by the framework.</p> + * + * @hide + */ +public abstract class DataSourceCallback implements Closeable { + + public static final int END_OF_STREAM = -1; + + /** + * Called to request data from the given position. + * + * Implementations should should write up to {@code size} bytes into + * {@code buffer}, and return the number of bytes written. + * + * Return {@code 0} if size is zero (thus no bytes are read). + * + * Return {@code -1} to indicate that end of stream is reached. + * + * @param position the position in the data source to read from. + * @param buffer the buffer to read the data into. + * @param offset the offset within buffer to read the data into. + * @param size the number of bytes to read. + * @throws IOException on fatal errors. + * @return the number of bytes read, or {@link #END_OF_STREAM} if end of stream is reached. + */ + public abstract int readAt(long position, @NonNull byte[] buffer, int offset, int size) + throws IOException; + + /** + * Called to get the size of the data source. + * + * @throws IOException on fatal errors + * @return the size of data source in bytes, or -1 if the size is unknown. + */ + public abstract long getSize() throws IOException; +} diff --git a/apex/media/framework/java/android/media/MediaConstants.java b/apex/media/framework/java/android/media/MediaConstants.java new file mode 100644 index 000000000000..ce108894b9a5 --- /dev/null +++ b/apex/media/framework/java/android/media/MediaConstants.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +class MediaConstants { + // Bundle key for int + static final String KEY_PID = "android.media.key.PID"; + + // Bundle key for String + static final String KEY_PACKAGE_NAME = "android.media.key.PACKAGE_NAME"; + + // Bundle key for Parcelable + static final String KEY_SESSION2LINK = "android.media.key.SESSION2LINK"; + static final String KEY_ALLOWED_COMMANDS = "android.media.key.ALLOWED_COMMANDS"; + static final String KEY_PLAYBACK_ACTIVE = "android.media.key.PLAYBACK_ACTIVE"; + static final String KEY_TOKEN_EXTRAS = "android.media.key.TOKEN_EXTRAS"; + static final String KEY_CONNECTION_HINTS = "android.media.key.CONNECTION_HINTS"; + + private MediaConstants() { + } +} diff --git a/apex/media/framework/java/android/media/MediaController2.java b/apex/media/framework/java/android/media/MediaController2.java new file mode 100644 index 000000000000..d059c670ccb6 --- /dev/null +++ b/apex/media/framework/java/android/media/MediaController2.java @@ -0,0 +1,638 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import static android.media.MediaConstants.KEY_ALLOWED_COMMANDS; +import static android.media.MediaConstants.KEY_CONNECTION_HINTS; +import static android.media.MediaConstants.KEY_PACKAGE_NAME; +import static android.media.MediaConstants.KEY_PID; +import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE; +import static android.media.MediaConstants.KEY_SESSION2LINK; +import static android.media.MediaConstants.KEY_TOKEN_EXTRAS; +import static android.media.Session2Command.Result.RESULT_ERROR_UNKNOWN_ERROR; +import static android.media.Session2Command.Result.RESULT_INFO_SKIPPED; +import static android.media.Session2Token.TYPE_SESSION; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; + +import java.util.concurrent.Executor; + +/** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * + * Allows an app to interact with an active {@link MediaSession2} or a + * {@link MediaSession2Service} which would provide {@link MediaSession2}. Media buttons and other + * commands can be sent to the session. + */ +public class MediaController2 implements AutoCloseable { + static final String TAG = "MediaController2"; + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final ControllerCallback mCallback; + + private final IBinder.DeathRecipient mDeathRecipient = () -> close(); + private final Context mContext; + private final Session2Token mSessionToken; + private final Executor mCallbackExecutor; + private final Controller2Link mControllerStub; + private final Handler mResultHandler; + private final SessionServiceConnection mServiceConnection; + + private final Object mLock = new Object(); + //@GuardedBy("mLock") + private boolean mClosed; + //@GuardedBy("mLock") + private int mNextSeqNumber; + //@GuardedBy("mLock") + private Session2Link mSessionBinder; + //@GuardedBy("mLock") + private Session2CommandGroup mAllowedCommands; + //@GuardedBy("mLock") + private Session2Token mConnectedToken; + //@GuardedBy("mLock") + private ArrayMap<ResultReceiver, Integer> mPendingCommands; + //@GuardedBy("mLock") + private ArraySet<Integer> mRequestedCommandSeqNumbers; + //@GuardedBy("mLock") + private boolean mPlaybackActive; + + /** + * Create a {@link MediaController2} from the {@link Session2Token}. + * This connects to the session and may wake up the service if it's not available. + * + * @param context context + * @param token token to connect to + * @param connectionHints a session-specific argument to send to the session when connecting. + * The contents of this bundle may affect the connection result. + * @param executor executor to run callbacks on. + * @param callback controller callback to receive changes in. + */ + MediaController2(@NonNull Context context, @NonNull Session2Token token, + @NonNull Bundle connectionHints, @NonNull Executor executor, + @NonNull ControllerCallback callback) { + if (context == null) { + throw new IllegalArgumentException("context shouldn't be null"); + } + if (token == null) { + throw new IllegalArgumentException("token shouldn't be null"); + } + mContext = context; + mSessionToken = token; + mCallbackExecutor = (executor == null) ? context.getMainExecutor() : executor; + mCallback = (callback == null) ? new ControllerCallback() {} : callback; + mControllerStub = new Controller2Link(this); + // NOTE: mResultHandler uses main looper, so this MUST NOT be blocked. + mResultHandler = new Handler(context.getMainLooper()); + + mNextSeqNumber = 0; + mPendingCommands = new ArrayMap<>(); + mRequestedCommandSeqNumbers = new ArraySet<>(); + + boolean connectRequested; + if (token.getType() == TYPE_SESSION) { + mServiceConnection = null; + connectRequested = requestConnectToSession(connectionHints); + } else { + mServiceConnection = new SessionServiceConnection(connectionHints); + connectRequested = requestConnectToService(); + } + if (!connectRequested) { + close(); + } + } + + @Override + public void close() { + synchronized (mLock) { + if (mClosed) { + // Already closed. Ignore rest of clean up code. + // Note: unbindService() throws IllegalArgumentException when it's called twice. + return; + } + if (DEBUG) { + Log.d(TAG, "closing " + this); + } + mClosed = true; + if (mServiceConnection != null) { + // Note: This should be called even when the bindService() has returned false. + mContext.unbindService(mServiceConnection); + } + if (mSessionBinder != null) { + try { + mSessionBinder.disconnect(mControllerStub, getNextSeqNumber()); + mSessionBinder.unlinkToDeath(mDeathRecipient, 0); + } catch (RuntimeException e) { + // No-op + } + } + mConnectedToken = null; + mPendingCommands.clear(); + mRequestedCommandSeqNumbers.clear(); + mCallbackExecutor.execute(() -> { + mCallback.onDisconnected(MediaController2.this); + }); + mSessionBinder = null; + } + } + + /** + * Returns {@link Session2Token} of the connected session. + * If it is not connected yet, it returns {@code null}. + * <p> + * This may differ with the {@link Session2Token} from the constructor. For example, if the + * controller is created with the token for {@link MediaSession2Service}, this would return + * token for the {@link MediaSession2} in the service. + * + * @return Session2Token of the connected session, or {@code null} if not connected + */ + @Nullable + public Session2Token getConnectedToken() { + synchronized (mLock) { + return mConnectedToken; + } + } + + /** + * Returns whether the session's playback is active. + * + * @return {@code true} if playback active. {@code false} otherwise. + * @see ControllerCallback#onPlaybackActiveChanged(MediaController2, boolean) + */ + public boolean isPlaybackActive() { + synchronized (mLock) { + return mPlaybackActive; + } + } + + /** + * Sends a session command to the session + * <p> + * @param command the session command + * @param args optional arguments + * @return a token which will be sent together in {@link ControllerCallback#onCommandResult} + * when its result is received. + */ + @NonNull + public Object sendSessionCommand(@NonNull Session2Command command, @Nullable Bundle args) { + if (command == null) { + throw new IllegalArgumentException("command shouldn't be null"); + } + + ResultReceiver resultReceiver = new ResultReceiver(mResultHandler) { + protected void onReceiveResult(int resultCode, Bundle resultData) { + synchronized (mLock) { + mPendingCommands.remove(this); + } + mCallbackExecutor.execute(() -> { + mCallback.onCommandResult(MediaController2.this, this, + command, new Session2Command.Result(resultCode, resultData)); + }); + } + }; + + synchronized (mLock) { + if (mSessionBinder != null) { + int seq = getNextSeqNumber(); + mPendingCommands.put(resultReceiver, seq); + try { + mSessionBinder.sendSessionCommand(mControllerStub, seq, command, args, + resultReceiver); + } catch (RuntimeException e) { + mPendingCommands.remove(resultReceiver); + resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null); + } + } + } + return resultReceiver; + } + + /** + * Cancels the session command previously sent. + * + * @param token the token which is returned from {@link #sendSessionCommand}. + */ + public void cancelSessionCommand(@NonNull Object token) { + if (token == null) { + throw new IllegalArgumentException("token shouldn't be null"); + } + synchronized (mLock) { + if (mSessionBinder == null) return; + Integer seq = mPendingCommands.remove(token); + if (seq != null) { + mSessionBinder.cancelSessionCommand(mControllerStub, seq); + } + } + } + + // Called by Controller2Link.onConnected + void onConnected(int seq, Bundle connectionResult) { + Session2Link sessionBinder = connectionResult.getParcelable(KEY_SESSION2LINK); + Session2CommandGroup allowedCommands = + connectionResult.getParcelable(KEY_ALLOWED_COMMANDS); + boolean playbackActive = connectionResult.getBoolean(KEY_PLAYBACK_ACTIVE); + + Bundle tokenExtras = connectionResult.getBundle(KEY_TOKEN_EXTRAS); + if (tokenExtras == null) { + Log.w(TAG, "extras shouldn't be null."); + tokenExtras = Bundle.EMPTY; + } else if (MediaSession2.hasCustomParcelable(tokenExtras)) { + Log.w(TAG, "extras contain custom parcelable. Ignoring."); + tokenExtras = Bundle.EMPTY; + } + + if (DEBUG) { + Log.d(TAG, "notifyConnected sessionBinder=" + sessionBinder + + ", allowedCommands=" + allowedCommands); + } + if (sessionBinder == null || allowedCommands == null) { + // Connection rejected. + close(); + return; + } + synchronized (mLock) { + mSessionBinder = sessionBinder; + mAllowedCommands = allowedCommands; + mPlaybackActive = playbackActive; + + // Implementation for the local binder is no-op, + // so can be used without worrying about deadlock. + sessionBinder.linkToDeath(mDeathRecipient, 0); + mConnectedToken = new Session2Token(mSessionToken.getUid(), TYPE_SESSION, + mSessionToken.getPackageName(), sessionBinder, tokenExtras); + } + mCallbackExecutor.execute(() -> { + mCallback.onConnected(MediaController2.this, allowedCommands); + }); + } + + // Called by Controller2Link.onDisconnected + void onDisconnected(int seq) { + // close() will call mCallback.onDisconnected + close(); + } + + // Called by Controller2Link.onPlaybackActiveChanged + void onPlaybackActiveChanged(int seq, boolean playbackActive) { + synchronized (mLock) { + mPlaybackActive = playbackActive; + } + mCallbackExecutor.execute(() -> { + mCallback.onPlaybackActiveChanged(MediaController2.this, playbackActive); + }); + } + + // Called by Controller2Link.onSessionCommand + void onSessionCommand(int seq, Session2Command command, Bundle args, + @Nullable ResultReceiver resultReceiver) { + synchronized (mLock) { + mRequestedCommandSeqNumbers.add(seq); + } + mCallbackExecutor.execute(() -> { + boolean isCanceled; + synchronized (mLock) { + isCanceled = !mRequestedCommandSeqNumbers.remove(seq); + } + if (isCanceled) { + if (resultReceiver != null) { + resultReceiver.send(RESULT_INFO_SKIPPED, null); + } + return; + } + Session2Command.Result result = mCallback.onSessionCommand( + MediaController2.this, command, args); + if (resultReceiver != null) { + if (result == null) { + resultReceiver.send(RESULT_INFO_SKIPPED, null); + } else { + resultReceiver.send(result.getResultCode(), result.getResultData()); + } + } + }); + } + + // Called by Controller2Link.onSessionCommand + void onCancelCommand(int seq) { + synchronized (mLock) { + mRequestedCommandSeqNumbers.remove(seq); + } + } + + private int getNextSeqNumber() { + synchronized (mLock) { + return mNextSeqNumber++; + } + } + + private Bundle createConnectionRequest(@NonNull Bundle connectionHints) { + Bundle connectionRequest = new Bundle(); + connectionRequest.putString(KEY_PACKAGE_NAME, mContext.getPackageName()); + connectionRequest.putInt(KEY_PID, Process.myPid()); + connectionRequest.putBundle(KEY_CONNECTION_HINTS, connectionHints); + return connectionRequest; + } + + private boolean requestConnectToSession(@NonNull Bundle connectionHints) { + Session2Link sessionBinder = mSessionToken.getSessionLink(); + Bundle connectionRequest = createConnectionRequest(connectionHints); + try { + sessionBinder.connect(mControllerStub, getNextSeqNumber(), connectionRequest); + } catch (RuntimeException e) { + Log.w(TAG, "Failed to call connection request", e); + return false; + } + return true; + } + + private boolean requestConnectToService() { + // Service. Needs to get fresh binder whenever connection is needed. + final Intent intent = new Intent(MediaSession2Service.SERVICE_INTERFACE); + intent.setClassName(mSessionToken.getPackageName(), mSessionToken.getServiceName()); + + // Use bindService() instead of startForegroundService() to start session service for three + // reasons. + // 1. Prevent session service owner's stopSelf() from destroying service. + // With the startForegroundService(), service's call of stopSelf() will trigger immediate + // onDestroy() calls on the main thread even when onConnect() is running in another + // thread. + // 2. Minimize APIs for developers to take care about. + // With bindService(), developers only need to take care about Service.onBind() + // but Service.onStartCommand() should be also taken care about with the + // startForegroundService(). + // 3. Future support for UI-less playback + // If a service wants to keep running, it should be either foreground service or + // bound service. But there had been request for the feature for system apps + // and using bindService() will be better fit with it. + synchronized (mLock) { + boolean result = mContext.bindService( + intent, mServiceConnection, Context.BIND_AUTO_CREATE); + if (!result) { + Log.w(TAG, "bind to " + mSessionToken + " failed"); + return false; + } else if (DEBUG) { + Log.d(TAG, "bind to " + mSessionToken + " succeeded"); + } + } + return true; + } + + /** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Builder for {@link MediaController2}. + * <p> + * Any incoming event from the {@link MediaSession2} will be handled on the callback + * executor. If it's not set, {@link Context#getMainExecutor()} will be used by default. + */ + public static final class Builder { + private Context mContext; + private Session2Token mToken; + private Bundle mConnectionHints; + private Executor mCallbackExecutor; + private ControllerCallback mCallback; + + /** + * Creates a builder for {@link MediaController2}. + * + * @param context context + * @param token token of the session to connect to + */ + public Builder(@NonNull Context context, @NonNull Session2Token token) { + if (context == null) { + throw new IllegalArgumentException("context shouldn't be null"); + } + if (token == null) { + throw new IllegalArgumentException("token shouldn't be null"); + } + mContext = context; + mToken = token; + } + + /** + * Set the connection hints for the controller. + * <p> + * {@code connectionHints} is a session-specific argument to send to the session when + * connecting. The contents of this bundle may affect the connection result. + * <p> + * An {@link IllegalArgumentException} will be thrown if the bundle contains any + * non-framework Parcelable objects. + * + * @param connectionHints a bundle which contains the connection hints + * @return The Builder to allow chaining + */ + @NonNull + public Builder setConnectionHints(@NonNull Bundle connectionHints) { + if (connectionHints == null) { + throw new IllegalArgumentException("connectionHints shouldn't be null"); + } + if (MediaSession2.hasCustomParcelable(connectionHints)) { + throw new IllegalArgumentException("connectionHints shouldn't contain any custom " + + "parcelables"); + } + mConnectionHints = new Bundle(connectionHints); + return this; + } + + /** + * Set callback for the controller and its executor. + * + * @param executor callback executor + * @param callback session callback. + * @return The Builder to allow chaining + */ + @NonNull + public Builder setControllerCallback(@NonNull Executor executor, + @NonNull ControllerCallback callback) { + if (executor == null) { + throw new IllegalArgumentException("executor shouldn't be null"); + } + if (callback == null) { + throw new IllegalArgumentException("callback shouldn't be null"); + } + mCallbackExecutor = executor; + mCallback = callback; + return this; + } + + /** + * Build {@link MediaController2}. + * + * @return a new controller + */ + @NonNull + public MediaController2 build() { + if (mCallbackExecutor == null) { + mCallbackExecutor = mContext.getMainExecutor(); + } + if (mCallback == null) { + mCallback = new ControllerCallback() {}; + } + if (mConnectionHints == null) { + mConnectionHints = Bundle.EMPTY; + } + return new MediaController2( + mContext, mToken, mConnectionHints, mCallbackExecutor, mCallback); + } + } + + /** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Interface for listening to change in activeness of the {@link MediaSession2}. + */ + public abstract static class ControllerCallback { + /** + * Called when the controller is successfully connected to the session. The controller + * becomes available afterwards. + * + * @param controller the controller for this event + * @param allowedCommands commands that's allowed by the session. + */ + public void onConnected(@NonNull MediaController2 controller, + @NonNull Session2CommandGroup allowedCommands) {} + + /** + * Called when the session refuses the controller or the controller is disconnected from + * the session. The controller becomes unavailable afterwards and the callback wouldn't + * be called. + * <p> + * It will be also called after the {@link #close()}, so you can put clean up code here. + * You don't need to call {@link #close()} after this. + * + * @param controller the controller for this event + */ + public void onDisconnected(@NonNull MediaController2 controller) {} + + /** + * Called when the session's playback activeness is changed. + * + * @param controller the controller for this event + * @param playbackActive {@code true} if the session's playback is active. + * {@code false} otherwise. + * @see MediaController2#isPlaybackActive() + */ + public void onPlaybackActiveChanged(@NonNull MediaController2 controller, + boolean playbackActive) {} + + /** + * Called when the connected session sent a session command. + * + * @param controller the controller for this event + * @param command the session command + * @param args optional arguments + * @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED + * will be sent to the session. + */ + @Nullable + public Session2Command.Result onSessionCommand(@NonNull MediaController2 controller, + @NonNull Session2Command command, @Nullable Bundle args) { + return null; + } + + /** + * Called when the command sent to the connected session is finished. + * + * @param controller the controller for this event + * @param token the token got from {@link MediaController2#sendSessionCommand} + * @param command the session command + * @param result the result of the session command + */ + public void onCommandResult(@NonNull MediaController2 controller, @NonNull Object token, + @NonNull Session2Command command, @NonNull Session2Command.Result result) {} + } + + // This will be called on the main thread. + private class SessionServiceConnection implements ServiceConnection { + private final Bundle mConnectionHints; + + SessionServiceConnection(@Nullable Bundle connectionHints) { + mConnectionHints = connectionHints; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + // Note that it's always main-thread. + boolean connectRequested = false; + try { + if (DEBUG) { + Log.d(TAG, "onServiceConnected " + name + " " + this); + } + // Sanity check + if (!mSessionToken.getPackageName().equals(name.getPackageName())) { + Log.wtf(TAG, "Expected connection to " + mSessionToken.getPackageName() + + " but is connected to " + name); + return; + } + IMediaSession2Service iService = IMediaSession2Service.Stub.asInterface(service); + if (iService == null) { + Log.wtf(TAG, "Service interface is missing."); + return; + } + Bundle connectionRequest = createConnectionRequest(mConnectionHints); + iService.connect(mControllerStub, getNextSeqNumber(), connectionRequest); + connectRequested = true; + } catch (RemoteException e) { + Log.w(TAG, "Service " + name + " has died prematurely", e); + } finally { + if (!connectRequested) { + close(); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + // Temporal lose of the binding because of the service crash. System will automatically + // rebind, so just no-op. + if (DEBUG) { + Log.w(TAG, "Session service " + name + " is disconnected."); + } + close(); + } + + @Override + public void onBindingDied(ComponentName name) { + // Permanent lose of the binding because of the service package update or removed. + // This SessionServiceRecord will be removed accordingly, but forget session binder here + // for sure. + close(); + } + } +} diff --git a/apex/media/framework/java/android/media/MediaParser.java b/apex/media/framework/java/android/media/MediaParser.java new file mode 100644 index 000000000000..e4b5d19e67c9 --- /dev/null +++ b/apex/media/framework/java/android/media/MediaParser.java @@ -0,0 +1,2130 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.media; + +import android.annotation.CheckResult; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringDef; +import android.media.MediaCodec.CryptoInfo; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.extractor.ChunkIndex; +import com.google.android.exoplayer2.extractor.DefaultExtractorInput; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.amr.AmrExtractor; +import com.google.android.exoplayer2.extractor.flac.FlacExtractor; +import com.google.android.exoplayer2.extractor.flv.FlvExtractor; +import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; +import com.google.android.exoplayer2.extractor.ogg.OggExtractor; +import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import com.google.android.exoplayer2.extractor.ts.Ac4Extractor; +import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; +import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; +import com.google.android.exoplayer2.extractor.ts.PsExtractor; +import com.google.android.exoplayer2.extractor.ts.TsExtractor; +import com.google.android.exoplayer2.extractor.wav.WavExtractor; +import com.google.android.exoplayer2.upstream.DataReader; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.ColorInfo; + +import java.io.EOFException; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Parses media container formats and extracts contained media samples and metadata. + * + * <p>This class provides access to a battery of low-level media container parsers. Each instance of + * this class is associated to a specific media parser implementation which is suitable for + * extraction from a specific media container format. The media parser implementation assignment + * depends on the factory method (see {@link #create} and {@link #createByName}) used to create the + * instance. + * + * <p>Users must implement the following to use this class. + * + * <ul> + * <li>{@link InputReader}: Provides the media container's bytes to parse. + * <li>{@link OutputConsumer}: Provides a sink for all extracted data and metadata. + * </ul> + * + * <p>The following code snippet includes a usage example: + * + * <pre> + * MyOutputConsumer myOutputConsumer = new MyOutputConsumer(); + * MyInputReader myInputReader = new MyInputReader("www.example.com"); + * MediaParser mediaParser = MediaParser.create(myOutputConsumer); + * + * while (mediaParser.advance(myInputReader)) {} + * + * mediaParser.release(); + * mediaParser = null; + * </pre> + * + * <p>The following code snippet provides a rudimentary {@link OutputConsumer} sample implementation + * which extracts and publishes all video samples: + * + * <pre> + * class VideoOutputConsumer implements MediaParser.OutputConsumer { + * + * private byte[] sampleDataBuffer = new byte[4096]; + * private byte[] discardedDataBuffer = new byte[4096]; + * private int videoTrackIndex = -1; + * private int bytesWrittenCount = 0; + * + * @Override + * public void onSeekMapFound(int i, @NonNull MediaFormat mediaFormat) { + * // Do nothing. + * } + * + * @Override + * public void onTrackDataFound(int i, @NonNull TrackData trackData) { + * MediaFormat mediaFormat = trackData.mediaFormat; + * if (videoTrackIndex == -1 && + * mediaFormat + * .getString(MediaFormat.KEY_MIME, /* defaultValue= */ "") + * .startsWith("video/")) { + * videoTrackIndex = i; + * } + * } + * + * @Override + * public void onSampleDataFound(int trackIndex, @NonNull InputReader inputReader) + * throws IOException { + * int numberOfBytesToRead = (int) inputReader.getLength(); + * if (videoTrackIndex != trackIndex) { + * // Discard contents. + * inputReader.read( + * discardedDataBuffer, + * /* offset= */ 0, + * Math.min(discardDataBuffer.length, numberOfBytesToRead)); + * } else { + * ensureSpaceInBuffer(numberOfBytesToRead); + * int bytesRead = inputReader.read( + * sampleDataBuffer, bytesWrittenCount, numberOfBytesToRead); + * bytesWrittenCount += bytesRead; + * } + * } + * + * @Override + * public void onSampleCompleted( + * int trackIndex, + * long timeMicros, + * int flags, + * int size, + * int offset, + * @Nullable CryptoInfo cryptoData) { + * if (videoTrackIndex != trackIndex) { + * return; // It's not the video track. Ignore. + * } + * byte[] sampleData = new byte[size]; + * int sampleStartOffset = bytesWrittenCount - size - offset; + * System.arraycopy( + * sampleDataBuffer, + * sampleStartOffset, + * sampleData, + * /* destPos= */ 0, + * size); + * // Place trailing bytes at the start of the buffer. + * System.arraycopy( + * sampleDataBuffer, + * bytesWrittenCount - offset, + * sampleDataBuffer, + * /* destPos= */ 0, + * /* size= */ offset); + * bytesWrittenCount = bytesWrittenCount - offset; + * publishSample(sampleData, timeMicros, flags); + * } + * + * private void ensureSpaceInBuffer(int numberOfBytesToRead) { + * int requiredLength = bytesWrittenCount + numberOfBytesToRead; + * if (requiredLength > sampleDataBuffer.length) { + * sampleDataBuffer = Arrays.copyOf(sampleDataBuffer, requiredLength); + * } + * } + * + * } + * + * </pre> + */ +public final class MediaParser { + + /** + * Maps seek positions to {@link SeekPoint SeekPoints} in the stream. + * + * <p>A {@link SeekPoint} is a position in the stream from which a player may successfully start + * playing media samples. + */ + public static final class SeekMap { + + /** Returned by {@link #getDurationMicros()} when the duration is unknown. */ + public static final int UNKNOWN_DURATION = Integer.MIN_VALUE; + + /** + * For each {@link #getSeekPoints} call, returns a single {@link SeekPoint} whose {@link + * SeekPoint#timeMicros} matches the requested timestamp, and whose {@link + * SeekPoint#position} is 0. + * + * @hide + */ + public static final SeekMap DUMMY = new SeekMap(new DummyExoPlayerSeekMap()); + + private final com.google.android.exoplayer2.extractor.SeekMap mExoPlayerSeekMap; + + private SeekMap(com.google.android.exoplayer2.extractor.SeekMap exoplayerSeekMap) { + mExoPlayerSeekMap = exoplayerSeekMap; + } + + /** Returns whether seeking is supported. */ + public boolean isSeekable() { + return mExoPlayerSeekMap.isSeekable(); + } + + /** + * Returns the duration of the stream in microseconds or {@link #UNKNOWN_DURATION} if the + * duration is unknown. + */ + public long getDurationMicros() { + long durationUs = mExoPlayerSeekMap.getDurationUs(); + return durationUs != C.TIME_UNSET ? durationUs : UNKNOWN_DURATION; + } + + /** + * Obtains {@link SeekPoint SeekPoints} for the specified seek time in microseconds. + * + * <p>{@code getSeekPoints(timeMicros).first} contains the latest seek point for samples + * with timestamp equal to or smaller than {@code timeMicros}. + * + * <p>{@code getSeekPoints(timeMicros).second} contains the earliest seek point for samples + * with timestamp equal to or greater than {@code timeMicros}. If a seek point exists for + * {@code timeMicros}, the returned pair will contain the same {@link SeekPoint} twice. + * + * @param timeMicros A seek time in microseconds. + * @return The corresponding {@link SeekPoint SeekPoints}. + */ + @NonNull + public Pair<SeekPoint, SeekPoint> getSeekPoints(long timeMicros) { + SeekPoints seekPoints = mExoPlayerSeekMap.getSeekPoints(timeMicros); + return new Pair<>(toSeekPoint(seekPoints.first), toSeekPoint(seekPoints.second)); + } + } + + /** Holds information associated with a track. */ + public static final class TrackData { + + /** Holds {@link MediaFormat} information for the track. */ + @NonNull public final MediaFormat mediaFormat; + + /** + * Holds {@link DrmInitData} necessary to acquire keys associated with the track, or null if + * the track has no encryption data. + */ + @Nullable public final DrmInitData drmInitData; + + private TrackData(MediaFormat mediaFormat, DrmInitData drmInitData) { + this.mediaFormat = mediaFormat; + this.drmInitData = drmInitData; + } + } + + /** Defines a seek point in a media stream. */ + public static final class SeekPoint { + + /** A {@link SeekPoint} whose time and byte offset are both set to 0. */ + @NonNull public static final SeekPoint START = new SeekPoint(0, 0); + + /** The time of the seek point, in microseconds. */ + public final long timeMicros; + + /** The byte offset of the seek point. */ + public final long position; + + /** + * @param timeMicros The time of the seek point, in microseconds. + * @param position The byte offset of the seek point. + */ + private SeekPoint(long timeMicros, long position) { + this.timeMicros = timeMicros; + this.position = position; + } + + @Override + @NonNull + public String toString() { + return "[timeMicros=" + timeMicros + ", position=" + position + "]"; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekPoint other = (SeekPoint) obj; + return timeMicros == other.timeMicros && position == other.position; + } + + @Override + public int hashCode() { + int result = (int) timeMicros; + result = 31 * result + (int) position; + return result; + } + } + + /** Provides input data to {@link MediaParser}. */ + public interface InputReader { + + /** + * Reads up to {@code readLength} bytes of data and stores them into {@code buffer}, + * starting at index {@code offset}. + * + * <p>This method blocks until at least one byte is read, the end of input is detected, or + * an exception is thrown. The read position advances to the first unread byte. + * + * @param buffer The buffer into which the read data should be stored. + * @param offset The start offset into {@code buffer} at which data should be written. + * @param readLength The maximum number of bytes to read. + * @return The non-zero number of bytes read, or -1 if no data is available because the end + * of the input has been reached. + * @throws java.io.IOException If an error occurs reading from the source. + */ + int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException; + + /** Returns the current read position (byte offset) in the stream. */ + long getPosition(); + + /** Returns the length of the input in bytes, or -1 if the length is unknown. */ + long getLength(); + } + + /** {@link InputReader} that allows setting the read position. */ + public interface SeekableInputReader extends InputReader { + + /** + * Sets the read position at the given {@code position}. + * + * <p>{@link #advance} will immediately return after calling this method. + * + * @param position The position to seek to, in bytes. + */ + void seekToPosition(long position); + } + + /** Receives extracted media sample data and metadata from {@link MediaParser}. */ + public interface OutputConsumer { + + /** + * Called when a {@link SeekMap} has been extracted from the stream. + * + * <p>This method is called at least once before any samples are {@link #onSampleCompleted + * complete}. May be called multiple times after that in order to add {@link SeekPoint + * SeekPoints}. + * + * @param seekMap The extracted {@link SeekMap}. + */ + void onSeekMapFound(@NonNull SeekMap seekMap); + + /** + * Called when the number of tracks is found. + * + * @param numberOfTracks The number of tracks in the stream. + */ + void onTrackCountFound(int numberOfTracks); + + /** + * Called when new {@link TrackData} is found in the stream. + * + * @param trackIndex The index of the track for which the {@link TrackData} was extracted. + * @param trackData The extracted {@link TrackData}. + */ + void onTrackDataFound(int trackIndex, @NonNull TrackData trackData); + + /** + * Called when sample data is found in the stream. + * + * <p>If the invocation of this method returns before the entire {@code inputReader} {@link + * InputReader#getLength() length} is consumed, the method will be called again for the + * implementer to read the remaining data. Implementers should surface any thrown {@link + * IOException} caused by reading from {@code input}. + * + * @param trackIndex The index of the track to which the sample data corresponds. + * @param inputReader The {@link InputReader} from which to read the data. + * @throws IOException If an exception occurs while reading from {@code inputReader}. + */ + void onSampleDataFound(int trackIndex, @NonNull InputReader inputReader) throws IOException; + + /** + * Called once all the data of a sample has been passed to {@link #onSampleDataFound}. + * + * <p>Includes sample metadata, like presentation timestamp and flags. + * + * @param trackIndex The index of the track to which the sample corresponds. + * @param timeMicros The media timestamp associated with the sample, in microseconds. + * @param flags Flags associated with the sample. See the {@code SAMPLE_FLAG_*} constants. + * @param size The size of the sample data, in bytes. + * @param offset The number of bytes that have been consumed by {@code + * onSampleDataFound(int, MediaParser.InputReader)} for the specified track, since the + * last byte belonging to the sample whose metadata is being passed. + * @param cryptoInfo Encryption data required to decrypt the sample. May be null for + * unencrypted samples. Implementors should treat any output {@link CryptoInfo} + * instances as immutable. MediaParser will not modify any output {@code cryptoInfos} + * and implementors should not modify them either. + */ + void onSampleCompleted( + int trackIndex, + long timeMicros, + @SampleFlags int flags, + int size, + int offset, + @Nullable CryptoInfo cryptoInfo); + } + + /** + * Thrown if all parser implementations provided to {@link #create} failed to sniff the input + * content. + */ + public static final class UnrecognizedInputFormatException extends IOException { + + /** + * Creates a new instance which signals that the parsers with the given names failed to + * parse the input. + */ + @NonNull + @CheckResult + private static UnrecognizedInputFormatException createForExtractors( + @NonNull String... extractorNames) { + StringBuilder builder = new StringBuilder(); + builder.append("None of the available parsers ( "); + builder.append(extractorNames[0]); + for (int i = 1; i < extractorNames.length; i++) { + builder.append(", "); + builder.append(extractorNames[i]); + } + builder.append(") could read the stream."); + return new UnrecognizedInputFormatException(builder.toString()); + } + + private UnrecognizedInputFormatException(String extractorNames) { + super(extractorNames); + } + } + + /** Thrown when an error occurs while parsing a media stream. */ + public static final class ParsingException extends IOException { + + private ParsingException(ParserException cause) { + super(cause); + } + } + + // Sample flags. + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + SAMPLE_FLAG_KEY_FRAME, + SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA, + SAMPLE_FLAG_LAST_SAMPLE, + SAMPLE_FLAG_ENCRYPTED, + SAMPLE_FLAG_DECODE_ONLY + }) + public @interface SampleFlags {} + /** Indicates that the sample holds a synchronization sample. */ + public static final int SAMPLE_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME; + /** + * Indicates that the sample has supplemental data. + * + * <p>Samples will not have this flag set unless the {@code + * "android.media.mediaparser.includeSupplementalData"} parameter is set to {@code true} via + * {@link #setParameter}. + * + * <p>Samples with supplemental data have the following sample data format: + * + * <ul> + * <li>If the {@code "android.media.mediaparser.inBandCryptoInfo"} parameter is set, all + * encryption information. + * <li>(4 bytes) {@code sample_data_size}: The size of the actual sample data, not including + * supplemental data or encryption information. + * <li>({@code sample_data_size} bytes): The media sample data. + * <li>(remaining bytes) The supplemental data. + * </ul> + */ + public static final int SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28; + /** Indicates that the sample is known to contain the last media sample of the stream. */ + public static final int SAMPLE_FLAG_LAST_SAMPLE = 1 << 29; + /** Indicates that the sample is (at least partially) encrypted. */ + public static final int SAMPLE_FLAG_ENCRYPTED = 1 << 30; + /** Indicates that the sample should be decoded but not rendered. */ + public static final int SAMPLE_FLAG_DECODE_ONLY = 1 << 31; + + // Parser implementation names. + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @StringDef( + prefix = {"PARSER_NAME_"}, + value = { + PARSER_NAME_UNKNOWN, + PARSER_NAME_MATROSKA, + PARSER_NAME_FMP4, + PARSER_NAME_MP4, + PARSER_NAME_MP3, + PARSER_NAME_ADTS, + PARSER_NAME_AC3, + PARSER_NAME_TS, + PARSER_NAME_FLV, + PARSER_NAME_OGG, + PARSER_NAME_PS, + PARSER_NAME_WAV, + PARSER_NAME_AMR, + PARSER_NAME_AC4, + PARSER_NAME_FLAC + }) + public @interface ParserName {} + + /** Parser name returned by {@link #getParserName()} when no parser has been selected yet. */ + public static final String PARSER_NAME_UNKNOWN = "android.media.mediaparser.UNKNOWN"; + /** + * Parser for the Matroska container format, as defined in the <a + * href="https://matroska.org/technical/specs/">spec</a>. + */ + public static final String PARSER_NAME_MATROSKA = "android.media.mediaparser.MatroskaParser"; + /** + * Parser for fragmented files using the MP4 container format, as defined in ISO/IEC 14496-12. + */ + public static final String PARSER_NAME_FMP4 = "android.media.mediaparser.FragmentedMp4Parser"; + /** + * Parser for non-fragmented files using the MP4 container format, as defined in ISO/IEC + * 14496-12. + */ + public static final String PARSER_NAME_MP4 = "android.media.mediaparser.Mp4Parser"; + /** Parser for the MP3 container format, as defined in ISO/IEC 11172-3. */ + public static final String PARSER_NAME_MP3 = "android.media.mediaparser.Mp3Parser"; + /** Parser for the ADTS container format, as defined in ISO/IEC 13818-7. */ + public static final String PARSER_NAME_ADTS = "android.media.mediaparser.AdtsParser"; + /** + * Parser for the AC-3 container format, as defined in Digital Audio Compression Standard + * (AC-3). + */ + public static final String PARSER_NAME_AC3 = "android.media.mediaparser.Ac3Parser"; + /** Parser for the TS container format, as defined in ISO/IEC 13818-1. */ + public static final String PARSER_NAME_TS = "android.media.mediaparser.TsParser"; + /** + * Parser for the FLV container format, as defined in Adobe Flash Video File Format + * Specification. + */ + public static final String PARSER_NAME_FLV = "android.media.mediaparser.FlvParser"; + /** Parser for the OGG container format, as defined in RFC 3533. */ + public static final String PARSER_NAME_OGG = "android.media.mediaparser.OggParser"; + /** Parser for the PS container format, as defined in ISO/IEC 11172-1. */ + public static final String PARSER_NAME_PS = "android.media.mediaparser.PsParser"; + /** + * Parser for the WAV container format, as defined in Multimedia Programming Interface and Data + * Specifications. + */ + public static final String PARSER_NAME_WAV = "android.media.mediaparser.WavParser"; + /** Parser for the AMR container format, as defined in RFC 4867. */ + public static final String PARSER_NAME_AMR = "android.media.mediaparser.AmrParser"; + /** + * Parser for the AC-4 container format, as defined by Dolby AC-4: Audio delivery for + * Next-Generation Entertainment Services. + */ + public static final String PARSER_NAME_AC4 = "android.media.mediaparser.Ac4Parser"; + /** + * Parser for the FLAC container format, as defined in the <a + * href="https://xiph.org/flac/">spec</a>. + */ + public static final String PARSER_NAME_FLAC = "android.media.mediaparser.FlacParser"; + + // MediaParser parameters. + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @StringDef( + prefix = {"PARAMETER_"}, + value = { + PARAMETER_ADTS_ENABLE_CBR_SEEKING, + PARAMETER_AMR_ENABLE_CBR_SEEKING, + PARAMETER_FLAC_DISABLE_ID3, + PARAMETER_MP4_IGNORE_EDIT_LISTS, + PARAMETER_MP4_IGNORE_TFDT_BOX, + PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES, + PARAMETER_MATROSKA_DISABLE_CUES_SEEKING, + PARAMETER_MP3_DISABLE_ID3, + PARAMETER_MP3_ENABLE_CBR_SEEKING, + PARAMETER_MP3_ENABLE_INDEX_SEEKING, + PARAMETER_TS_MODE, + PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES, + PARAMETER_TS_IGNORE_AAC_STREAM, + PARAMETER_TS_IGNORE_AVC_STREAM, + PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM, + PARAMETER_TS_DETECT_ACCESS_UNITS, + PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS, + PARAMETER_IN_BAND_CRYPTO_INFO, + PARAMETER_INCLUDE_SUPPLEMENTAL_DATA + }) + public @interface ParameterName {} + + /** + * Sets whether constant bitrate seeking should be enabled for ADTS parsing. {@code boolean} + * expected. Default value is {@code false}. + */ + public static final String PARAMETER_ADTS_ENABLE_CBR_SEEKING = + "android.media.mediaparser.adts.enableCbrSeeking"; + /** + * Sets whether constant bitrate seeking should be enabled for AMR. {@code boolean} expected. + * Default value is {@code false}. + */ + public static final String PARAMETER_AMR_ENABLE_CBR_SEEKING = + "android.media.mediaparser.amr.enableCbrSeeking"; + /** + * Sets whether the ID3 track should be disabled for FLAC. {@code boolean} expected. Default + * value is {@code false}. + */ + public static final String PARAMETER_FLAC_DISABLE_ID3 = + "android.media.mediaparser.flac.disableId3"; + /** + * Sets whether MP4 parsing should ignore edit lists. {@code boolean} expected. Default value is + * {@code false}. + */ + public static final String PARAMETER_MP4_IGNORE_EDIT_LISTS = + "android.media.mediaparser.mp4.ignoreEditLists"; + /** + * Sets whether MP4 parsing should ignore the tfdt box. {@code boolean} expected. Default value + * is {@code false}. + */ + public static final String PARAMETER_MP4_IGNORE_TFDT_BOX = + "android.media.mediaparser.mp4.ignoreTfdtBox"; + /** + * Sets whether MP4 parsing should treat all video frames as key frames. {@code boolean} + * expected. Default value is {@code false}. + */ + public static final String PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES = + "android.media.mediaparser.mp4.treatVideoFramesAsKeyframes"; + /** + * Sets whether Matroska parsing should avoid seeking to the cues element. {@code boolean} + * expected. Default value is {@code false}. + * + * <p>If this flag is enabled and the cues element occurs after the first cluster, then the + * media is treated as unseekable. + */ + public static final String PARAMETER_MATROSKA_DISABLE_CUES_SEEKING = + "android.media.mediaparser.matroska.disableCuesSeeking"; + /** + * Sets whether the ID3 track should be disabled for MP3. {@code boolean} expected. Default + * value is {@code false}. + */ + public static final String PARAMETER_MP3_DISABLE_ID3 = + "android.media.mediaparser.mp3.disableId3"; + /** + * Sets whether constant bitrate seeking should be enabled for MP3. {@code boolean} expected. + * Default value is {@code false}. + */ + public static final String PARAMETER_MP3_ENABLE_CBR_SEEKING = + "android.media.mediaparser.mp3.enableCbrSeeking"; + /** + * Sets whether MP3 parsing should generate a time-to-byte mapping. {@code boolean} expected. + * Default value is {@code false}. + * + * <p>Enabling this flag may require to scan a significant portion of the file to compute a seek + * point. Therefore, it should only be used if: + * + * <ul> + * <li>the file is small, or + * <li>the bitrate is variable (or the type of bitrate is unknown) and the seeking metadata + * provided in the file is not precise enough (or is not present). + * </ul> + */ + public static final String PARAMETER_MP3_ENABLE_INDEX_SEEKING = + "android.media.mediaparser.mp3.enableIndexSeeking"; + /** + * Sets the operation mode for TS parsing. {@code String} expected. Valid values are {@code + * "single_pmt"}, {@code "multi_pmt"}, and {@code "hls"}. Default value is {@code "single_pmt"}. + * + * <p>The operation modes alter the way TS behaves so that it can handle certain kinds of + * commonly-occurring malformed media. + * + * <ul> + * <li>{@code "single_pmt"}: Only the first found PMT is parsed. Others are ignored, even if + * more PMTs are declared in the PAT. + * <li>{@code "multi_pmt"}: Behave as described in ISO/IEC 13818-1. + * <li>{@code "hls"}: Enable {@code "single_pmt"} mode, and ignore continuity counters. + * </ul> + */ + public static final String PARAMETER_TS_MODE = "android.media.mediaparser.ts.mode"; + /** + * Sets whether TS should treat samples consisting of non-IDR I slices as synchronization + * samples (key-frames). {@code boolean} expected. Default value is {@code false}. + */ + public static final String PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES = + "android.media.mediaparser.ts.allowNonIdrAvcKeyframes"; + /** + * Sets whether TS parsing should ignore AAC elementary streams. {@code boolean} expected. + * Default value is {@code false}. + */ + public static final String PARAMETER_TS_IGNORE_AAC_STREAM = + "android.media.mediaparser.ts.ignoreAacStream"; + /** + * Sets whether TS parsing should ignore AVC elementary streams. {@code boolean} expected. + * Default value is {@code false}. + */ + public static final String PARAMETER_TS_IGNORE_AVC_STREAM = + "android.media.mediaparser.ts.ignoreAvcStream"; + /** + * Sets whether TS parsing should ignore splice information streams. {@code boolean} expected. + * Default value is {@code false}. + */ + public static final String PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM = + "android.media.mediaparser.ts.ignoreSpliceInfoStream"; + /** + * Sets whether TS parsing should split AVC stream into access units based on slice headers. + * {@code boolean} expected. Default value is {@code false}. + * + * <p>This flag should be left disabled if the stream contains access units delimiters in order + * to avoid unnecessary computational costs. + */ + public static final String PARAMETER_TS_DETECT_ACCESS_UNITS = + "android.media.mediaparser.ts.ignoreDetectAccessUnits"; + /** + * Sets whether TS parsing should handle HDMV DTS audio streams. {@code boolean} expected. + * Default value is {@code false}. + * + * <p>Enabling this flag will disable the detection of SCTE subtitles. + */ + public static final String PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS = + "android.media.mediaparser.ts.enableHdmvDtsAudioStreams"; + /** + * Sets whether encryption data should be sent in-band with the sample data, as per {@link + * OutputConsumer#onSampleDataFound}. {@code boolean} expected. Default value is {@code false}. + * + * <p>If this parameter is set, encrypted samples' data will be prefixed with the encryption + * information bytes. The format for in-band encryption information is: + * + * <ul> + * <li>(1 byte) {@code encryption_signal_byte}: Most significant bit signals whether the + * encryption data contains subsample encryption data. The remaining bits contain {@code + * initialization_vector_size}. + * <li>({@code initialization_vector_size} bytes) Initialization vector. + * <li>If subsample encryption data is present, as per {@code encryption_signal_byte}, the + * encryption data also contains: + * <ul> + * <li>(2 bytes) {@code subsample_encryption_data_length}. + * <li>({@code subsample_encryption_data_length * 6} bytes) Subsample encryption data + * (repeated {@code subsample_encryption_data_length} times): + * <ul> + * <li>(2 bytes) Size of a clear section in sample. + * <li>(4 bytes) Size of an encryption section in sample. + * </ul> + * </ul> + * </ul> + * + * @hide + */ + public static final String PARAMETER_IN_BAND_CRYPTO_INFO = + "android.media.mediaparser.inBandCryptoInfo"; + /** + * Sets whether supplemental data should be included as part of the sample data. {@code boolean} + * expected. Default value is {@code false}. See {@link #SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA} for + * information about the sample data format. + * + * @hide + */ + public static final String PARAMETER_INCLUDE_SUPPLEMENTAL_DATA = + "android.media.mediaparser.includeSupplementalData"; + /** + * Sets whether sample timestamps may start from non-zero offsets. {@code boolean} expected. + * Default value is {@code false}. + * + * <p>When set to true, sample timestamps will not be offset to start from zero, and the media + * provided timestamps will be used instead. For example, transport stream sample timestamps + * will not be converted to a zero-based timebase. + * + * @hide + */ + public static final String PARAMETER_IGNORE_TIMESTAMP_OFFSET = + "android.media.mediaparser.ignoreTimestampOffset"; + /** + * Sets whether each track type should be eagerly exposed. {@code boolean} expected. Default + * value is {@code false}. + * + * <p>When set to true, each track type will be eagerly exposed through a call to {@link + * OutputConsumer#onTrackDataFound} containing a single-value {@link MediaFormat}. The key for + * the track type is {@code "track-type-string"}, and the possible values are {@code "video"}, + * {@code "audio"}, {@code "text"}, {@code "metadata"}, and {@code "unknown"}. + * + * @hide + */ + public static final String PARAMETER_EAGERLY_EXPOSE_TRACKTYPE = + "android.media.mediaparser.eagerlyExposeTrackType"; + /** + * Sets whether a dummy {@link SeekMap} should be exposed before starting extraction. {@code + * boolean} expected. Default value is {@code false}. + * + * <p>For each {@link SeekMap#getSeekPoints} call, the dummy {@link SeekMap} returns a single + * {@link SeekPoint} whose {@link SeekPoint#timeMicros} matches the requested timestamp, and + * whose {@link SeekPoint#position} is 0. + * + * @hide + */ + public static final String PARAMETER_EXPOSE_DUMMY_SEEKMAP = + "android.media.mediaparser.exposeDummySeekMap"; + + /** + * Sets whether chunk indices available in the extracted media should be exposed as {@link + * MediaFormat MediaFormats}. {@code boolean} expected. Default value is {@link false}. + * + * <p>When set to true, any information about media segmentation will be exposed as a {@link + * MediaFormat} (with track index 0) containing four {@link ByteBuffer} elements under the + * following keys: + * + * <ul> + * <li>"chunk-index-int-sizes": Contains {@code ints} representing the sizes in bytes of each + * of the media segments. + * <li>"chunk-index-long-offsets": Contains {@code longs} representing the byte offsets of + * each segment in the stream. + * <li>"chunk-index-long-us-durations": Contains {@code longs} representing the media duration + * of each segment, in microseconds. + * <li>"chunk-index-long-us-times": Contains {@code longs} representing the start time of each + * segment, in microseconds. + * </ul> + * + * @hide + */ + public static final String PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT = + "android.media.mediaParser.exposeChunkIndexAsMediaFormat"; + /** + * Sets a list of closed-caption {@link MediaFormat MediaFormats} that should be exposed as part + * of the extracted media. {@code List<MediaFormat>} expected. Default value is an empty list. + * + * <p>Expected keys in the {@link MediaFormat} are: + * + * <ul> + * <p>{@link MediaFormat#KEY_MIME}: Determine the type of captions (for example, + * application/cea-608). Mandatory. + * <p>{@link MediaFormat#KEY_CAPTION_SERVICE_NUMBER}: Determine the channel on which the + * captions are transmitted. Optional. + * </ul> + * + * @hide + */ + public static final String PARAMETER_EXPOSE_CAPTION_FORMATS = + "android.media.mediaParser.exposeCaptionFormats"; + /** + * Sets whether the value associated with {@link #PARAMETER_EXPOSE_CAPTION_FORMATS} should + * override any in-band caption service declarations. {@code boolean} expected. Default value is + * {@link false}. + * + * <p>When {@code false}, any present in-band caption services information will override the + * values associated with {@link #PARAMETER_EXPOSE_CAPTION_FORMATS}. + * + * @hide + */ + public static final String PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS = + "android.media.mediaParser.overrideInBandCaptionDeclarations"; + /** + * Sets whether a track for EMSG events should be exposed in case of parsing a container that + * supports them. {@code boolean} expected. Default value is {@link false}. + * + * @hide + */ + public static final String PARAMETER_EXPOSE_EMSG_TRACK = + "android.media.mediaParser.exposeEmsgTrack"; + + // Private constants. + + private static final String TAG = "MediaParser"; + private static final Map<String, ExtractorFactory> EXTRACTOR_FACTORIES_BY_NAME; + private static final Map<String, Class> EXPECTED_TYPE_BY_PARAMETER_NAME; + private static final String TS_MODE_SINGLE_PMT = "single_pmt"; + private static final String TS_MODE_MULTI_PMT = "multi_pmt"; + private static final String TS_MODE_HLS = "hls"; + private static final int BYTES_PER_SUBSAMPLE_ENCRYPTION_ENTRY = 6; + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + @IntDef( + value = { + STATE_READING_SIGNAL_BYTE, + STATE_READING_INIT_VECTOR, + STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE, + STATE_READING_SUBSAMPLE_ENCRYPTION_DATA + }) + private @interface EncryptionDataReadState {} + + private static final int STATE_READING_SIGNAL_BYTE = 0; + private static final int STATE_READING_INIT_VECTOR = 1; + private static final int STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE = 2; + private static final int STATE_READING_SUBSAMPLE_ENCRYPTION_DATA = 3; + + // Instance creation methods. + + /** + * Creates an instance backed by the parser with the given {@code name}. The returned instance + * will attempt parsing without sniffing the content. + * + * @param name The name of the parser that will be associated with the created instance. + * @param outputConsumer The {@link OutputConsumer} to which track data and samples are pushed. + * @return A new instance. + * @throws IllegalArgumentException If an invalid name is provided. + */ + @NonNull + public static MediaParser createByName( + @NonNull @ParserName String name, @NonNull OutputConsumer outputConsumer) { + String[] nameAsArray = new String[] {name}; + assertValidNames(nameAsArray); + return new MediaParser(outputConsumer, /* sniff= */ false, name); + } + + /** + * Creates an instance whose backing parser will be selected by sniffing the content during the + * first {@link #advance} call. Parser implementations will sniff the content in order of + * appearance in {@code parserNames}. + * + * @param outputConsumer The {@link OutputConsumer} to which extracted data is output. + * @param parserNames The names of the parsers to sniff the content with. If empty, a default + * array of names is used. + * @return A new instance. + */ + @NonNull + public static MediaParser create( + @NonNull OutputConsumer outputConsumer, @NonNull @ParserName String... parserNames) { + assertValidNames(parserNames); + if (parserNames.length == 0) { + parserNames = EXTRACTOR_FACTORIES_BY_NAME.keySet().toArray(new String[0]); + } + return new MediaParser(outputConsumer, /* sniff= */ true, parserNames); + } + + // Misc static methods. + + /** + * Returns an immutable list with the names of the parsers that are suitable for container + * formats with the given {@link MediaFormat}. + * + * <p>A parser supports a {@link MediaFormat} if the mime type associated with {@link + * MediaFormat#KEY_MIME} corresponds to the supported container format. + * + * @param mediaFormat The {@link MediaFormat} to check support for. + * @return The parser names that support the given {@code mediaFormat}, or the list of all + * parsers available if no container specific format information is provided. + */ + @NonNull + @ParserName + public static List<String> getParserNames(@NonNull MediaFormat mediaFormat) { + String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME); + mimeType = mimeType == null ? null : Util.toLowerInvariant(mimeType.trim()); + if (TextUtils.isEmpty(mimeType)) { + // No MIME type provided. Return all. + return Collections.unmodifiableList( + new ArrayList<>(EXTRACTOR_FACTORIES_BY_NAME.keySet())); + } + ArrayList<String> result = new ArrayList<>(); + switch (mimeType) { + case "video/x-matroska": + case "audio/x-matroska": + case "video/x-webm": + case "audio/x-webm": + result.add(PARSER_NAME_MATROSKA); + break; + case "video/mp4": + case "audio/mp4": + case "application/mp4": + result.add(PARSER_NAME_MP4); + result.add(PARSER_NAME_FMP4); + break; + case "audio/mpeg": + result.add(PARSER_NAME_MP3); + break; + case "audio/aac": + result.add(PARSER_NAME_ADTS); + break; + case "audio/ac3": + result.add(PARSER_NAME_AC3); + break; + case "video/mp2t": + case "audio/mp2t": + result.add(PARSER_NAME_TS); + break; + case "video/x-flv": + result.add(PARSER_NAME_FLV); + break; + case "video/ogg": + case "audio/ogg": + case "application/ogg": + result.add(PARSER_NAME_OGG); + break; + case "video/mp2p": + case "video/mp1s": + result.add(PARSER_NAME_PS); + break; + case "audio/vnd.wave": + case "audio/wav": + case "audio/wave": + case "audio/x-wav": + result.add(PARSER_NAME_WAV); + break; + case "audio/amr": + result.add(PARSER_NAME_AMR); + break; + case "audio/ac4": + result.add(PARSER_NAME_AC4); + break; + case "audio/flac": + case "audio/x-flac": + result.add(PARSER_NAME_FLAC); + break; + default: + // No parsers support the given mime type. Do nothing. + break; + } + return Collections.unmodifiableList(result); + } + + // Private fields. + + private final Map<String, Object> mParserParameters; + private final OutputConsumer mOutputConsumer; + private final String[] mParserNamesPool; + private final PositionHolder mPositionHolder; + private final InputReadingDataReader mExoDataReader; + private final DataReaderAdapter mScratchDataReaderAdapter; + private final ParsableByteArrayAdapter mScratchParsableByteArrayAdapter; + @Nullable private final Constructor<DrmInitData.SchemeInitData> mSchemeInitDataConstructor; + private final ArrayList<Format> mMuxedCaptionFormats; + private boolean mInBandCryptoInfo; + private boolean mIncludeSupplementalData; + private boolean mIgnoreTimestampOffset; + private boolean mEagerlyExposeTrackType; + private boolean mExposeDummySeekMap; + private boolean mExposeChunkIndexAsMediaFormat; + private String mParserName; + private Extractor mExtractor; + private ExtractorInput mExtractorInput; + private boolean mPendingExtractorInit; + private long mPendingSeekPosition; + private long mPendingSeekTimeMicros; + private boolean mLoggedSchemeInitDataCreationException; + + // Public methods. + + /** + * Sets parser-specific parameters which allow customizing behavior. + * + * <p>Must be called before the first call to {@link #advance}. + * + * @param parameterName The name of the parameter to set. See {@code PARAMETER_*} constants for + * documentation on possible values. + * @param value The value to set for the given {@code parameterName}. See {@code PARAMETER_*} + * constants for documentation on the expected types. + * @return This instance, for convenience. + * @throws IllegalStateException If called after calling {@link #advance} on the same instance. + */ + @NonNull + public MediaParser setParameter( + @NonNull @ParameterName String parameterName, @NonNull Object value) { + if (mExtractor != null) { + throw new IllegalStateException( + "setParameters() must be called before the first advance() call."); + } + Class expectedType = EXPECTED_TYPE_BY_PARAMETER_NAME.get(parameterName); + // Ignore parameter names that are not contained in the map, in case the client is passing + // a parameter that is being added in a future version of this library. + if (expectedType != null && !expectedType.isInstance(value)) { + throw new IllegalArgumentException( + parameterName + + " expects a " + + expectedType.getSimpleName() + + " but a " + + value.getClass().getSimpleName() + + " was passed."); + } + if (PARAMETER_TS_MODE.equals(parameterName) + && !TS_MODE_SINGLE_PMT.equals(value) + && !TS_MODE_HLS.equals(value) + && !TS_MODE_MULTI_PMT.equals(value)) { + throw new IllegalArgumentException(PARAMETER_TS_MODE + " does not accept: " + value); + } + if (PARAMETER_IN_BAND_CRYPTO_INFO.equals(parameterName)) { + mInBandCryptoInfo = (boolean) value; + } + if (PARAMETER_INCLUDE_SUPPLEMENTAL_DATA.equals(parameterName)) { + mIncludeSupplementalData = (boolean) value; + } + if (PARAMETER_IGNORE_TIMESTAMP_OFFSET.equals(parameterName)) { + mIgnoreTimestampOffset = (boolean) value; + } + if (PARAMETER_EAGERLY_EXPOSE_TRACKTYPE.equals(parameterName)) { + mEagerlyExposeTrackType = (boolean) value; + } + if (PARAMETER_EXPOSE_DUMMY_SEEKMAP.equals(parameterName)) { + mExposeDummySeekMap = (boolean) value; + } + if (PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT.equals(parameterName)) { + mExposeChunkIndexAsMediaFormat = (boolean) value; + } + if (PARAMETER_EXPOSE_CAPTION_FORMATS.equals(parameterName)) { + setMuxedCaptionFormats((List<MediaFormat>) value); + } + mParserParameters.put(parameterName, value); + return this; + } + + /** + * Returns whether the given {@code parameterName} is supported by this parser. + * + * @param parameterName The parameter name to check support for. One of the {@code PARAMETER_*} + * constants. + * @return Whether the given {@code parameterName} is supported. + */ + public boolean supportsParameter(@NonNull @ParameterName String parameterName) { + return EXPECTED_TYPE_BY_PARAMETER_NAME.containsKey(parameterName); + } + + /** + * Returns the name of the backing parser implementation. + * + * <p>If this instance was creating using {@link #createByName}, the provided name is returned. + * If this instance was created using {@link #create}, this method will return {@link + * #PARSER_NAME_UNKNOWN} until the first call to {@link #advance}, after which the name of the + * backing parser implementation is returned. + * + * @return The name of the backing parser implementation, or null if the backing parser + * implementation has not yet been selected. + */ + @NonNull + @ParserName + public String getParserName() { + return mParserName; + } + + /** + * Makes progress in the extraction of the input media stream, unless the end of the input has + * been reached. + * + * <p>This method will block until some progress has been made. + * + * <p>If this instance was created using {@link #create}, the first call to this method will + * sniff the content using the selected parser implementations. + * + * @param seekableInputReader The {@link SeekableInputReader} from which to obtain the media + * container data. + * @return Whether there is any data left to extract. Returns false if the end of input has been + * reached. + * @throws IOException If an error occurs while reading from the {@link SeekableInputReader}. + * @throws UnrecognizedInputFormatException If the format cannot be recognized by any of the + * underlying parser implementations. + */ + public boolean advance(@NonNull SeekableInputReader seekableInputReader) throws IOException { + if (mExtractorInput == null) { + // TODO: For efficiency, the same implementation should be used, by providing a + // clearBuffers() method, or similar. + mExtractorInput = + new DefaultExtractorInput( + mExoDataReader, + seekableInputReader.getPosition(), + seekableInputReader.getLength()); + } + mExoDataReader.mInputReader = seekableInputReader; + + if (mExtractor == null) { + mPendingExtractorInit = true; + if (!mParserName.equals(PARSER_NAME_UNKNOWN)) { + mExtractor = createExtractor(mParserName); + } else { + for (String parserName : mParserNamesPool) { + Extractor extractor = createExtractor(parserName); + try { + if (extractor.sniff(mExtractorInput)) { + mParserName = parserName; + mExtractor = extractor; + mPendingExtractorInit = true; + break; + } + } catch (EOFException e) { + // Do nothing. + } finally { + mExtractorInput.resetPeekPosition(); + } + } + if (mExtractor == null) { + throw UnrecognizedInputFormatException.createForExtractors(mParserNamesPool); + } + return true; + } + } + + if (mPendingExtractorInit) { + if (mExposeDummySeekMap) { + // We propagate the dummy seek map before initializing the extractor, in case the + // extractor initialization outputs a seek map. + mOutputConsumer.onSeekMapFound(SeekMap.DUMMY); + } + mExtractor.init(new ExtractorOutputAdapter()); + mPendingExtractorInit = false; + // We return after initialization to allow clients use any output information before + // starting actual extraction. + return true; + } + + if (isPendingSeek()) { + mExtractor.seek(mPendingSeekPosition, mPendingSeekTimeMicros); + removePendingSeek(); + } + + mPositionHolder.position = seekableInputReader.getPosition(); + int result; + try { + result = mExtractor.read(mExtractorInput, mPositionHolder); + } catch (ParserException e) { + throw new ParsingException(e); + } + if (result == Extractor.RESULT_END_OF_INPUT) { + mExtractorInput = null; + return false; + } + if (result == Extractor.RESULT_SEEK) { + mExtractorInput = null; + seekableInputReader.seekToPosition(mPositionHolder.position); + } + return true; + } + + /** + * Seeks within the media container being extracted. + * + * <p>{@link SeekPoint SeekPoints} can be obtained from the {@link SeekMap} passed to {@link + * OutputConsumer#onSeekMapFound(SeekMap)}. + * + * <p>Following a call to this method, the {@link InputReader} passed to the next invocation of + * {@link #advance} must provide data starting from {@link SeekPoint#position} in the stream. + * + * @param seekPoint The {@link SeekPoint} to seek to. + */ + public void seek(@NonNull SeekPoint seekPoint) { + if (mExtractor == null) { + mPendingSeekPosition = seekPoint.position; + mPendingSeekTimeMicros = seekPoint.timeMicros; + } else { + mExtractor.seek(seekPoint.position, seekPoint.timeMicros); + } + } + + /** + * Releases any acquired resources. + * + * <p>After calling this method, this instance becomes unusable and no other methods should be + * invoked. + */ + public void release() { + // TODO: Dump media metrics here. + mExtractorInput = null; + mExtractor = null; + } + + // Private methods. + + private MediaParser(OutputConsumer outputConsumer, boolean sniff, String... parserNamesPool) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + throw new UnsupportedOperationException("Android version must be R or greater."); + } + mParserParameters = new HashMap<>(); + mOutputConsumer = outputConsumer; + mParserNamesPool = parserNamesPool; + mParserName = sniff ? PARSER_NAME_UNKNOWN : parserNamesPool[0]; + mPositionHolder = new PositionHolder(); + mExoDataReader = new InputReadingDataReader(); + removePendingSeek(); + mScratchDataReaderAdapter = new DataReaderAdapter(); + mScratchParsableByteArrayAdapter = new ParsableByteArrayAdapter(); + mSchemeInitDataConstructor = getSchemeInitDataConstructor(); + mMuxedCaptionFormats = new ArrayList<>(); + } + + private void setMuxedCaptionFormats(List<MediaFormat> mediaFormats) { + mMuxedCaptionFormats.clear(); + for (MediaFormat mediaFormat : mediaFormats) { + mMuxedCaptionFormats.add(toExoPlayerCaptionFormat(mediaFormat)); + } + } + + private boolean isPendingSeek() { + return mPendingSeekPosition >= 0; + } + + private void removePendingSeek() { + mPendingSeekPosition = -1; + mPendingSeekTimeMicros = -1; + } + + private Extractor createExtractor(String parserName) { + int flags = 0; + TimestampAdjuster timestampAdjuster = null; + if (mIgnoreTimestampOffset) { + timestampAdjuster = new TimestampAdjuster(TimestampAdjuster.DO_NOT_OFFSET); + } + switch (parserName) { + case PARSER_NAME_MATROSKA: + flags = + getBooleanParameter(PARAMETER_MATROSKA_DISABLE_CUES_SEEKING) + ? MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES + : 0; + return new MatroskaExtractor(flags); + case PARSER_NAME_FMP4: + flags |= + getBooleanParameter(PARAMETER_EXPOSE_EMSG_TRACK) + ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK + : 0; + flags |= + getBooleanParameter(PARAMETER_MP4_IGNORE_EDIT_LISTS) + ? FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTS + : 0; + flags |= + getBooleanParameter(PARAMETER_MP4_IGNORE_TFDT_BOX) + ? FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX + : 0; + flags |= + getBooleanParameter(PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES) + ? FragmentedMp4Extractor + .FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME + : 0; + return new FragmentedMp4Extractor( + flags, + timestampAdjuster, + /* sideloadedTrack= */ null, + mMuxedCaptionFormats); + case PARSER_NAME_MP4: + flags |= + getBooleanParameter(PARAMETER_MP4_IGNORE_EDIT_LISTS) + ? Mp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTS + : 0; + return new Mp4Extractor(flags); + case PARSER_NAME_MP3: + flags |= + getBooleanParameter(PARAMETER_MP3_DISABLE_ID3) + ? Mp3Extractor.FLAG_DISABLE_ID3_METADATA + : 0; + flags |= + getBooleanParameter(PARAMETER_MP3_ENABLE_CBR_SEEKING) + ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0; + // TODO: Add index seeking once we update the ExoPlayer version. + return new Mp3Extractor(flags); + case PARSER_NAME_ADTS: + flags |= + getBooleanParameter(PARAMETER_ADTS_ENABLE_CBR_SEEKING) + ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0; + return new AdtsExtractor(flags); + case PARSER_NAME_AC3: + return new Ac3Extractor(); + case PARSER_NAME_TS: + flags |= + getBooleanParameter(PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES) + ? DefaultTsPayloadReaderFactory.FLAG_ALLOW_NON_IDR_KEYFRAMES + : 0; + flags |= + getBooleanParameter(PARAMETER_TS_DETECT_ACCESS_UNITS) + ? DefaultTsPayloadReaderFactory.FLAG_DETECT_ACCESS_UNITS + : 0; + flags |= + getBooleanParameter(PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS) + ? DefaultTsPayloadReaderFactory.FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS + : 0; + flags |= + getBooleanParameter(PARAMETER_TS_IGNORE_AAC_STREAM) + ? DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM + : 0; + flags |= + getBooleanParameter(PARAMETER_TS_IGNORE_AVC_STREAM) + ? DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM + : 0; + flags |= + getBooleanParameter(PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM) + ? DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM + : 0; + flags |= + getBooleanParameter(PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS) + ? DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS + : 0; + String tsMode = getStringParameter(PARAMETER_TS_MODE, TS_MODE_SINGLE_PMT); + int hlsMode = + TS_MODE_SINGLE_PMT.equals(tsMode) + ? TsExtractor.MODE_SINGLE_PMT + : TS_MODE_HLS.equals(tsMode) + ? TsExtractor.MODE_HLS + : TsExtractor.MODE_MULTI_PMT; + return new TsExtractor( + hlsMode, + timestampAdjuster != null + ? timestampAdjuster + : new TimestampAdjuster(/* firstSampleTimestampUs= */ 0), + new DefaultTsPayloadReaderFactory(flags, mMuxedCaptionFormats)); + case PARSER_NAME_FLV: + return new FlvExtractor(); + case PARSER_NAME_OGG: + return new OggExtractor(); + case PARSER_NAME_PS: + return new PsExtractor(); + case PARSER_NAME_WAV: + return new WavExtractor(); + case PARSER_NAME_AMR: + flags |= + getBooleanParameter(PARAMETER_AMR_ENABLE_CBR_SEEKING) + ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0; + return new AmrExtractor(flags); + case PARSER_NAME_AC4: + return new Ac4Extractor(); + case PARSER_NAME_FLAC: + flags |= + getBooleanParameter(PARAMETER_FLAC_DISABLE_ID3) + ? FlacExtractor.FLAG_DISABLE_ID3_METADATA + : 0; + return new FlacExtractor(flags); + default: + // Should never happen. + throw new IllegalStateException("Unexpected attempt to create: " + parserName); + } + } + + private boolean getBooleanParameter(String name) { + return (boolean) mParserParameters.getOrDefault(name, false); + } + + private String getStringParameter(String name, String defaultValue) { + return (String) mParserParameters.getOrDefault(name, defaultValue); + } + + // Private classes. + + private static final class InputReadingDataReader implements DataReader { + + public InputReader mInputReader; + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + return mInputReader.read(buffer, offset, readLength); + } + } + + private final class MediaParserDrmInitData extends DrmInitData { + + private final SchemeInitData[] mSchemeDatas; + + private MediaParserDrmInitData(com.google.android.exoplayer2.drm.DrmInitData exoDrmInitData) + throws IllegalAccessException, InstantiationException, InvocationTargetException { + mSchemeDatas = new SchemeInitData[exoDrmInitData.schemeDataCount]; + for (int i = 0; i < mSchemeDatas.length; i++) { + mSchemeDatas[i] = toFrameworkSchemeInitData(exoDrmInitData.get(i)); + } + } + + @Override + @Nullable + public SchemeInitData get(UUID schemeUuid) { + for (SchemeInitData schemeInitData : mSchemeDatas) { + if (schemeInitData.uuid.equals(schemeUuid)) { + return schemeInitData; + } + } + return null; + } + + @Override + public SchemeInitData getSchemeInitDataAt(int index) { + return mSchemeDatas[index]; + } + + @Override + public int getSchemeInitDataCount() { + return mSchemeDatas.length; + } + + private DrmInitData.SchemeInitData toFrameworkSchemeInitData(SchemeData exoSchemeData) + throws IllegalAccessException, InvocationTargetException, InstantiationException { + return mSchemeInitDataConstructor.newInstance( + exoSchemeData.uuid, exoSchemeData.mimeType, exoSchemeData.data); + } + } + + private final class ExtractorOutputAdapter implements ExtractorOutput { + + private final SparseArray<TrackOutput> mTrackOutputAdapters; + private boolean mTracksEnded; + + private ExtractorOutputAdapter() { + mTrackOutputAdapters = new SparseArray<>(); + } + + @Override + public TrackOutput track(int id, int type) { + TrackOutput trackOutput = mTrackOutputAdapters.get(id); + if (trackOutput == null) { + int trackIndex = mTrackOutputAdapters.size(); + trackOutput = new TrackOutputAdapter(trackIndex); + mTrackOutputAdapters.put(id, trackOutput); + if (mEagerlyExposeTrackType) { + MediaFormat mediaFormat = new MediaFormat(); + mediaFormat.setString("track-type-string", toTypeString(type)); + mOutputConsumer.onTrackDataFound( + trackIndex, new TrackData(mediaFormat, /* drmInitData= */ null)); + } + } + return trackOutput; + } + + @Override + public void endTracks() { + mOutputConsumer.onTrackCountFound(mTrackOutputAdapters.size()); + } + + @Override + public void seekMap(com.google.android.exoplayer2.extractor.SeekMap exoplayerSeekMap) { + if (mExposeChunkIndexAsMediaFormat && exoplayerSeekMap instanceof ChunkIndex) { + ChunkIndex chunkIndex = (ChunkIndex) exoplayerSeekMap; + MediaFormat mediaFormat = new MediaFormat(); + mediaFormat.setByteBuffer("chunk-index-int-sizes", toByteBuffer(chunkIndex.sizes)); + mediaFormat.setByteBuffer( + "chunk-index-long-offsets", toByteBuffer(chunkIndex.offsets)); + mediaFormat.setByteBuffer( + "chunk-index-long-us-durations", toByteBuffer(chunkIndex.durationsUs)); + mediaFormat.setByteBuffer( + "chunk-index-long-us-times", toByteBuffer(chunkIndex.timesUs)); + mOutputConsumer.onTrackDataFound( + /* trackIndex= */ 0, new TrackData(mediaFormat, /* drmInitData= */ null)); + } + mOutputConsumer.onSeekMapFound(new SeekMap(exoplayerSeekMap)); + } + } + + private class TrackOutputAdapter implements TrackOutput { + + private final int mTrackIndex; + + private CryptoInfo mLastOutputCryptoInfo; + private CryptoInfo.Pattern mLastOutputEncryptionPattern; + private CryptoData mLastReceivedCryptoData; + + @EncryptionDataReadState private int mEncryptionDataReadState; + private int mEncryptionDataSizeToSubtractFromSampleDataSize; + private int mEncryptionVectorSize; + private byte[] mScratchIvSpace; + private int mSubsampleEncryptionDataSize; + private int[] mScratchSubsampleEncryptedBytesCount; + private int[] mScratchSubsampleClearBytesCount; + private boolean mHasSubsampleEncryptionData; + private int mSkippedSupplementalDataBytes; + + private TrackOutputAdapter(int trackIndex) { + mTrackIndex = trackIndex; + mScratchIvSpace = new byte[16]; // Size documented in CryptoInfo. + mScratchSubsampleEncryptedBytesCount = new int[32]; + mScratchSubsampleClearBytesCount = new int[32]; + mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE; + mLastOutputEncryptionPattern = + new CryptoInfo.Pattern(/* blocksToEncrypt= */ 0, /* blocksToSkip= */ 0); + } + + @Override + public void format(Format format) { + mOutputConsumer.onTrackDataFound( + mTrackIndex, + new TrackData( + toMediaFormat(format), toFrameworkDrmInitData(format.drmInitData))); + } + + @Override + public int sampleData( + DataReader input, + int length, + boolean allowEndOfInput, + @SampleDataPart int sampleDataPart) + throws IOException { + mScratchDataReaderAdapter.setDataReader(input, length); + long positionBeforeReading = mScratchDataReaderAdapter.getPosition(); + mOutputConsumer.onSampleDataFound(mTrackIndex, mScratchDataReaderAdapter); + return (int) (mScratchDataReaderAdapter.getPosition() - positionBeforeReading); + } + + @Override + public void sampleData( + ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) { + if (sampleDataPart == SAMPLE_DATA_PART_ENCRYPTION && !mInBandCryptoInfo) { + while (length > 0) { + switch (mEncryptionDataReadState) { + case STATE_READING_SIGNAL_BYTE: + int encryptionSignalByte = data.readUnsignedByte(); + length--; + mHasSubsampleEncryptionData = ((encryptionSignalByte >> 7) & 1) != 0; + mEncryptionVectorSize = encryptionSignalByte & 0x7F; + mEncryptionDataSizeToSubtractFromSampleDataSize = + mEncryptionVectorSize + 1; // Signal byte. + mEncryptionDataReadState = STATE_READING_INIT_VECTOR; + break; + case STATE_READING_INIT_VECTOR: + Arrays.fill(mScratchIvSpace, (byte) 0); // Ensure 0-padding. + data.readBytes(mScratchIvSpace, /* offset= */ 0, mEncryptionVectorSize); + length -= mEncryptionVectorSize; + if (mHasSubsampleEncryptionData) { + mEncryptionDataReadState = STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE; + } else { + mSubsampleEncryptionDataSize = 0; + mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE; + } + break; + case STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE: + mSubsampleEncryptionDataSize = data.readUnsignedShort(); + if (mScratchSubsampleClearBytesCount.length + < mSubsampleEncryptionDataSize) { + mScratchSubsampleClearBytesCount = + new int[mSubsampleEncryptionDataSize]; + mScratchSubsampleEncryptedBytesCount = + new int[mSubsampleEncryptionDataSize]; + } + length -= 2; + mEncryptionDataSizeToSubtractFromSampleDataSize += + 2 + + mSubsampleEncryptionDataSize + * BYTES_PER_SUBSAMPLE_ENCRYPTION_ENTRY; + mEncryptionDataReadState = STATE_READING_SUBSAMPLE_ENCRYPTION_DATA; + break; + case STATE_READING_SUBSAMPLE_ENCRYPTION_DATA: + for (int i = 0; i < mSubsampleEncryptionDataSize; i++) { + mScratchSubsampleClearBytesCount[i] = data.readUnsignedShort(); + mScratchSubsampleEncryptedBytesCount[i] = data.readInt(); + } + length -= + mSubsampleEncryptionDataSize + * BYTES_PER_SUBSAMPLE_ENCRYPTION_ENTRY; + mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE; + if (length != 0) { + throw new IllegalStateException(); + } + break; + default: + // Never happens. + throw new IllegalStateException(); + } + } + } else if (sampleDataPart == SAMPLE_DATA_PART_SUPPLEMENTAL + && !mIncludeSupplementalData) { + mSkippedSupplementalDataBytes += length; + data.skipBytes(length); + } else { + outputSampleData(data, length); + } + } + + @Override + public void sampleMetadata( + long timeUs, int flags, int size, int offset, @Nullable CryptoData cryptoData) { + size -= mSkippedSupplementalDataBytes; + mSkippedSupplementalDataBytes = 0; + mOutputConsumer.onSampleCompleted( + mTrackIndex, + timeUs, + getMediaParserFlags(flags), + size - mEncryptionDataSizeToSubtractFromSampleDataSize, + offset, + getPopulatedCryptoInfo(cryptoData)); + mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE; + mEncryptionDataSizeToSubtractFromSampleDataSize = 0; + } + + @Nullable + private CryptoInfo getPopulatedCryptoInfo(@Nullable CryptoData cryptoData) { + if (cryptoData == null) { + // The sample is not encrypted. + return null; + } else if (mInBandCryptoInfo) { + if (cryptoData != mLastReceivedCryptoData) { + mLastOutputCryptoInfo = + createNewCryptoInfoAndPopulateWithCryptoData(cryptoData); + // We are using in-band crypto info, so the IV will be ignored. But we prevent + // it from being null because toString assumes it non-null. + mLastOutputCryptoInfo.iv = EMPTY_BYTE_ARRAY; + } + } else /* We must populate the full CryptoInfo. */ { + // CryptoInfo.pattern is not accessible to the user, so the user needs to feed + // this CryptoInfo directly to MediaCodec. We need to create a new CryptoInfo per + // sample because of per-sample initialization vector changes. + CryptoInfo newCryptoInfo = createNewCryptoInfoAndPopulateWithCryptoData(cryptoData); + newCryptoInfo.iv = Arrays.copyOf(mScratchIvSpace, mScratchIvSpace.length); + boolean canReuseSubsampleInfo = + mLastOutputCryptoInfo != null + && mLastOutputCryptoInfo.numSubSamples + == mSubsampleEncryptionDataSize; + for (int i = 0; i < mSubsampleEncryptionDataSize && canReuseSubsampleInfo; i++) { + canReuseSubsampleInfo = + mLastOutputCryptoInfo.numBytesOfClearData[i] + == mScratchSubsampleClearBytesCount[i] + && mLastOutputCryptoInfo.numBytesOfEncryptedData[i] + == mScratchSubsampleEncryptedBytesCount[i]; + } + newCryptoInfo.numSubSamples = mSubsampleEncryptionDataSize; + if (canReuseSubsampleInfo) { + newCryptoInfo.numBytesOfClearData = mLastOutputCryptoInfo.numBytesOfClearData; + newCryptoInfo.numBytesOfEncryptedData = + mLastOutputCryptoInfo.numBytesOfEncryptedData; + } else { + newCryptoInfo.numBytesOfClearData = + Arrays.copyOf( + mScratchSubsampleClearBytesCount, mSubsampleEncryptionDataSize); + newCryptoInfo.numBytesOfEncryptedData = + Arrays.copyOf( + mScratchSubsampleEncryptedBytesCount, + mSubsampleEncryptionDataSize); + } + mLastOutputCryptoInfo = newCryptoInfo; + } + mLastReceivedCryptoData = cryptoData; + return mLastOutputCryptoInfo; + } + + private CryptoInfo createNewCryptoInfoAndPopulateWithCryptoData(CryptoData cryptoData) { + CryptoInfo cryptoInfo = new CryptoInfo(); + cryptoInfo.key = cryptoData.encryptionKey; + cryptoInfo.mode = cryptoData.cryptoMode; + if (cryptoData.clearBlocks != mLastOutputEncryptionPattern.getSkipBlocks() + || cryptoData.encryptedBlocks + != mLastOutputEncryptionPattern.getEncryptBlocks()) { + mLastOutputEncryptionPattern = + new CryptoInfo.Pattern(cryptoData.encryptedBlocks, cryptoData.clearBlocks); + } + cryptoInfo.setPattern(mLastOutputEncryptionPattern); + return cryptoInfo; + } + + private void outputSampleData(ParsableByteArray data, int length) { + mScratchParsableByteArrayAdapter.resetWithByteArray(data, length); + try { + // Read all bytes from data. ExoPlayer extractors expect all sample data to be + // consumed by TrackOutput implementations when passing a ParsableByteArray. + while (mScratchParsableByteArrayAdapter.getLength() > 0) { + mOutputConsumer.onSampleDataFound( + mTrackIndex, mScratchParsableByteArrayAdapter); + } + } catch (IOException e) { + // Unexpected. + throw new RuntimeException(e); + } + } + } + + private static final class DataReaderAdapter implements InputReader { + + private DataReader mDataReader; + private int mCurrentPosition; + private long mLength; + + public void setDataReader(DataReader dataReader, long length) { + mDataReader = dataReader; + mCurrentPosition = 0; + mLength = length; + } + + // Input implementation. + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + int readBytes = 0; + readBytes = mDataReader.read(buffer, offset, readLength); + mCurrentPosition += readBytes; + return readBytes; + } + + @Override + public long getPosition() { + return mCurrentPosition; + } + + @Override + public long getLength() { + return mLength - mCurrentPosition; + } + } + + private static final class ParsableByteArrayAdapter implements InputReader { + + private ParsableByteArray mByteArray; + private long mLength; + private int mCurrentPosition; + + public void resetWithByteArray(ParsableByteArray byteArray, long length) { + mByteArray = byteArray; + mCurrentPosition = 0; + mLength = length; + } + + // Input implementation. + + @Override + public int read(byte[] buffer, int offset, int readLength) { + mByteArray.readBytes(buffer, offset, readLength); + mCurrentPosition += readLength; + return readLength; + } + + @Override + public long getPosition() { + return mCurrentPosition; + } + + @Override + public long getLength() { + return mLength - mCurrentPosition; + } + } + + private static final class DummyExoPlayerSeekMap + implements com.google.android.exoplayer2.extractor.SeekMap { + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return C.TIME_UNSET; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + com.google.android.exoplayer2.extractor.SeekPoint seekPoint = + new com.google.android.exoplayer2.extractor.SeekPoint( + timeUs, /* position= */ 0); + return new SeekPoints(seekPoint, seekPoint); + } + } + + /** Creates extractor instances. */ + private interface ExtractorFactory { + + /** Returns a new extractor instance. */ + Extractor createInstance(); + } + + // Private static methods. + + private static Format toExoPlayerCaptionFormat(MediaFormat mediaFormat) { + Format.Builder formatBuilder = + new Format.Builder().setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME)); + if (mediaFormat.containsKey(MediaFormat.KEY_CAPTION_SERVICE_NUMBER)) { + formatBuilder.setAccessibilityChannel( + mediaFormat.getInteger(MediaFormat.KEY_CAPTION_SERVICE_NUMBER)); + } + return formatBuilder.build(); + } + + private static MediaFormat toMediaFormat(Format format) { + MediaFormat result = new MediaFormat(); + setOptionalMediaFormatInt(result, MediaFormat.KEY_BIT_RATE, format.bitrate); + setOptionalMediaFormatInt(result, MediaFormat.KEY_CHANNEL_COUNT, format.channelCount); + + ColorInfo colorInfo = format.colorInfo; + if (colorInfo != null) { + setOptionalMediaFormatInt( + result, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer); + setOptionalMediaFormatInt(result, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange); + setOptionalMediaFormatInt(result, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace); + + if (format.colorInfo.hdrStaticInfo != null) { + result.setByteBuffer( + MediaFormat.KEY_HDR_STATIC_INFO, + ByteBuffer.wrap(format.colorInfo.hdrStaticInfo)); + } + } + + setOptionalMediaFormatString(result, MediaFormat.KEY_MIME, format.sampleMimeType); + setOptionalMediaFormatString(result, MediaFormat.KEY_CODECS_STRING, format.codecs); + if (format.frameRate != Format.NO_VALUE) { + result.setFloat(MediaFormat.KEY_FRAME_RATE, format.frameRate); + } + setOptionalMediaFormatInt(result, MediaFormat.KEY_WIDTH, format.width); + setOptionalMediaFormatInt(result, MediaFormat.KEY_HEIGHT, format.height); + + List<byte[]> initData = format.initializationData; + for (int i = 0; i < initData.size(); i++) { + result.setByteBuffer("csd-" + i, ByteBuffer.wrap(initData.get(i))); + } + setPcmEncoding(format, result); + setOptionalMediaFormatString(result, MediaFormat.KEY_LANGUAGE, format.language); + setOptionalMediaFormatInt(result, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); + setOptionalMediaFormatInt(result, MediaFormat.KEY_ROTATION, format.rotationDegrees); + setOptionalMediaFormatInt(result, MediaFormat.KEY_SAMPLE_RATE, format.sampleRate); + setOptionalMediaFormatInt( + result, MediaFormat.KEY_CAPTION_SERVICE_NUMBER, format.accessibilityChannel); + + int selectionFlags = format.selectionFlags; + result.setInteger( + MediaFormat.KEY_IS_AUTOSELECT, selectionFlags & C.SELECTION_FLAG_AUTOSELECT); + result.setInteger(MediaFormat.KEY_IS_DEFAULT, selectionFlags & C.SELECTION_FLAG_DEFAULT); + result.setInteger( + MediaFormat.KEY_IS_FORCED_SUBTITLE, selectionFlags & C.SELECTION_FLAG_FORCED); + + setOptionalMediaFormatInt(result, MediaFormat.KEY_ENCODER_DELAY, format.encoderDelay); + setOptionalMediaFormatInt(result, MediaFormat.KEY_ENCODER_PADDING, format.encoderPadding); + + if (format.pixelWidthHeightRatio != Format.NO_VALUE && format.pixelWidthHeightRatio != 0) { + int parWidth = 1; + int parHeight = 1; + if (format.pixelWidthHeightRatio < 1.0f) { + parHeight = 1 << 30; + parWidth = (int) (format.pixelWidthHeightRatio * parHeight); + } else if (format.pixelWidthHeightRatio > 1.0f) { + parWidth = 1 << 30; + parHeight = (int) (parWidth / format.pixelWidthHeightRatio); + } + result.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH, parWidth); + result.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT, parHeight); + result.setFloat("pixel-width-height-ratio-float", format.pixelWidthHeightRatio); + } + if (format.drmInitData != null) { + // The crypto mode is propagated along with sample metadata. We also include it in the + // format for convenient use from ExoPlayer. + result.setString("crypto-mode-fourcc", format.drmInitData.schemeType); + } + if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { + result.setLong("subsample-offset-us-long", format.subsampleOffsetUs); + } + // LACK OF SUPPORT FOR: + // format.id; + // format.metadata; + // format.stereoMode; + return result; + } + + private static ByteBuffer toByteBuffer(long[] longArray) { + ByteBuffer byteBuffer = ByteBuffer.allocateDirect(longArray.length * Long.BYTES); + for (long element : longArray) { + byteBuffer.putLong(element); + } + byteBuffer.flip(); + return byteBuffer; + } + + private static ByteBuffer toByteBuffer(int[] intArray) { + ByteBuffer byteBuffer = ByteBuffer.allocateDirect(intArray.length * Integer.BYTES); + for (int element : intArray) { + byteBuffer.putInt(element); + } + byteBuffer.flip(); + return byteBuffer; + } + + private static String toTypeString(int type) { + switch (type) { + case C.TRACK_TYPE_VIDEO: + return "video"; + case C.TRACK_TYPE_AUDIO: + return "audio"; + case C.TRACK_TYPE_TEXT: + return "text"; + case C.TRACK_TYPE_METADATA: + return "metadata"; + default: + return "unknown"; + } + } + + private static void setPcmEncoding(Format format, MediaFormat result) { + int exoPcmEncoding = format.pcmEncoding; + setOptionalMediaFormatInt(result, "exo-pcm-encoding", format.pcmEncoding); + int mediaFormatPcmEncoding; + switch (exoPcmEncoding) { + case C.ENCODING_PCM_8BIT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_8BIT; + break; + case C.ENCODING_PCM_16BIT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_16BIT; + break; + case C.ENCODING_PCM_FLOAT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_FLOAT; + break; + default: + // No matching value. Do nothing. + return; + } + result.setInteger(MediaFormat.KEY_PCM_ENCODING, mediaFormatPcmEncoding); + } + + private static void setOptionalMediaFormatInt(MediaFormat mediaFormat, String key, int value) { + if (value != Format.NO_VALUE) { + mediaFormat.setInteger(key, value); + } + } + + private static void setOptionalMediaFormatString( + MediaFormat mediaFormat, String key, @Nullable String value) { + if (value != null) { + mediaFormat.setString(key, value); + } + } + + private DrmInitData toFrameworkDrmInitData( + com.google.android.exoplayer2.drm.DrmInitData exoDrmInitData) { + try { + return exoDrmInitData != null && mSchemeInitDataConstructor != null + ? new MediaParserDrmInitData(exoDrmInitData) + : null; + } catch (Throwable e) { + if (!mLoggedSchemeInitDataCreationException) { + mLoggedSchemeInitDataCreationException = true; + Log.e(TAG, "Unable to create SchemeInitData instance."); + } + return null; + } + } + + /** Returns a new {@link SeekPoint} equivalent to the given {@code exoPlayerSeekPoint}. */ + private static SeekPoint toSeekPoint( + com.google.android.exoplayer2.extractor.SeekPoint exoPlayerSeekPoint) { + return new SeekPoint(exoPlayerSeekPoint.timeUs, exoPlayerSeekPoint.position); + } + + private static void assertValidNames(@NonNull String[] names) { + for (String name : names) { + if (!EXTRACTOR_FACTORIES_BY_NAME.containsKey(name)) { + throw new IllegalArgumentException( + "Invalid extractor name: " + + name + + ". Supported parsers are: " + + TextUtils.join(", ", EXTRACTOR_FACTORIES_BY_NAME.keySet()) + + "."); + } + } + } + + private int getMediaParserFlags(int flags) { + @SampleFlags int result = 0; + result |= (flags & C.BUFFER_FLAG_ENCRYPTED) != 0 ? SAMPLE_FLAG_ENCRYPTED : 0; + result |= (flags & C.BUFFER_FLAG_KEY_FRAME) != 0 ? SAMPLE_FLAG_KEY_FRAME : 0; + result |= (flags & C.BUFFER_FLAG_DECODE_ONLY) != 0 ? SAMPLE_FLAG_DECODE_ONLY : 0; + result |= + (flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0 && mIncludeSupplementalData + ? SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA + : 0; + result |= (flags & C.BUFFER_FLAG_LAST_SAMPLE) != 0 ? SAMPLE_FLAG_LAST_SAMPLE : 0; + return result; + } + + @Nullable + private static Constructor<DrmInitData.SchemeInitData> getSchemeInitDataConstructor() { + // TODO: Use constructor statically when available. + Constructor<DrmInitData.SchemeInitData> constructor; + try { + return DrmInitData.SchemeInitData.class.getConstructor( + UUID.class, String.class, byte[].class); + } catch (Throwable e) { + Log.e(TAG, "Unable to get SchemeInitData constructor."); + return null; + } + } + + // Static initialization. + + static { + // Using a LinkedHashMap to keep the insertion order when iterating over the keys. + LinkedHashMap<String, ExtractorFactory> extractorFactoriesByName = new LinkedHashMap<>(); + // Parsers are ordered to match ExoPlayer's DefaultExtractorsFactory extractor ordering, + // which in turn aims to minimize the chances of incorrect extractor selections. + extractorFactoriesByName.put(PARSER_NAME_MATROSKA, MatroskaExtractor::new); + extractorFactoriesByName.put(PARSER_NAME_FMP4, FragmentedMp4Extractor::new); + extractorFactoriesByName.put(PARSER_NAME_MP4, Mp4Extractor::new); + extractorFactoriesByName.put(PARSER_NAME_MP3, Mp3Extractor::new); + extractorFactoriesByName.put(PARSER_NAME_ADTS, AdtsExtractor::new); + extractorFactoriesByName.put(PARSER_NAME_AC3, Ac3Extractor::new); + extractorFactoriesByName.put(PARSER_NAME_TS, TsExtractor::new); + extractorFactoriesByName.put(PARSER_NAME_FLV, FlvExtractor::new); + extractorFactoriesByName.put(PARSER_NAME_OGG, OggExtractor::new); + extractorFactoriesByName.put(PARSER_NAME_PS, PsExtractor::new); + extractorFactoriesByName.put(PARSER_NAME_WAV, WavExtractor::new); + extractorFactoriesByName.put(PARSER_NAME_AMR, AmrExtractor::new); + extractorFactoriesByName.put(PARSER_NAME_AC4, Ac4Extractor::new); + extractorFactoriesByName.put(PARSER_NAME_FLAC, FlacExtractor::new); + EXTRACTOR_FACTORIES_BY_NAME = Collections.unmodifiableMap(extractorFactoriesByName); + + HashMap<String, Class> expectedTypeByParameterName = new HashMap<>(); + expectedTypeByParameterName.put(PARAMETER_ADTS_ENABLE_CBR_SEEKING, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_AMR_ENABLE_CBR_SEEKING, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_FLAC_DISABLE_ID3, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_MP4_IGNORE_EDIT_LISTS, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_MP4_IGNORE_TFDT_BOX, Boolean.class); + expectedTypeByParameterName.put( + PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_MATROSKA_DISABLE_CUES_SEEKING, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_MP3_DISABLE_ID3, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_MP3_ENABLE_CBR_SEEKING, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_MP3_ENABLE_INDEX_SEEKING, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_TS_MODE, String.class); + expectedTypeByParameterName.put(PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_TS_IGNORE_AAC_STREAM, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_TS_IGNORE_AVC_STREAM, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_TS_DETECT_ACCESS_UNITS, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_IN_BAND_CRYPTO_INFO, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_INCLUDE_SUPPLEMENTAL_DATA, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_IGNORE_TIMESTAMP_OFFSET, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_EAGERLY_EXPOSE_TRACKTYPE, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_EXPOSE_DUMMY_SEEKMAP, Boolean.class); + expectedTypeByParameterName.put( + PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT, Boolean.class); + expectedTypeByParameterName.put( + PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS, Boolean.class); + expectedTypeByParameterName.put(PARAMETER_EXPOSE_EMSG_TRACK, Boolean.class); + // We do not check PARAMETER_EXPOSE_CAPTION_FORMATS here, and we do it in setParameters + // instead. Checking that the value is a List is insufficient to catch wrong parameter + // value types. + EXPECTED_TYPE_BY_PARAMETER_NAME = Collections.unmodifiableMap(expectedTypeByParameterName); + } +} diff --git a/apex/media/framework/java/android/media/MediaSession2.java b/apex/media/framework/java/android/media/MediaSession2.java new file mode 100644 index 000000000000..6560afedab0f --- /dev/null +++ b/apex/media/framework/java/android/media/MediaSession2.java @@ -0,0 +1,907 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import static android.media.MediaConstants.KEY_ALLOWED_COMMANDS; +import static android.media.MediaConstants.KEY_CONNECTION_HINTS; +import static android.media.MediaConstants.KEY_PACKAGE_NAME; +import static android.media.MediaConstants.KEY_PID; +import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE; +import static android.media.MediaConstants.KEY_SESSION2LINK; +import static android.media.MediaConstants.KEY_TOKEN_EXTRAS; +import static android.media.Session2Command.Result.RESULT_ERROR_UNKNOWN_ERROR; +import static android.media.Session2Command.Result.RESULT_INFO_SKIPPED; +import static android.media.Session2Token.TYPE_SESSION; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.media.session.MediaSessionManager; +import android.media.session.MediaSessionManager.RemoteUserInfo; +import android.os.BadParcelableException; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; +import android.os.Process; +import android.os.ResultReceiver; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Allows a media app to expose its transport controls and playback information in a process to + * other processes including the Android framework and other apps. + */ +public class MediaSession2 implements AutoCloseable { + static final String TAG = "MediaSession2"; + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + // Note: This checks the uniqueness of a session ID only in a single process. + // When the framework becomes able to check the uniqueness, this logic should be removed. + //@GuardedBy("MediaSession.class") + private static final List<String> SESSION_ID_LIST = new ArrayList<>(); + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Object mLock = new Object(); + //@GuardedBy("mLock") + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Map<Controller2Link, ControllerInfo> mConnectedControllers = new HashMap<>(); + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Context mContext; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Executor mCallbackExecutor; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final SessionCallback mCallback; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Session2Link mSessionStub; + + private final String mSessionId; + private final PendingIntent mSessionActivity; + private final Session2Token mSessionToken; + private final MediaSessionManager mSessionManager; + private final Handler mResultHandler; + + //@GuardedBy("mLock") + private boolean mClosed; + //@GuardedBy("mLock") + private boolean mPlaybackActive; + //@GuardedBy("mLock") + private ForegroundServiceEventCallback mForegroundServiceEventCallback; + + MediaSession2(@NonNull Context context, @NonNull String id, PendingIntent sessionActivity, + @NonNull Executor callbackExecutor, @NonNull SessionCallback callback, + @NonNull Bundle tokenExtras) { + synchronized (MediaSession2.class) { + if (SESSION_ID_LIST.contains(id)) { + throw new IllegalStateException("Session ID must be unique. ID=" + id); + } + SESSION_ID_LIST.add(id); + } + + mContext = context; + mSessionId = id; + mSessionActivity = sessionActivity; + mCallbackExecutor = callbackExecutor; + mCallback = callback; + mSessionStub = new Session2Link(this); + mSessionToken = new Session2Token(Process.myUid(), TYPE_SESSION, context.getPackageName(), + mSessionStub, tokenExtras); + mSessionManager = (MediaSessionManager) mContext.getSystemService( + Context.MEDIA_SESSION_SERVICE); + // NOTE: mResultHandler uses main looper, so this MUST NOT be blocked. + mResultHandler = new Handler(context.getMainLooper()); + mClosed = false; + } + + @Override + public void close() { + try { + List<ControllerInfo> controllerInfos; + ForegroundServiceEventCallback callback; + synchronized (mLock) { + if (mClosed) { + return; + } + mClosed = true; + controllerInfos = getConnectedControllers(); + mConnectedControllers.clear(); + callback = mForegroundServiceEventCallback; + mForegroundServiceEventCallback = null; + } + synchronized (MediaSession2.class) { + SESSION_ID_LIST.remove(mSessionId); + } + if (callback != null) { + callback.onSessionClosed(this); + } + for (ControllerInfo info : controllerInfos) { + info.notifyDisconnected(); + } + } catch (Exception e) { + // Should not be here. + } + } + + /** + * Returns the session ID + */ + @NonNull + public String getId() { + return mSessionId; + } + + /** + * Returns the {@link Session2Token} for creating {@link MediaController2}. + */ + @NonNull + public Session2Token getToken() { + return mSessionToken; + } + + /** + * Broadcasts a session command to all the connected controllers + * <p> + * @param command the session command + * @param args optional arguments + */ + public void broadcastSessionCommand(@NonNull Session2Command command, @Nullable Bundle args) { + if (command == null) { + throw new IllegalArgumentException("command shouldn't be null"); + } + List<ControllerInfo> controllerInfos = getConnectedControllers(); + for (ControllerInfo controller : controllerInfos) { + controller.sendSessionCommand(command, args, null); + } + } + + /** + * Sends a session command to a specific controller + * <p> + * @param controller the controller to get the session command + * @param command the session command + * @param args optional arguments + * @return a token which will be sent together in {@link SessionCallback#onCommandResult} + * when its result is received. + */ + @NonNull + public Object sendSessionCommand(@NonNull ControllerInfo controller, + @NonNull Session2Command command, @Nullable Bundle args) { + if (controller == null) { + throw new IllegalArgumentException("controller shouldn't be null"); + } + if (command == null) { + throw new IllegalArgumentException("command shouldn't be null"); + } + ResultReceiver resultReceiver = new ResultReceiver(mResultHandler) { + protected void onReceiveResult(int resultCode, Bundle resultData) { + controller.receiveCommandResult(this); + mCallbackExecutor.execute(() -> { + mCallback.onCommandResult(MediaSession2.this, controller, this, + command, new Session2Command.Result(resultCode, resultData)); + }); + } + }; + controller.sendSessionCommand(command, args, resultReceiver); + return resultReceiver; + } + + /** + * Cancels the session command previously sent. + * + * @param controller the controller to get the session command + * @param token the token which is returned from {@link #sendSessionCommand}. + */ + public void cancelSessionCommand(@NonNull ControllerInfo controller, @NonNull Object token) { + if (controller == null) { + throw new IllegalArgumentException("controller shouldn't be null"); + } + if (token == null) { + throw new IllegalArgumentException("token shouldn't be null"); + } + controller.cancelSessionCommand(token); + } + + /** + * Sets whether the playback is active (i.e. playing something) + * + * @param playbackActive {@code true} if the playback active, {@code false} otherwise. + **/ + public void setPlaybackActive(boolean playbackActive) { + final ForegroundServiceEventCallback serviceCallback; + synchronized (mLock) { + if (mPlaybackActive == playbackActive) { + return; + } + mPlaybackActive = playbackActive; + serviceCallback = mForegroundServiceEventCallback; + } + if (serviceCallback != null) { + serviceCallback.onPlaybackActiveChanged(this, playbackActive); + } + List<ControllerInfo> controllerInfos = getConnectedControllers(); + for (ControllerInfo controller : controllerInfos) { + controller.notifyPlaybackActiveChanged(playbackActive); + } + } + + /** + * Returns whehther the playback is active (i.e. playing something) + * + * @return {@code true} if the playback active, {@code false} otherwise. + */ + public boolean isPlaybackActive() { + synchronized (mLock) { + return mPlaybackActive; + } + } + + /** + * Gets the list of the connected controllers + * + * @return list of the connected controllers. + */ + @NonNull + public List<ControllerInfo> getConnectedControllers() { + List<ControllerInfo> controllers = new ArrayList<>(); + synchronized (mLock) { + controllers.addAll(mConnectedControllers.values()); + } + return controllers; + } + + /** + * Returns whether the given bundle includes non-framework Parcelables. + */ + static boolean hasCustomParcelable(@Nullable Bundle bundle) { + if (bundle == null) { + return false; + } + + // Try writing the bundle to parcel, and read it with framework classloader. + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + parcel.writeBundle(bundle); + parcel.setDataPosition(0); + Bundle out = parcel.readBundle(null); + + // Calling Bundle#size() will trigger Bundle#unparcel(). + out.size(); + } catch (BadParcelableException e) { + Log.d(TAG, "Custom parcelable in bundle.", e); + return true; + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + return false; + } + + boolean isClosed() { + synchronized (mLock) { + return mClosed; + } + } + + SessionCallback getCallback() { + return mCallback; + } + + void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) { + synchronized (mLock) { + if (mForegroundServiceEventCallback == callback) { + return; + } + if (mForegroundServiceEventCallback != null && callback != null) { + throw new IllegalStateException("A session cannot be added to multiple services"); + } + mForegroundServiceEventCallback = callback; + } + } + + // Called by Session2Link.onConnect and MediaSession2Service.MediaSession2ServiceStub.connect + void onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq, + Bundle connectionRequest) { + if (callingPid == 0) { + // The pid here is from Binder.getCallingPid(), which can be 0 for an oneway call from + // the remote process. If it's the case, use PID from the connectionRequest. + callingPid = connectionRequest.getInt(KEY_PID); + } + String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME); + + RemoteUserInfo remoteUserInfo = new RemoteUserInfo(callingPkg, callingPid, callingUid); + + Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS); + if (connectionHints == null) { + Log.w(TAG, "connectionHints shouldn't be null."); + connectionHints = Bundle.EMPTY; + } else if (hasCustomParcelable(connectionHints)) { + Log.w(TAG, "connectionHints contain custom parcelable. Ignoring."); + connectionHints = Bundle.EMPTY; + } + + final ControllerInfo controllerInfo = new ControllerInfo( + remoteUserInfo, + mSessionManager.isTrustedForMediaControl(remoteUserInfo), + controller, + connectionHints); + mCallbackExecutor.execute(() -> { + boolean connected = false; + try { + if (isClosed()) { + return; + } + controllerInfo.mAllowedCommands = + mCallback.onConnect(MediaSession2.this, controllerInfo); + // Don't reject connection for the request from trusted app. + // Otherwise server will fail to retrieve session's information to dispatch + // media keys to. + if (controllerInfo.mAllowedCommands == null && !controllerInfo.isTrusted()) { + return; + } + if (controllerInfo.mAllowedCommands == null) { + // For trusted apps, send non-null allowed commands to keep + // connection. + controllerInfo.mAllowedCommands = + new Session2CommandGroup.Builder().build(); + } + if (DEBUG) { + Log.d(TAG, "Accepting connection: " + controllerInfo); + } + // If connection is accepted, notify the current state to the controller. + // It's needed because we cannot call synchronous calls between + // session/controller. + Bundle connectionResult = new Bundle(); + connectionResult.putParcelable(KEY_SESSION2LINK, mSessionStub); + connectionResult.putParcelable(KEY_ALLOWED_COMMANDS, + controllerInfo.mAllowedCommands); + connectionResult.putBoolean(KEY_PLAYBACK_ACTIVE, isPlaybackActive()); + connectionResult.putBundle(KEY_TOKEN_EXTRAS, mSessionToken.getExtras()); + + // Double check if session is still there, because close() can be called in + // another thread. + if (isClosed()) { + return; + } + controllerInfo.notifyConnected(connectionResult); + synchronized (mLock) { + if (mConnectedControllers.containsKey(controller)) { + Log.w(TAG, "Controller " + controllerInfo + " has sent connection" + + " request multiple times"); + } + mConnectedControllers.put(controller, controllerInfo); + } + mCallback.onPostConnect(MediaSession2.this, controllerInfo); + connected = true; + } finally { + if (!connected || isClosed()) { + if (DEBUG) { + Log.d(TAG, "Rejecting connection or notifying that session is closed" + + ", controllerInfo=" + controllerInfo); + } + synchronized (mLock) { + mConnectedControllers.remove(controller); + } + controllerInfo.notifyDisconnected(); + } + } + }); + } + + // Called by Session2Link.onDisconnect + void onDisconnect(@NonNull final Controller2Link controller, int seq) { + final ControllerInfo controllerInfo; + synchronized (mLock) { + controllerInfo = mConnectedControllers.remove(controller); + } + if (controllerInfo == null) { + return; + } + mCallbackExecutor.execute(() -> { + mCallback.onDisconnected(MediaSession2.this, controllerInfo); + }); + } + + // Called by Session2Link.onSessionCommand + void onSessionCommand(@NonNull final Controller2Link controller, final int seq, + final Session2Command command, final Bundle args, + @Nullable ResultReceiver resultReceiver) { + if (controller == null) { + return; + } + final ControllerInfo controllerInfo; + synchronized (mLock) { + controllerInfo = mConnectedControllers.get(controller); + } + if (controllerInfo == null) { + return; + } + + // TODO: check allowed commands. + synchronized (mLock) { + controllerInfo.addRequestedCommandSeqNumber(seq); + } + mCallbackExecutor.execute(() -> { + if (!controllerInfo.removeRequestedCommandSeqNumber(seq)) { + resultReceiver.send(RESULT_INFO_SKIPPED, null); + return; + } + Session2Command.Result result = mCallback.onSessionCommand( + MediaSession2.this, controllerInfo, command, args); + if (resultReceiver != null) { + if (result == null) { + resultReceiver.send(RESULT_INFO_SKIPPED, null); + } else { + resultReceiver.send(result.getResultCode(), result.getResultData()); + } + } + }); + } + + // Called by Session2Link.onCancelCommand + void onCancelCommand(@NonNull final Controller2Link controller, final int seq) { + final ControllerInfo controllerInfo; + synchronized (mLock) { + controllerInfo = mConnectedControllers.get(controller); + } + if (controllerInfo == null) { + return; + } + controllerInfo.removeRequestedCommandSeqNumber(seq); + } + + /** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Builder for {@link MediaSession2}. + * <p> + * Any incoming event from the {@link MediaController2} will be handled on the callback + * executor. If it's not set, {@link Context#getMainExecutor()} will be used by default. + */ + public static final class Builder { + private Context mContext; + private String mId; + private PendingIntent mSessionActivity; + private Executor mCallbackExecutor; + private SessionCallback mCallback; + private Bundle mExtras; + + /** + * Creates a builder for {@link MediaSession2}. + * + * @param context Context + * @throws IllegalArgumentException if context is {@code null}. + */ + public Builder(@NonNull Context context) { + if (context == null) { + throw new IllegalArgumentException("context shouldn't be null"); + } + mContext = context; + } + + /** + * Set an intent for launching UI for this Session. This can be used as a + * quick link to an ongoing media screen. The intent should be for an + * activity that may be started using {@link Context#startActivity(Intent)}. + * + * @param pi The intent to launch to show UI for this session. + * @return The Builder to allow chaining + */ + @NonNull + public Builder setSessionActivity(@Nullable PendingIntent pi) { + mSessionActivity = pi; + return this; + } + + /** + * Set ID of the session. If it's not set, an empty string will be used to create a session. + * <p> + * Use this if and only if your app supports multiple playback at the same time and also + * wants to provide external apps to have finer controls of them. + * + * @param id id of the session. Must be unique per package. + * @throws IllegalArgumentException if id is {@code null}. + * @return The Builder to allow chaining + */ + @NonNull + public Builder setId(@NonNull String id) { + if (id == null) { + throw new IllegalArgumentException("id shouldn't be null"); + } + mId = id; + return this; + } + + /** + * Set callback for the session and its executor. + * + * @param executor callback executor + * @param callback session callback. + * @return The Builder to allow chaining + */ + @NonNull + public Builder setSessionCallback(@NonNull Executor executor, + @NonNull SessionCallback callback) { + mCallbackExecutor = executor; + mCallback = callback; + return this; + } + + /** + * Set extras for the session token. If null or not set, {@link Session2Token#getExtras()} + * will return an empty {@link Bundle}. An {@link IllegalArgumentException} will be thrown + * if the bundle contains any non-framework Parcelable objects. + * + * @return The Builder to allow chaining + * @see Session2Token#getExtras() + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + if (extras == null) { + throw new NullPointerException("extras shouldn't be null"); + } + if (hasCustomParcelable(extras)) { + throw new IllegalArgumentException( + "extras shouldn't contain any custom parcelables"); + } + mExtras = new Bundle(extras); + return this; + } + + /** + * Build {@link MediaSession2}. + * + * @return a new session + * @throws IllegalStateException if the session with the same id is already exists for the + * package. + */ + @NonNull + public MediaSession2 build() { + if (mCallbackExecutor == null) { + mCallbackExecutor = mContext.getMainExecutor(); + } + if (mCallback == null) { + mCallback = new SessionCallback() {}; + } + if (mId == null) { + mId = ""; + } + if (mExtras == null) { + mExtras = Bundle.EMPTY; + } + MediaSession2 session2 = new MediaSession2(mContext, mId, mSessionActivity, + mCallbackExecutor, mCallback, mExtras); + + // Notify framework about the newly create session after the constructor is finished. + // Otherwise, framework may access the session before the initialization is finished. + try { + MediaSessionManager manager = (MediaSessionManager) mContext.getSystemService( + Context.MEDIA_SESSION_SERVICE); + manager.notifySession2Created(session2.getToken()); + } catch (Exception e) { + session2.close(); + throw e; + } + + return session2; + } + } + + /** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Information of a controller. + */ + public static final class ControllerInfo { + private final RemoteUserInfo mRemoteUserInfo; + private final boolean mIsTrusted; + private final Controller2Link mControllerBinder; + private final Bundle mConnectionHints; + private final Object mLock = new Object(); + //@GuardedBy("mLock") + private int mNextSeqNumber; + //@GuardedBy("mLock") + private ArrayMap<ResultReceiver, Integer> mPendingCommands; + //@GuardedBy("mLock") + private ArraySet<Integer> mRequestedCommandSeqNumbers; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + Session2CommandGroup mAllowedCommands; + + /** + * @param remoteUserInfo remote user info + * @param trusted {@code true} if trusted, {@code false} otherwise + * @param controllerBinder Controller2Link for the connected controller. + * @param connectionHints a session-specific argument sent from the controller for the + * connection. The contents of this bundle may affect the + * connection result. + */ + ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted, + @Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints) { + mRemoteUserInfo = remoteUserInfo; + mIsTrusted = trusted; + mControllerBinder = controllerBinder; + mConnectionHints = connectionHints; + mPendingCommands = new ArrayMap<>(); + mRequestedCommandSeqNumbers = new ArraySet<>(); + } + + /** + * @return remote user info of the controller. + */ + @NonNull + public RemoteUserInfo getRemoteUserInfo() { + return mRemoteUserInfo; + } + + /** + * @return package name of the controller. + */ + @NonNull + public String getPackageName() { + return mRemoteUserInfo.getPackageName(); + } + + /** + * @return uid of the controller. Can be a negative value if the uid cannot be obtained. + */ + public int getUid() { + return mRemoteUserInfo.getUid(); + } + + /** + * @return connection hints sent from controller. + */ + @NonNull + public Bundle getConnectionHints() { + return new Bundle(mConnectionHints); + } + + /** + * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or + * has a enabled notification listener so can be trusted to accept connection and incoming + * command request. + * + * @return {@code true} if the controller is trusted. + * @hide + */ + public boolean isTrusted() { + return mIsTrusted; + } + + @Override + public int hashCode() { + return Objects.hash(mControllerBinder, mRemoteUserInfo); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof ControllerInfo)) return false; + if (this == obj) return true; + + ControllerInfo other = (ControllerInfo) obj; + if (mControllerBinder != null || other.mControllerBinder != null) { + return Objects.equals(mControllerBinder, other.mControllerBinder); + } + return mRemoteUserInfo.equals(other.mRemoteUserInfo); + } + + @Override + @NonNull + public String toString() { + return "ControllerInfo {pkg=" + mRemoteUserInfo.getPackageName() + ", uid=" + + mRemoteUserInfo.getUid() + ", allowedCommands=" + mAllowedCommands + "})"; + } + + void notifyConnected(Bundle connectionResult) { + if (mControllerBinder == null) return; + + try { + mControllerBinder.notifyConnected(getNextSeqNumber(), connectionResult); + } catch (RuntimeException e) { + // Controller may be died prematurely. + } + } + + void notifyDisconnected() { + if (mControllerBinder == null) return; + + try { + mControllerBinder.notifyDisconnected(getNextSeqNumber()); + } catch (RuntimeException e) { + // Controller may be died prematurely. + } + } + + void notifyPlaybackActiveChanged(boolean playbackActive) { + if (mControllerBinder == null) return; + + try { + mControllerBinder.notifyPlaybackActiveChanged(getNextSeqNumber(), playbackActive); + } catch (RuntimeException e) { + // Controller may be died prematurely. + } + } + + void sendSessionCommand(Session2Command command, Bundle args, + ResultReceiver resultReceiver) { + if (mControllerBinder == null) return; + + try { + int seq = getNextSeqNumber(); + synchronized (mLock) { + mPendingCommands.put(resultReceiver, seq); + } + mControllerBinder.sendSessionCommand(seq, command, args, resultReceiver); + } catch (RuntimeException e) { + // Controller may be died prematurely. + synchronized (mLock) { + mPendingCommands.remove(resultReceiver); + } + resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null); + } + } + + void cancelSessionCommand(@NonNull Object token) { + if (mControllerBinder == null) return; + Integer seq; + synchronized (mLock) { + seq = mPendingCommands.remove(token); + } + if (seq != null) { + mControllerBinder.cancelSessionCommand(seq); + } + } + + void receiveCommandResult(ResultReceiver resultReceiver) { + synchronized (mLock) { + mPendingCommands.remove(resultReceiver); + } + } + + void addRequestedCommandSeqNumber(int seq) { + synchronized (mLock) { + mRequestedCommandSeqNumbers.add(seq); + } + } + + boolean removeRequestedCommandSeqNumber(int seq) { + synchronized (mLock) { + return mRequestedCommandSeqNumbers.remove(seq); + } + } + + private int getNextSeqNumber() { + synchronized (mLock) { + return mNextSeqNumber++; + } + } + } + + /** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Callback to be called for all incoming commands from {@link MediaController2}s. + */ + public abstract static class SessionCallback { + /** + * Called when a controller is created for this session. Return allowed commands for + * controller. By default it returns {@code null}. + * <p> + * You can reject the connection by returning {@code null}. In that case, controller + * receives {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)} + * and cannot be used. + * <p> + * The controller hasn't connected yet in this method, so calls to the controller + * (e.g. {@link #sendSessionCommand}) would be ignored. Override {@link #onPostConnect} for + * the custom initialization for the controller instead. + * + * @param session the session for this event + * @param controller controller information. + * @return allowed commands. Can be {@code null} to reject connection. + */ + @Nullable + public Session2CommandGroup onConnect(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller) { + return null; + } + + /** + * Called immediately after a controller is connected. This is a convenient method to add + * custom initialization between the session and a controller. + * <p> + * Note that calls to the controller (e.g. {@link #sendSessionCommand}) work here but don't + * work in {@link #onConnect} because the controller hasn't connected yet in + * {@link #onConnect}. + * + * @param session the session for this event + * @param controller controller information. + */ + public void onPostConnect(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller) { + } + + /** + * Called when a controller is disconnected + * + * @param session the session for this event + * @param controller controller information + */ + public void onDisconnected(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller) {} + + /** + * Called when a controller sent a session command. + * + * @param session the session for this event + * @param controller controller information + * @param command the session command + * @param args optional arguments + * @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED + * will be sent to the session. + */ + @Nullable + public Session2Command.Result onSessionCommand(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller, @NonNull Session2Command command, + @Nullable Bundle args) { + return null; + } + + /** + * Called when the command sent to the controller is finished. + * + * @param session the session for this event + * @param controller controller information + * @param token the token got from {@link MediaSession2#sendSessionCommand} + * @param command the session command + * @param result the result of the session command + */ + public void onCommandResult(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller, @NonNull Object token, + @NonNull Session2Command command, @NonNull Session2Command.Result result) {} + } + + abstract static class ForegroundServiceEventCallback { + public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {} + public void onSessionClosed(MediaSession2 session) {} + } +} diff --git a/apex/media/framework/java/android/media/MediaSession2Service.java b/apex/media/framework/java/android/media/MediaSession2Service.java new file mode 100644 index 000000000000..f6fd509fd245 --- /dev/null +++ b/apex/media/framework/java/android/media/MediaSession2Service.java @@ -0,0 +1,452 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import static android.media.MediaConstants.KEY_CONNECTION_HINTS; +import static android.media.MediaConstants.KEY_PACKAGE_NAME; +import static android.media.MediaConstants.KEY_PID; + +import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.media.MediaSession2.ControllerInfo; +import android.media.session.MediaSessionManager; +import android.media.session.MediaSessionManager.RemoteUserInfo; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.util.ArrayMap; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Service containing {@link MediaSession2}. + */ +public abstract class MediaSession2Service extends Service { + /** + * The {@link Intent} that must be declared as handled by the service. + */ + public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service"; + + private static final String TAG = "MediaSession2Service"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final MediaSession2.ForegroundServiceEventCallback mForegroundServiceEventCallback = + new MediaSession2.ForegroundServiceEventCallback() { + @Override + public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) { + MediaSession2Service.this.onPlaybackActiveChanged(session, playbackActive); + } + + @Override + public void onSessionClosed(MediaSession2 session) { + removeSession(session); + } + }; + + private final Object mLock = new Object(); + //@GuardedBy("mLock") + private NotificationManager mNotificationManager; + //@GuardedBy("mLock") + private MediaSessionManager mMediaSessionManager; + //@GuardedBy("mLock") + private Intent mStartSelfIntent; + //@GuardedBy("mLock") + private Map<String, MediaSession2> mSessions = new ArrayMap<>(); + //@GuardedBy("mLock") + private Map<MediaSession2, MediaNotification> mNotifications = new ArrayMap<>(); + //@GuardedBy("mLock") + private MediaSession2ServiceStub mStub; + + /** + * Called by the system when the service is first created. Do not call this method directly. + * <p> + * Override this method if you need your own initialization. Derived classes MUST call through + * to the super class's implementation of this method. + */ + @CallSuper + @Override + public void onCreate() { + super.onCreate(); + synchronized (mLock) { + mStub = new MediaSession2ServiceStub(this); + mStartSelfIntent = new Intent(this, this.getClass()); + mNotificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + mMediaSessionManager = + (MediaSessionManager) getSystemService(Context.MEDIA_SESSION_SERVICE); + } + } + + @CallSuper + @Override + @Nullable + public IBinder onBind(@NonNull Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction())) { + synchronized (mLock) { + return mStub; + } + } + return null; + } + + /** + * Called by the system to notify that it is no longer used and is being removed. Do not call + * this method directly. + * <p> + * Override this method if you need your own clean up. Derived classes MUST call through + * to the super class's implementation of this method. + */ + @CallSuper + @Override + public void onDestroy() { + super.onDestroy(); + synchronized (mLock) { + List<MediaSession2> sessions = getSessions(); + for (MediaSession2 session : sessions) { + removeSession(session); + } + mSessions.clear(); + mNotifications.clear(); + } + mStub.close(); + } + + /** + * Called when a {@link MediaController2} is created with the this service's + * {@link Session2Token}. Return the session for telling the controller which session to + * connect. Return {@code null} to reject the connection from this controller. + * <p> + * Session returned here will be added to this service automatically. You don't need to call + * {@link #addSession(MediaSession2)} for that. + * <p> + * This method is always called on the main thread. + * + * @param controllerInfo information of the controller which is trying to connect. + * @return a {@link MediaSession2} instance for the controller to connect to, or {@code null} + * to reject connection + * @see MediaSession2.Builder + * @see #getSessions() + */ + @Nullable + public abstract MediaSession2 onGetSession(@NonNull ControllerInfo controllerInfo); + + /** + * Called when notification UI needs update. Override this method to show or cancel your own + * notification UI. + * <p> + * This would be called on {@link MediaSession2}'s callback executor when playback state is + * changed. + * <p> + * With the notification returned here, the service becomes foreground service when the playback + * is started. Apps must request the permission + * {@link android.Manifest.permission#FOREGROUND_SERVICE} in order to use this API. It becomes + * background service after the playback is stopped. + * + * @param session a session that needs notification update. + * @return a {@link MediaNotification}. Can be {@code null}. + */ + @Nullable + public abstract MediaNotification onUpdateNotification(@NonNull MediaSession2 session); + + /** + * Adds a session to this service. + * <p> + * Added session will be removed automatically when it's closed, or removed when + * {@link #removeSession} is called. + * + * @param session a session to be added. + * @see #removeSession(MediaSession2) + */ + public final void addSession(@NonNull MediaSession2 session) { + if (session == null) { + throw new IllegalArgumentException("session shouldn't be null"); + } + if (session.isClosed()) { + throw new IllegalArgumentException("session is already closed"); + } + synchronized (mLock) { + MediaSession2 previousSession = mSessions.get(session.getId()); + if (previousSession != null) { + if (previousSession != session) { + Log.w(TAG, "Session ID should be unique, ID=" + session.getId() + + ", previous=" + previousSession + ", session=" + session); + } + return; + } + mSessions.put(session.getId(), session); + session.setForegroundServiceEventCallback(mForegroundServiceEventCallback); + } + } + + /** + * Removes a session from this service. + * + * @param session a session to be removed. + * @see #addSession(MediaSession2) + */ + public final void removeSession(@NonNull MediaSession2 session) { + if (session == null) { + throw new IllegalArgumentException("session shouldn't be null"); + } + MediaNotification notification; + synchronized (mLock) { + if (mSessions.get(session.getId()) != session) { + // Session isn't added or removed already. + return; + } + mSessions.remove(session.getId()); + notification = mNotifications.remove(session); + } + session.setForegroundServiceEventCallback(null); + if (notification != null) { + mNotificationManager.cancel(notification.getNotificationId()); + } + if (getSessions().isEmpty()) { + stopForeground(false); + } + } + + /** + * Gets the list of {@link MediaSession2}s that you've added to this service. + * + * @return sessions + */ + public final @NonNull List<MediaSession2> getSessions() { + List<MediaSession2> list = new ArrayList<>(); + synchronized (mLock) { + list.addAll(mSessions.values()); + } + return list; + } + + /** + * Returns the {@link MediaSessionManager}. + */ + @NonNull + MediaSessionManager getMediaSessionManager() { + synchronized (mLock) { + return mMediaSessionManager; + } + } + + /** + * Called by registered {@link MediaSession2.ForegroundServiceEventCallback} + * + * @param session session with change + * @param playbackActive {@code true} if playback is active. + */ + void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) { + MediaNotification mediaNotification = onUpdateNotification(session); + if (mediaNotification == null) { + // The service implementation doesn't want to use the automatic start/stopForeground + // feature. + return; + } + synchronized (mLock) { + mNotifications.put(session, mediaNotification); + } + int id = mediaNotification.getNotificationId(); + Notification notification = mediaNotification.getNotification(); + if (!playbackActive) { + mNotificationManager.notify(id, notification); + return; + } + // playbackActive == true + startForegroundService(mStartSelfIntent); + startForeground(id, notification); + } + + /** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Returned by {@link #onUpdateNotification(MediaSession2)} for making session service + * foreground service to keep playback running in the background. It's highly recommended to + * show media style notification here. + */ + public static class MediaNotification { + private final int mNotificationId; + private final Notification mNotification; + + /** + * Default constructor + * + * @param notificationId notification id to be used for + * {@link NotificationManager#notify(int, Notification)}. + * @param notification a notification to make session service run in the foreground. Media + * style notification is recommended here. + */ + public MediaNotification(int notificationId, @NonNull Notification notification) { + if (notification == null) { + throw new IllegalArgumentException("notification shouldn't be null"); + } + mNotificationId = notificationId; + mNotification = notification; + } + + /** + * Gets the id of the notification. + * + * @return the notification id + */ + public int getNotificationId() { + return mNotificationId; + } + + /** + * Gets the notification. + * + * @return the notification + */ + @NonNull + public Notification getNotification() { + return mNotification; + } + } + + private static final class MediaSession2ServiceStub extends IMediaSession2Service.Stub + implements AutoCloseable { + final WeakReference<MediaSession2Service> mService; + final Handler mHandler; + + MediaSession2ServiceStub(MediaSession2Service service) { + mService = new WeakReference<>(service); + mHandler = new Handler(service.getMainLooper()); + } + + @Override + public void connect(Controller2Link caller, int seq, Bundle connectionRequest) { + if (mService.get() == null) { + if (DEBUG) { + Log.d(TAG, "Service is already destroyed"); + } + return; + } + if (caller == null || connectionRequest == null) { + if (DEBUG) { + Log.d(TAG, "Ignoring calls with illegal arguments, caller=" + caller + + ", connectionRequest=" + connectionRequest); + } + return; + } + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + mHandler.post(() -> { + boolean shouldNotifyDisconnected = true; + try { + final MediaSession2Service service = mService.get(); + if (service == null) { + if (DEBUG) { + Log.d(TAG, "Service isn't available"); + } + return; + } + + String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME); + // The Binder.getCallingPid() can be 0 for an oneway call from the + // remote process. If it's the case, use PID from the connectionRequest. + RemoteUserInfo remoteUserInfo = new RemoteUserInfo( + callingPkg, + pid == 0 ? connectionRequest.getInt(KEY_PID) : pid, + uid); + + Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS); + if (connectionHints == null) { + Log.w(TAG, "connectionHints shouldn't be null."); + connectionHints = Bundle.EMPTY; + } else if (MediaSession2.hasCustomParcelable(connectionHints)) { + Log.w(TAG, "connectionHints contain custom parcelable. Ignoring."); + connectionHints = Bundle.EMPTY; + } + + final ControllerInfo controllerInfo = new ControllerInfo( + remoteUserInfo, + service.getMediaSessionManager() + .isTrustedForMediaControl(remoteUserInfo), + caller, + connectionHints); + + if (DEBUG) { + Log.d(TAG, "Handling incoming connection request from the" + + " controller=" + controllerInfo); + } + + final MediaSession2 session; + session = service.onGetSession(controllerInfo); + + if (session == null) { + if (DEBUG) { + Log.d(TAG, "Rejecting incoming connection request from the" + + " controller=" + controllerInfo); + } + // Note: Trusted controllers also can be rejected according to the + // service implementation. + return; + } + service.addSession(session); + shouldNotifyDisconnected = false; + session.onConnect(caller, pid, uid, seq, connectionRequest); + } catch (Exception e) { + // Don't propagate exception in service to the controller. + Log.w(TAG, "Failed to add a session to session service", e); + } finally { + // Trick to call onDisconnected() in one place. + if (shouldNotifyDisconnected) { + if (DEBUG) { + Log.d(TAG, "Notifying the controller of its disconnection"); + } + try { + caller.notifyDisconnected(0); + } catch (RuntimeException e) { + // Controller may be died prematurely. + // Not an issue because we'll ignore it anyway. + } + } + } + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void close() { + mHandler.removeCallbacksAndMessages(null); + mService.clear(); + } + } +} diff --git a/apex/media/framework/java/android/media/ProxyDataSourceCallback.java b/apex/media/framework/java/android/media/ProxyDataSourceCallback.java new file mode 100644 index 000000000000..14d3ce87f03d --- /dev/null +++ b/apex/media/framework/java/android/media/ProxyDataSourceCallback.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.IOException; + +/** + * A DataSourceCallback that is backed by a ParcelFileDescriptor. + */ +class ProxyDataSourceCallback extends DataSourceCallback { + private static final String TAG = "TestDataSourceCallback"; + + ParcelFileDescriptor mPFD; + FileDescriptor mFD; + + ProxyDataSourceCallback(ParcelFileDescriptor pfd) throws IOException { + mPFD = pfd.dup(); + mFD = mPFD.getFileDescriptor(); + } + + @Override + public synchronized int readAt(long position, byte[] buffer, int offset, int size) + throws IOException { + try { + Os.lseek(mFD, position, OsConstants.SEEK_SET); + int ret = Os.read(mFD, buffer, offset, size); + return (ret == 0) ? END_OF_STREAM : ret; + } catch (ErrnoException e) { + throw new IOException(e); + } + } + + @Override + public synchronized long getSize() throws IOException { + return mPFD.getStatSize(); + } + + @Override + public synchronized void close() { + try { + mPFD.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close the PFD.", e); + } + } +} + diff --git a/apex/media/framework/java/android/media/RoutingDelegate.java b/apex/media/framework/java/android/media/RoutingDelegate.java new file mode 100644 index 000000000000..23598130f391 --- /dev/null +++ b/apex/media/framework/java/android/media/RoutingDelegate.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.os.Handler; + +class RoutingDelegate implements AudioRouting.OnRoutingChangedListener { + private AudioRouting mAudioRouting; + private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener; + private Handler mHandler; + + RoutingDelegate(final AudioRouting audioRouting, + final AudioRouting.OnRoutingChangedListener listener, + Handler handler) { + mAudioRouting = audioRouting; + mOnRoutingChangedListener = listener; + mHandler = handler; + } + + public AudioRouting.OnRoutingChangedListener getListener() { + return mOnRoutingChangedListener; + } + + public Handler getHandler() { + return mHandler; + } + + @Override + public void onRoutingChanged(AudioRouting router) { + if (mOnRoutingChangedListener != null) { + mOnRoutingChangedListener.onRoutingChanged(mAudioRouting); + } + } +} diff --git a/apex/media/framework/java/android/media/Session2Command.java b/apex/media/framework/java/android/media/Session2Command.java new file mode 100644 index 000000000000..26f4568fa7e5 --- /dev/null +++ b/apex/media/framework/java/android/media/Session2Command.java @@ -0,0 +1,218 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.Objects; + +/** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Define a command that a {@link MediaController2} can send to a {@link MediaSession2}. + * <p> + * If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command. + * If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and + * {@link #getCustomAction()} shouldn't be {@code null}. + * <p> + * Refer to the + * <a href="{@docRoot}reference/androidx/media2/SessionCommand2.html">AndroidX SessionCommand</a> + * class for the list of valid commands. + */ +public final class Session2Command implements Parcelable { + /** + * Command code for the custom command which can be defined by string action in the + * {@link Session2Command}. + */ + public static final int COMMAND_CODE_CUSTOM = 0; + + public static final @android.annotation.NonNull Parcelable.Creator<Session2Command> CREATOR = + new Parcelable.Creator<Session2Command>() { + @Override + public Session2Command createFromParcel(Parcel in) { + return new Session2Command(in); + } + + @Override + public Session2Command[] newArray(int size) { + return new Session2Command[size]; + } + }; + + private final int mCommandCode; + // Nonnull if it's custom command + private final String mCustomAction; + private final Bundle mCustomExtras; + + /** + * Constructor for creating a command predefined in AndroidX media2. + * + * @param commandCode A command code for a command predefined in AndroidX media2. + */ + public Session2Command(int commandCode) { + if (commandCode == COMMAND_CODE_CUSTOM) { + throw new IllegalArgumentException("commandCode shouldn't be COMMAND_CODE_CUSTOM"); + } + mCommandCode = commandCode; + mCustomAction = null; + mCustomExtras = null; + } + + /** + * Constructor for creating a custom command. + * + * @param action The action of this custom command. + * @param extras An extra bundle for this custom command. + */ + public Session2Command(@NonNull String action, @Nullable Bundle extras) { + if (action == null) { + throw new IllegalArgumentException("action shouldn't be null"); + } + mCommandCode = COMMAND_CODE_CUSTOM; + mCustomAction = action; + mCustomExtras = extras; + } + + /** + * Used by parcelable creator. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + Session2Command(Parcel in) { + mCommandCode = in.readInt(); + mCustomAction = in.readString(); + mCustomExtras = in.readBundle(); + } + + /** + * Gets the command code of a predefined command. + * This will return {@link #COMMAND_CODE_CUSTOM} for a custom command. + */ + public int getCommandCode() { + return mCommandCode; + } + + /** + * Gets the action of a custom command. + * This will return {@code null} for a predefined command. + */ + @Nullable + public String getCustomAction() { + return mCustomAction; + } + + /** + * Gets the extra bundle of a custom command. + * This will return {@code null} for a predefined command. + */ + @Nullable + public Bundle getCustomExtras() { + return mCustomExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + if (dest == null) { + throw new IllegalArgumentException("parcel shouldn't be null"); + } + dest.writeInt(mCommandCode); + dest.writeString(mCustomAction); + dest.writeBundle(mCustomExtras); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof Session2Command)) { + return false; + } + Session2Command other = (Session2Command) obj; + return mCommandCode == other.mCommandCode + && TextUtils.equals(mCustomAction, other.mCustomAction); + } + + @Override + public int hashCode() { + return Objects.hash(mCustomAction, mCommandCode); + } + + /** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Contains the result of {@link Session2Command}. + */ + public static final class Result { + private final int mResultCode; + private final Bundle mResultData; + + /** + * Result code representing that the command is skipped or canceled. For an example, a seek + * command can be skipped if it is followed by another seek command. + */ + public static final int RESULT_INFO_SKIPPED = 1; + + /** + * Result code representing that the command is successfully completed. + */ + public static final int RESULT_SUCCESS = 0; + + /** + * Result code represents that call is ended with an unknown error. + */ + public static final int RESULT_ERROR_UNKNOWN_ERROR = -1; + + /** + * Constructor of {@link Result}. + * + * @param resultCode result code + * @param resultData result data + */ + public Result(int resultCode, @Nullable Bundle resultData) { + mResultCode = resultCode; + mResultData = resultData; + } + + /** + * Returns the result code. + */ + public int getResultCode() { + return mResultCode; + } + + /** + * Returns the result data. + */ + @Nullable + public Bundle getResultData() { + return mResultData; + } + } +} diff --git a/apex/media/framework/java/android/media/Session2CommandGroup.java b/apex/media/framework/java/android/media/Session2CommandGroup.java new file mode 100644 index 000000000000..13aabfc45ab7 --- /dev/null +++ b/apex/media/framework/java/android/media/Session2CommandGroup.java @@ -0,0 +1,197 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import static android.media.Session2Command.COMMAND_CODE_CUSTOM; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * A set of {@link Session2Command} which represents a command group. + */ +public final class Session2CommandGroup implements Parcelable { + private static final String TAG = "Session2CommandGroup"; + + public static final @android.annotation.NonNull Parcelable.Creator<Session2CommandGroup> + CREATOR = new Parcelable.Creator<Session2CommandGroup>() { + @Override + public Session2CommandGroup createFromParcel(Parcel in) { + return new Session2CommandGroup(in); + } + + @Override + public Session2CommandGroup[] newArray(int size) { + return new Session2CommandGroup[size]; + } + }; + + Set<Session2Command> mCommands = new HashSet<>(); + + /** + * Creates a new Session2CommandGroup with commands copied from another object. + * + * @param commands The collection of commands to copy. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + Session2CommandGroup(@Nullable Collection<Session2Command> commands) { + if (commands != null) { + mCommands.addAll(commands); + } + } + + /** + * Used by parcelable creator. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + Session2CommandGroup(Parcel in) { + Parcelable[] commands = in.readParcelableArray(Session2Command.class.getClassLoader()); + if (commands != null) { + for (Parcelable command : commands) { + mCommands.add((Session2Command) command); + } + } + } + + /** + * Checks whether this command group has a command that matches given {@code command}. + * + * @param command A command to find. Shouldn't be {@code null}. + */ + public boolean hasCommand(@NonNull Session2Command command) { + if (command == null) { + throw new IllegalArgumentException("command shouldn't be null"); + } + return mCommands.contains(command); + } + + /** + * Checks whether this command group has a command that matches given {@code commandCode}. + * + * @param commandCode A command code to find. + * Shouldn't be {@link Session2Command#COMMAND_CODE_CUSTOM}. + */ + public boolean hasCommand(int commandCode) { + if (commandCode == COMMAND_CODE_CUSTOM) { + throw new IllegalArgumentException("Use hasCommand(Command) for custom command"); + } + for (Session2Command command : mCommands) { + if (command.getCommandCode() == commandCode) { + return true; + } + } + return false; + } + + /** + * Gets all commands of this command group. + */ + @NonNull + public Set<Session2Command> getCommands() { + return new HashSet<>(mCommands); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + if (dest == null) { + throw new IllegalArgumentException("parcel shouldn't be null"); + } + dest.writeParcelableArray(mCommands.toArray(new Session2Command[0]), 0); + } + + /** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Builds a {@link Session2CommandGroup} object. + */ + public static final class Builder { + private Set<Session2Command> mCommands; + + public Builder() { + mCommands = new HashSet<>(); + } + + /** + * Creates a new builder for {@link Session2CommandGroup} with commands copied from another + * {@link Session2CommandGroup} object. + * @param commandGroup + */ + public Builder(@NonNull Session2CommandGroup commandGroup) { + if (commandGroup == null) { + throw new IllegalArgumentException("command group shouldn't be null"); + } + mCommands = commandGroup.getCommands(); + } + + /** + * Adds a command to this command group. + * + * @param command A command to add. Shouldn't be {@code null}. + */ + @NonNull + public Builder addCommand(@NonNull Session2Command command) { + if (command == null) { + throw new IllegalArgumentException("command shouldn't be null"); + } + mCommands.add(command); + return this; + } + + /** + * Removes a command from this group which matches given {@code command}. + * + * @param command A command to find. Shouldn't be {@code null}. + */ + @NonNull + public Builder removeCommand(@NonNull Session2Command command) { + if (command == null) { + throw new IllegalArgumentException("command shouldn't be null"); + } + mCommands.remove(command); + return this; + } + + /** + * Builds {@link Session2CommandGroup}. + * + * @return a new {@link Session2CommandGroup}. + */ + @NonNull + public Session2CommandGroup build() { + return new Session2CommandGroup(mCommands); + } + } +} diff --git a/apex/media/framework/java/android/media/Session2Link.java b/apex/media/framework/java/android/media/Session2Link.java new file mode 100644 index 000000000000..6e550e86a9fe --- /dev/null +++ b/apex/media/framework/java/android/media/Session2Link.java @@ -0,0 +1,226 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.annotation.NonNull; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.util.Log; + +import java.util.Objects; + +/** + * Handles incoming commands from {@link MediaController2} to {@link MediaSession2}. + * @hide + */ +// @SystemApi +public final class Session2Link implements Parcelable { + private static final String TAG = "Session2Link"; + private static final boolean DEBUG = MediaSession2.DEBUG; + + public static final @android.annotation.NonNull Parcelable.Creator<Session2Link> CREATOR = + new Parcelable.Creator<Session2Link>() { + @Override + public Session2Link createFromParcel(Parcel in) { + return new Session2Link(in); + } + + @Override + public Session2Link[] newArray(int size) { + return new Session2Link[size]; + } + }; + + private final MediaSession2 mSession; + private final IMediaSession2 mISession; + + public Session2Link(MediaSession2 session) { + mSession = session; + mISession = new Session2Stub(); + } + + Session2Link(Parcel in) { + mSession = null; + mISession = IMediaSession2.Stub.asInterface(in.readStrongBinder()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mISession.asBinder()); + } + + @Override + public int hashCode() { + return mISession.asBinder().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Session2Link)) { + return false; + } + Session2Link other = (Session2Link) obj; + return Objects.equals(mISession.asBinder(), other.mISession.asBinder()); + } + + /** Link to death with mISession */ + public void linkToDeath(@NonNull IBinder.DeathRecipient recipient, int flags) { + if (mISession != null) { + try { + mISession.asBinder().linkToDeath(recipient, flags); + } catch (RemoteException e) { + if (DEBUG) { + Log.d(TAG, "Session died too early.", e); + } + } + } + } + + /** Unlink to death with mISession */ + public boolean unlinkToDeath(@NonNull IBinder.DeathRecipient recipient, int flags) { + if (mISession != null) { + return mISession.asBinder().unlinkToDeath(recipient, flags); + } + return true; + } + + /** Interface method for IMediaSession2.connect */ + public void connect(final Controller2Link caller, int seq, Bundle connectionRequest) { + try { + mISession.connect(caller, seq, connectionRequest); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** Interface method for IMediaSession2.disconnect */ + public void disconnect(final Controller2Link caller, int seq) { + try { + mISession.disconnect(caller, seq); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** Interface method for IMediaSession2.sendSessionCommand */ + public void sendSessionCommand(final Controller2Link caller, final int seq, + final Session2Command command, final Bundle args, ResultReceiver resultReceiver) { + try { + mISession.sendSessionCommand(caller, seq, command, args, resultReceiver); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** Interface method for IMediaSession2.sendSessionCommand */ + public void cancelSessionCommand(final Controller2Link caller, final int seq) { + try { + mISession.cancelSessionCommand(caller, seq); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** Stub implementation for IMediaSession2.connect */ + public void onConnect(final Controller2Link caller, int pid, int uid, int seq, + Bundle connectionRequest) { + mSession.onConnect(caller, pid, uid, seq, connectionRequest); + } + + /** Stub implementation for IMediaSession2.disconnect */ + public void onDisconnect(final Controller2Link caller, int seq) { + mSession.onDisconnect(caller, seq); + } + + /** Stub implementation for IMediaSession2.sendSessionCommand */ + public void onSessionCommand(final Controller2Link caller, final int seq, + final Session2Command command, final Bundle args, ResultReceiver resultReceiver) { + mSession.onSessionCommand(caller, seq, command, args, resultReceiver); + } + + /** Stub implementation for IMediaSession2.cancelSessionCommand */ + public void onCancelCommand(final Controller2Link caller, final int seq) { + mSession.onCancelCommand(caller, seq); + } + + private class Session2Stub extends IMediaSession2.Stub { + @Override + public void connect(final Controller2Link caller, int seq, Bundle connectionRequest) { + if (caller == null || connectionRequest == null) { + return; + } + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + Session2Link.this.onConnect(caller, pid, uid, seq, connectionRequest); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void disconnect(final Controller2Link caller, int seq) { + if (caller == null) { + return; + } + final long token = Binder.clearCallingIdentity(); + try { + Session2Link.this.onDisconnect(caller, seq); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void sendSessionCommand(final Controller2Link caller, final int seq, + final Session2Command command, final Bundle args, ResultReceiver resultReceiver) { + if (caller == null) { + return; + } + final long token = Binder.clearCallingIdentity(); + try { + Session2Link.this.onSessionCommand(caller, seq, command, args, resultReceiver); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void cancelSessionCommand(final Controller2Link caller, final int seq) { + if (caller == null) { + return; + } + final long token = Binder.clearCallingIdentity(); + try { + Session2Link.this.onCancelCommand(caller, seq); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } +} diff --git a/apex/media/framework/java/android/media/Session2Token.java b/apex/media/framework/java/android/media/Session2Token.java new file mode 100644 index 000000000000..aae2e1bcb6df --- /dev/null +++ b/apex/media/framework/java/android/media/Session2Token.java @@ -0,0 +1,272 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Objects; + +/** + * This API is not generally intended for third party application developers. + * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> + * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session + * Library</a> for consistent behavior across all devices. + * <p> + * Represents an ongoing {@link MediaSession2} or a {@link MediaSession2Service}. + * If it's representing a session service, it may not be ongoing. + * <p> + * This may be passed to apps by the session owner to allow them to create a + * {@link MediaController2} to communicate with the session. + * <p> + * It can be also obtained by {@link android.media.session.MediaSessionManager}. + */ +public final class Session2Token implements Parcelable { + private static final String TAG = "Session2Token"; + + public static final @android.annotation.NonNull Creator<Session2Token> CREATOR = + new Creator<Session2Token>() { + @Override + public Session2Token createFromParcel(Parcel p) { + return new Session2Token(p); + } + + @Override + public Session2Token[] newArray(int size) { + return new Session2Token[size]; + } + }; + + /** + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = "TYPE_", value = {TYPE_SESSION, TYPE_SESSION_SERVICE}) + public @interface TokenType { + } + + /** + * Type for {@link MediaSession2}. + */ + public static final int TYPE_SESSION = 0; + + /** + * Type for {@link MediaSession2Service}. + */ + public static final int TYPE_SESSION_SERVICE = 1; + + private final int mUid; + @TokenType + private final int mType; + private final String mPackageName; + private final String mServiceName; + private final Session2Link mSessionLink; + private final ComponentName mComponentName; + private final Bundle mExtras; + + /** + * Constructor for the token with type {@link #TYPE_SESSION_SERVICE}. + * + * @param context The context. + * @param serviceComponent The component name of the service. + */ + public Session2Token(@NonNull Context context, @NonNull ComponentName serviceComponent) { + if (context == null) { + throw new IllegalArgumentException("context shouldn't be null"); + } + if (serviceComponent == null) { + throw new IllegalArgumentException("serviceComponent shouldn't be null"); + } + + final PackageManager manager = context.getPackageManager(); + final int uid = getUid(manager, serviceComponent.getPackageName()); + + if (!isInterfaceDeclared(manager, MediaSession2Service.SERVICE_INTERFACE, + serviceComponent)) { + Log.w(TAG, serviceComponent + " doesn't implement MediaSession2Service."); + } + mComponentName = serviceComponent; + mPackageName = serviceComponent.getPackageName(); + mServiceName = serviceComponent.getClassName(); + mUid = uid; + mType = TYPE_SESSION_SERVICE; + mSessionLink = null; + mExtras = Bundle.EMPTY; + } + + Session2Token(int uid, int type, String packageName, Session2Link sessionLink, + @NonNull Bundle tokenExtras) { + mUid = uid; + mType = type; + mPackageName = packageName; + mServiceName = null; + mComponentName = null; + mSessionLink = sessionLink; + mExtras = tokenExtras; + } + + Session2Token(Parcel in) { + mUid = in.readInt(); + mType = in.readInt(); + mPackageName = in.readString(); + mServiceName = in.readString(); + mSessionLink = in.readParcelable(null); + mComponentName = ComponentName.unflattenFromString(in.readString()); + + Bundle extras = in.readBundle(); + if (extras == null) { + Log.w(TAG, "extras shouldn't be null."); + extras = Bundle.EMPTY; + } else if (MediaSession2.hasCustomParcelable(extras)) { + Log.w(TAG, "extras contain custom parcelable. Ignoring."); + extras = Bundle.EMPTY; + } + mExtras = extras; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mUid); + dest.writeInt(mType); + dest.writeString(mPackageName); + dest.writeString(mServiceName); + dest.writeParcelable(mSessionLink, flags); + dest.writeString(mComponentName == null ? "" : mComponentName.flattenToString()); + dest.writeBundle(mExtras); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public int hashCode() { + return Objects.hash(mType, mUid, mPackageName, mServiceName, mSessionLink); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Session2Token)) { + return false; + } + Session2Token other = (Session2Token) obj; + return mUid == other.mUid + && TextUtils.equals(mPackageName, other.mPackageName) + && TextUtils.equals(mServiceName, other.mServiceName) + && mType == other.mType + && Objects.equals(mSessionLink, other.mSessionLink); + } + + @Override + public String toString() { + return "Session2Token {pkg=" + mPackageName + " type=" + mType + + " service=" + mServiceName + " Session2Link=" + mSessionLink + "}"; + } + + /** + * @return uid of the session + */ + public int getUid() { + return mUid; + } + + /** + * @return package name of the session + */ + @NonNull + public String getPackageName() { + return mPackageName; + } + + /** + * @return service name of the session. Can be {@code null} for {@link #TYPE_SESSION}. + */ + @Nullable + public String getServiceName() { + return mServiceName; + } + + /** + * @return type of the token + * @see #TYPE_SESSION + * @see #TYPE_SESSION_SERVICE + */ + public @TokenType int getType() { + return mType; + } + + /** + * @return extras of the token + * @see MediaSession2.Builder#setExtras(Bundle) + */ + @NonNull + public Bundle getExtras() { + return new Bundle(mExtras); + } + + Session2Link getSessionLink() { + return mSessionLink; + } + + private static boolean isInterfaceDeclared(PackageManager manager, String serviceInterface, + ComponentName serviceComponent) { + Intent serviceIntent = new Intent(serviceInterface); + // Use queryIntentServices to find services with MediaSession2Service.SERVICE_INTERFACE. + // We cannot use resolveService with intent specified class name, because resolveService + // ignores actions if Intent.setClassName() is specified. + serviceIntent.setPackage(serviceComponent.getPackageName()); + + List<ResolveInfo> list = manager.queryIntentServices( + serviceIntent, PackageManager.GET_META_DATA); + if (list != null) { + for (int i = 0; i < list.size(); i++) { + ResolveInfo resolveInfo = list.get(i); + if (resolveInfo == null || resolveInfo.serviceInfo == null) { + continue; + } + if (TextUtils.equals( + resolveInfo.serviceInfo.name, serviceComponent.getClassName())) { + return true; + } + } + } + return false; + } + + private static int getUid(PackageManager manager, String packageName) { + try { + return manager.getApplicationInfo(packageName, 0).uid; + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalArgumentException("Cannot find package " + packageName); + } + } +} diff --git a/apex/media/framework/updatable-media-proguard.flags b/apex/media/framework/updatable-media-proguard.flags new file mode 100644 index 000000000000..4e7d8422bf44 --- /dev/null +++ b/apex/media/framework/updatable-media-proguard.flags @@ -0,0 +1,2 @@ +# Keep all symbols in android.media. +-keep class android.media.* {*;} diff --git a/apex/permission/Android.bp b/apex/permission/Android.bp new file mode 100644 index 000000000000..71a52bb216ea --- /dev/null +++ b/apex/permission/Android.bp @@ -0,0 +1,43 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +apex { + name: "com.android.permission", + defaults: ["com.android.permission-defaults"], + manifest: "apex_manifest.json", +} + +apex_defaults { + name: "com.android.permission-defaults", + updatable: true, + min_sdk_version: "R", + key: "com.android.permission.key", + certificate: ":com.android.permission.certificate", + java_libs: [ + "framework-permission", + "service-permission", + ], + apps: ["PermissionController"], +} + +apex_key { + name: "com.android.permission.key", + public_key: "com.android.permission.avbpubkey", + private_key: "com.android.permission.pem", +} + +android_app_certificate { + name: "com.android.permission.certificate", + certificate: "com.android.permission", +} diff --git a/apex/permission/OWNERS b/apex/permission/OWNERS new file mode 100644 index 000000000000..957e10a582a0 --- /dev/null +++ b/apex/permission/OWNERS @@ -0,0 +1,6 @@ +svetoslavganov@google.com +moltmann@google.com +eugenesusla@google.com +zhanghai@google.com +evanseverson@google.com +ntmyren@google.com diff --git a/apex/permission/TEST_MAPPING b/apex/permission/TEST_MAPPING new file mode 100644 index 000000000000..6e67ce92a27e --- /dev/null +++ b/apex/permission/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit" : [ + { + "name" : "PermissionApexTests" + } + ] +} diff --git a/apex/permission/apex_manifest.json b/apex/permission/apex_manifest.json new file mode 100644 index 000000000000..7960598affa3 --- /dev/null +++ b/apex/permission/apex_manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.permission", + "version": 300000000 +} diff --git a/apex/permission/com.android.permission.avbpubkey b/apex/permission/com.android.permission.avbpubkey Binary files differnew file mode 100644 index 000000000000..9eaf85259637 --- /dev/null +++ b/apex/permission/com.android.permission.avbpubkey diff --git a/apex/permission/com.android.permission.pem b/apex/permission/com.android.permission.pem new file mode 100644 index 000000000000..3d584be5440d --- /dev/null +++ b/apex/permission/com.android.permission.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEA6snt4eqoz85xiL9Sf6w1S1b9FgSHK05zYTh2JYPvQKQ3yeZp +E6avJ6FN6XcbmkDzSd658BvUGDBSPhOlzuUO4BsoKBuLMxP6TxIQXFKidzDqY0vQ +4qkS++bdIhUjwBP3OSZ3Czu0BiihK8GC75Abr//EyCyObGIGGfHEGANiOgrpP4X5 ++OmLzQLCjk4iE1kg+U6cRSRI/XLaoWC0TvIIuzxznrQ6r5GmzgTOwyBWyIB+bj73 +bmsweHTU+w9Y7kGOx4hO3XCLIhoBWEw0EbuW9nZmQ4sZls5Jo/CbyJlCclF11yVo +SCf2LG/T+9pah5NOmDQ1kPbU+0iKZIV4YFHGTIhyGDE/aPOuUT05ziCGDifgHr0u +SG1x/RLqsVh/POvNxnvP9cQFMQ08BvbEJaTTgB785iwKsvdqCfmng/SAyxSetmzP +StXVB3fh1OoZ8vunRbQYxnmUxycVqaA96zmBx2wLvbvzKo7pZFDE6nbhnT5+MRAM +z/VIK89W26uB4gj8sBFslqZjT0jPqsAZuvDm7swOtMwIcEolyGJuFLqlhN7UwMz2 +9y8+IpYixR+HvD1TZI9NtmuCmv3kPrWgoMZg6yvaBayTIr8RdYzi6FO/C1lLiraz +48dH3sXWRa8cgw6VcSUwYrEBIc3sotdsupO1iOjcFybIwaee0YTZJfjvbqkCAwEA +AQKCAgEArRnfdpaJi1xLPGTCMDsIt9kUku0XswgN7PmxsYsKFAB+2S40/jYAIRm9 +1YjpItsMA8RgFfSOdJ77o6TctCMQyo17F8bm4+uwuic5RLfv7Cx2QmsdQF8jDfFx +y7UGPJD7znjbf76uxXOjEB2FqZX3s9TAgkzHXIUQtoQW7RVhkCWHPjxKxgd5+NY2 +FrDoUpd9xhD9CcTsw1+wbRZdGW88nL6/B50dP2AFORM2VYo8MWr6y9FEn3YLsGOC +uu7fxBk1aUrHyl81VRkTMMROB1zkuiUk1FtzrEm+5U15rXXBFYOVe9+qeLhtuOlh +wueDoz0pzvF/JLe24uTik6YL0Ae6SD0pFXQ2KDrdH3cUHLok3r76/yGzaDNTFjS2 +2WbQ8dEJV8veNHk8gjGpFTJIsBUlcZpmUCDHlfvVMb3+2ahQ+28piQUt5t3zqJdZ +NDqsOHzY6BRPc+Wm85Xii/lWiQceZSee/b1Enu+nchsyXhSenBfC6bIGZReyMI0K +KKKuVhyR6OSOiR5ZdZ/NyXGqsWy05fn/h0X9hnpETsNaNYNKWvpHLfKll+STJpf7 +AZquJPIclQyiq5NONx6kfPztoCLkKV/zOgIj3Sx5oSZq+5gpO91nXWVwkTbqK1d1 +004q2Mah6UQyAk1XGQc2pHx7ouVcWawjU30vZ4C015Hv2lm/gVkCggEBAPltATYS +OqOSL1YAtIHPiHxMjNAgUdglq8JiJFXVfkocGU9eNub3Ed3sSWu6GB9Myu/sSKje +bJ5DZqxJnvB2Fqmu9I9OunLGFSD0aXs4prwsQ1Rm5FcbImtrxcciASdkoo8Pj0z4 +vk2r2NZD3VtER5Uh+YjSDkxcS9gBStXUpCL6gj69UpOxMmWqZVjyHatVB4lEvYJl +N82uT7N7QVNL1DzcZ9z4C4r7ks1Pm7ka12s5m/oaAlAMdVeofiPJe1xA9zRToSr4 +tIbMkOeXFLVRLuji/7XsOgal5Rl59p+OwLshX5cswPVOMrH6zt+hbsJ5q8M5dqnX +VAOBK7KNQ/EKZwcCggEBAPD6KVvyCim46n5EbcEqCkO7gevwZkw/9vLwmM5YsxTh +z9FQkPO0iB7mwbX8w04I91Pre4NdfcgMG0pP1b13Sb4KHBchqW1a+TCs3kSGC6gn +1SxmXHnA9jRxAkrWlGkoAQEz+aP61cXiiy2tXpQwJ8xQCKprfoqWZwhkCtEVU6CE +S7v9cscOHIqgNxx4WoceMmq4EoihHAZzHxTcNVbByckMjb2XQJ0iNw3lDP4ddvc+ +a4HzHfHkhzeQ5ZNc8SvWU8z80aSCOKRsSD3aUTZzxhZ4O2tZSW7v7p+FpvVee7bC +g8YCfszTdpVUMlLRLjScimAcovcFLSvtyupinxWg4M8CggEAN9YGEmOsSte7zwXj +YrfhtumwEBtcFwX/2Ej+F1Tuq4p0xAa0RaoDjumJWhtTsRYQy/raHSuFpzwxbNoi +QXQ+CIhI6RfXtz/OlQ0B2/rHoJJMFEXgUfuaDfAXW0eqeHYXyezSyIlamKqipPyW +Pgsf9yue39keKEv1EorfhNTQVaA8rezV4oglXwrxGyNALw2e3UTNI7ai8mFWKDis +XAg6n9E7UwUYGGnO6DUtCBgRJ0jDOQ6/e8n+LrxiWIKPIgzNCiK6jpMUXqTGv4Fb +umdNGAdQ9RnHt5tFmRlrczaSwJFtA7uaCpAR2zPpQbiywchZAiAIB2dTwGEXNiZX +kksg2wKCAQEA6pNad3qhkgPDoK6T+Jkn7M82paoaqtcJWWwEE7oceZNnbWZz9Agl +CY+vuawXonrv5+0vCq2Tp4zBdBFLC2h3jFrjBVFrUFxifpOIukOSTVqZFON/2bWQ +9XOcu6UuSz7522Xw+UNPnZXtzcUacD6AP08ZYGvLfrTyDyTzspyED5k48ALEHCkM +d5WGkFxII4etpF0TDZVnZo/iDbhe49k4yFFEGO6Ho26PESOLBkNAb2V/2bwDxlij +l9+g21Z6HiZA5SamHPH2mXgeyrcen1cL2QupK9J6vVcqfnboE6qp2zp2c+Yx8MlY +gfy4EA44YFaSDQVTTgrn8f9Eq+zc130H2QKCAQEAqOKgv68nIPdDSngNyCVyWego +boFiDaEJoBBg8FrBjTJ6wFLrNAnXmbvfTtgNmNAzF1cUPJZlIIsHgGrMCfpehbXq +WQQIw+E+yFbTGLxseGRfsLrV0CsgnAoOVeod+yIHmqc3livaUbrWhL1V2f6Ue+sE +7YLp/iP43NaMfA4kYk2ep7+ZJoEVkCjHJJaHWgAG3RynPJHkTJlSgu7wLYvGc9uE +ZsEFUM46lX02t7rrtMfasVGrUy1c2xOxFb4v1vG6iEZ7+YWeq5o3AkxUwEGn+mG4 +/3p+k4AaTXJDXgyZ0Sv6CkGuPHenAYG4cswcUUEf/G4Ag77x6LBNMgycJBxUJA== +-----END RSA PRIVATE KEY----- diff --git a/apex/permission/com.android.permission.pk8 b/apex/permission/com.android.permission.pk8 Binary files differnew file mode 100644 index 000000000000..d51673dbc2fc --- /dev/null +++ b/apex/permission/com.android.permission.pk8 diff --git a/apex/permission/com.android.permission.x509.pem b/apex/permission/com.android.permission.x509.pem new file mode 100644 index 000000000000..4b146c9edd4f --- /dev/null +++ b/apex/permission/com.android.permission.x509.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGKzCCBBOgAwIBAgIUezo3fQeVZsmLpm/dkpGWJ/G/MN8wDQYJKoZIhvcNAQEL +BQAwgaMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH +DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy +b2lkMR8wHQYDVQQDDBZjb20uYW5kcm9pZC5wZXJtaXNzaW9uMSIwIAYJKoZIhvcN +AQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMCAXDTE5MTAwOTIxMzExOVoYDzQ3NTcw +OTA0MjEzMTE5WjCBozELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWEx +FjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEDAOBgNVBAoMB0FuZHJvaWQxEDAOBgNV +BAsMB0FuZHJvaWQxHzAdBgNVBAMMFmNvbS5hbmRyb2lkLnBlcm1pc3Npb24xIjAg +BgkqhkiG9w0BCQEWE2FuZHJvaWRAYW5kcm9pZC5jb20wggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCxefguRJ7E6tBCTEOeU2HJEGs6AQQapLz9hMed0aaJ +Qr7aTQiYJEk+sG4+jPYbjpxa8JDDzJHp+4g7DjfSb+dvT9n84A8lWaI/yRXTZTQN +Hu5m/bgHhi0LbySpiaFyodXBKUAnOhZyGPtYjtBFywFylueub8ryc1Z6UxxU7udH +1mkIr7sE48Qkq5SyjFROE96iFmYA+vS/JXOfS0NBHiMB4GBxx4V7kXpvrTI7hhZG +HiyhKvNh7wyHIhO9nDEw1rwtAH6CsL3YkQEVBeAU98m+0Au+qStLYkKHh2l8zT4W +7sVK1VSqfB+VqOUmeIGdzlBfqMsoXD+FJz6KnIdUHIwjFDjL7Xr+hd+7xve+Q3S+ +U3Blk/U6atY8PM09wNfilG+SvwcKk5IgriDcu3rWKgIFxbUUaxLrDW7pLlu6wt/d +GGtKK+Bc0jF+9Z901Tl33i5xhc5yOktT0btkKs7lSeE6VzP/Nk5g0SuzixmuRoh9 +f5Ge41N2ZCEHNXx3wZeVZwHIIPfYrL7Yql1Xoxbfs4ETFk6ChzVQcvjfDQQuK58J +uNc+TOCoI/qflXwGCwpuHl0ier8V5Z4tpMUl5rWyVR/QGRtLPvs2lLuxczDw1OXq +wEVtCMn9aNnd4y7R9PZ52hi53HAvDjpWefrLYi+Q04J6iGFQ1qAFBClK9DquBvmR +swIDAQABo1MwUTAdBgNVHQ4EFgQULpfus5s5SrqLkoUKyPXA0D1iHPMwHwYDVR0j +BBgwFoAULpfus5s5SrqLkoUKyPXA0D1iHPMwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAjxQG5EFv8V/9yV2glI53VOmlWMjfEgvUjd39s/XLyPlr +OzPOKSB0NFo8To3l4l+MsManxPK8y0OyfEVKbWVz9onv0ovo5MVokBmV/2G0jmsV +B4e9yjOq+DmqIvY/Qh63Ywb97sTgcFI8620MhQDbh2IpEGv4ZNV0H6rgXmgdSCBw +1EjBoYfFpN5aMgZjeyzZcq+d1IapdWqdhuEJQkMvoYS4WIumNIJlEXPQRoq/F5Ih +nszdbKI/jVyiGFa2oeZ3rja1Y6GCRU8TYEoKx1pjS8uQDOEDTwsG/QnUe9peEj0V +SsCkIidJWTomAmq9Tub9vpBe1zuTpuRAwxwR0qwgSxozV1Mvow1dJ19oFtHX0yD6 +ZjCpRn5PW9kMvSWSlrcrFs1NJf0j1Cvf7bHpkEDqLqpMnnh9jaFQq3nzDY+MWcIR +jDcgQpI+AiE2/qtauZnFEVhbce49nCnk9+5bpTTIZJdzqeaExe5KXHwEtZLaEDh4 +atLY9LuEvPsjmDIMOR6hycD9FvwGXhJOQBjESIWFwigtSb1Yud9n6201jw3MLJ4k ++WhkbmZgWy+xc+Mdm5H3XyB1lvHaHGkxu+QB9KyQuVQKwbUVcbwZIfTFPN6Zr/dS +ZXJqAbBhG/dBgF0LazuLaPVpibi+a3Y+tb9b8eXGkz4F97PWZIEDkELQ+9KOvhc= +-----END CERTIFICATE----- diff --git a/apex/permission/framework/Android.bp b/apex/permission/framework/Android.bp new file mode 100644 index 000000000000..c0560f61460f --- /dev/null +++ b/apex/permission/framework/Android.bp @@ -0,0 +1,45 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +filegroup { + name: "framework-permission-sources", + srcs: [ + "java/**/*.java", + "java/**/*.aidl", + ], + path: "java", +} + +java_sdk_library { + name: "framework-permission", + defaults: ["framework-module-defaults"], + + // Restrict access to implementation library. + impl_library_visibility: ["//frameworks/base/apex/permission:__subpackages__"], + + srcs: [ + ":framework-permission-sources", + ], + + apex_available: [ + "com.android.permission", + "test_com.android.permission", + ], + permitted_packages: [ + "android.permission", + "android.app.role", + ], + hostdex: true, + installable: true, +} diff --git a/apex/permission/framework/api/current.txt b/apex/permission/framework/api/current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/permission/framework/api/current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/permission/framework/api/module-lib-current.txt b/apex/permission/framework/api/module-lib-current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/permission/framework/api/module-lib-current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/permission/framework/api/module-lib-removed.txt b/apex/permission/framework/api/module-lib-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/permission/framework/api/module-lib-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/permission/framework/api/removed.txt b/apex/permission/framework/api/removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/permission/framework/api/removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/permission/framework/api/system-current.txt b/apex/permission/framework/api/system-current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/permission/framework/api/system-current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/permission/framework/api/system-removed.txt b/apex/permission/framework/api/system-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/permission/framework/api/system-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/permission/framework/java/android/permission/PermissionState.java b/apex/permission/framework/java/android/permission/PermissionState.java new file mode 100644 index 000000000000..e810db8ecbfe --- /dev/null +++ b/apex/permission/framework/java/android/permission/PermissionState.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.permission; + +/** + * @hide + */ +public class PermissionState {} diff --git a/apex/permission/service/Android.bp b/apex/permission/service/Android.bp new file mode 100644 index 000000000000..b7d843352d8e --- /dev/null +++ b/apex/permission/service/Android.bp @@ -0,0 +1,42 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +filegroup { + name: "service-permission-sources", + srcs: [ + "java/**/*.java", + ], + path: "java", +} + +java_sdk_library { + name: "service-permission", + defaults: ["framework-system-server-module-defaults"], + impl_library_visibility: [ + "//frameworks/base/apex/permission/tests", + "//frameworks/base/services/tests/mockingservicestests", + "//frameworks/base/services/tests/servicestests", + ], + srcs: [ + ":service-permission-sources", + ], + libs: [ + "framework-permission", + ], + apex_available: [ + "com.android.permission", + "test_com.android.permission", + ], + installable: true, +} diff --git a/apex/permission/service/api/current.txt b/apex/permission/service/api/current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/permission/service/api/current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/permission/service/api/removed.txt b/apex/permission/service/api/removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/permission/service/api/removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/permission/service/api/system-server-current.txt b/apex/permission/service/api/system-server-current.txt new file mode 100644 index 000000000000..c76cc3275737 --- /dev/null +++ b/apex/permission/service/api/system-server-current.txt @@ -0,0 +1,46 @@ +// Signature format: 2.0 +package com.android.permission.persistence { + + public interface RuntimePermissionsPersistence { + method @NonNull public static com.android.permission.persistence.RuntimePermissionsPersistence createInstance(); + method public void deleteForUser(@NonNull android.os.UserHandle); + method @Nullable public com.android.permission.persistence.RuntimePermissionsState readForUser(@NonNull android.os.UserHandle); + method public void writeForUser(@NonNull com.android.permission.persistence.RuntimePermissionsState, @NonNull android.os.UserHandle); + } + + public final class RuntimePermissionsState { + ctor public RuntimePermissionsState(int, @Nullable String, @NonNull java.util.Map<java.lang.String,java.util.List<com.android.permission.persistence.RuntimePermissionsState.PermissionState>>, @NonNull java.util.Map<java.lang.String,java.util.List<com.android.permission.persistence.RuntimePermissionsState.PermissionState>>); + method @Nullable public String getFingerprint(); + method @NonNull public java.util.Map<java.lang.String,java.util.List<com.android.permission.persistence.RuntimePermissionsState.PermissionState>> getPackagePermissions(); + method @NonNull public java.util.Map<java.lang.String,java.util.List<com.android.permission.persistence.RuntimePermissionsState.PermissionState>> getSharedUserPermissions(); + method public int getVersion(); + field public static final int NO_VERSION = -1; // 0xffffffff + } + + public static final class RuntimePermissionsState.PermissionState { + ctor public RuntimePermissionsState.PermissionState(@NonNull String, boolean, int); + method public int getFlags(); + method @NonNull public String getName(); + method public boolean isGranted(); + } + +} + +package com.android.role.persistence { + + public interface RolesPersistence { + method @NonNull public static com.android.role.persistence.RolesPersistence createInstance(); + method public void deleteForUser(@NonNull android.os.UserHandle); + method @Nullable public com.android.role.persistence.RolesState readForUser(@NonNull android.os.UserHandle); + method public void writeForUser(@NonNull com.android.role.persistence.RolesState, @NonNull android.os.UserHandle); + } + + public final class RolesState { + ctor public RolesState(int, @Nullable String, @NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>>); + method @Nullable public String getPackagesHash(); + method @NonNull public java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getRoles(); + method public int getVersion(); + } + +} + diff --git a/apex/permission/service/api/system-server-removed.txt b/apex/permission/service/api/system-server-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/permission/service/api/system-server-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/permission/service/java/com/android/permission/persistence/IoUtils.java b/apex/permission/service/java/com/android/permission/persistence/IoUtils.java new file mode 100644 index 000000000000..569a78c0ab41 --- /dev/null +++ b/apex/permission/service/java/com/android/permission/persistence/IoUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permission.persistence; + +import android.annotation.NonNull; + +/** + * Utility class for IO. + * + * @hide + */ +public class IoUtils { + + private IoUtils() {} + + /** + * Close 'closeable' ignoring any exceptions. + */ + public static void closeQuietly(@NonNull AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception ignored) { + // Ignored. + } + } +} diff --git a/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistence.java b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistence.java new file mode 100644 index 000000000000..aedba290db1f --- /dev/null +++ b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistence.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permission.persistence; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.SystemApi.Client; +import android.os.UserHandle; + +/** + * Persistence for runtime permissions. + * + * TODO(b/147914847): Remove @hide when it becomes the default. + * @hide + */ +@SystemApi(client = Client.SYSTEM_SERVER) +public interface RuntimePermissionsPersistence { + + /** + * Read the runtime permissions from persistence. + * + * This will perform I/O operations synchronously. + * + * @param user the user to read for + * @return the runtime permissions read + */ + @Nullable + RuntimePermissionsState readForUser(@NonNull UserHandle user); + + /** + * Write the runtime permissions to persistence. + * + * This will perform I/O operations synchronously. + * + * @param runtimePermissions the runtime permissions to write + * @param user the user to write for + */ + void writeForUser(@NonNull RuntimePermissionsState runtimePermissions, + @NonNull UserHandle user); + + /** + * Delete the runtime permissions from persistence. + * + * This will perform I/O operations synchronously. + * + * @param user the user to delete for + */ + void deleteForUser(@NonNull UserHandle user); + + /** + * Create a new instance of {@link RuntimePermissionsPersistence} implementation. + * + * @return the new instance. + */ + @NonNull + static RuntimePermissionsPersistence createInstance() { + return new RuntimePermissionsPersistenceImpl(); + } +} diff --git a/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistenceImpl.java b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistenceImpl.java new file mode 100644 index 000000000000..e43f59a3377a --- /dev/null +++ b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsPersistenceImpl.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permission.persistence; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ApexEnvironment; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.AtomicFile; +import android.util.Log; +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Persistence implementation for runtime permissions. + * + * TODO(b/147914847): Remove @hide when it becomes the default. + * @hide + */ +public class RuntimePermissionsPersistenceImpl implements RuntimePermissionsPersistence { + + private static final String LOG_TAG = RuntimePermissionsPersistenceImpl.class.getSimpleName(); + + private static final String APEX_MODULE_NAME = "com.android.permission"; + + private static final String RUNTIME_PERMISSIONS_FILE_NAME = "runtime-permissions.xml"; + + private static final String TAG_PACKAGE = "package"; + private static final String TAG_PERMISSION = "permission"; + private static final String TAG_RUNTIME_PERMISSIONS = "runtime-permissions"; + private static final String TAG_SHARED_USER = "shared-user"; + + private static final String ATTRIBUTE_FINGERPRINT = "fingerprint"; + private static final String ATTRIBUTE_FLAGS = "flags"; + private static final String ATTRIBUTE_GRANTED = "granted"; + private static final String ATTRIBUTE_NAME = "name"; + private static final String ATTRIBUTE_VERSION = "version"; + + @Nullable + @Override + public RuntimePermissionsState readForUser(@NonNull UserHandle user) { + File file = getFile(user); + try (FileInputStream inputStream = new AtomicFile(file).openRead()) { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(inputStream, null); + return parseXml(parser); + } catch (FileNotFoundException e) { + Log.i(LOG_TAG, "runtime-permissions.xml not found"); + return null; + } catch (XmlPullParserException | IOException e) { + throw new IllegalStateException("Failed to read runtime-permissions.xml: " + file , e); + } + } + + @NonNull + private static RuntimePermissionsState parseXml(@NonNull XmlPullParser parser) + throws IOException, XmlPullParserException { + int type; + int depth; + int innerDepth = parser.getDepth() + 1; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { + if (depth > innerDepth || type != XmlPullParser.START_TAG) { + continue; + } + + if (parser.getName().equals(TAG_RUNTIME_PERMISSIONS)) { + return parseRuntimePermissions(parser); + } + } + throw new IllegalStateException("Missing <" + TAG_RUNTIME_PERMISSIONS + + "> in runtime-permissions.xml"); + } + + @NonNull + private static RuntimePermissionsState parseRuntimePermissions(@NonNull XmlPullParser parser) + throws IOException, XmlPullParserException { + String versionValue = parser.getAttributeValue(null, ATTRIBUTE_VERSION); + int version = versionValue != null ? Integer.parseInt(versionValue) + : RuntimePermissionsState.NO_VERSION; + String fingerprint = parser.getAttributeValue(null, ATTRIBUTE_FINGERPRINT); + + Map<String, List<RuntimePermissionsState.PermissionState>> packagePermissions = + new ArrayMap<>(); + Map<String, List<RuntimePermissionsState.PermissionState>> sharedUserPermissions = + new ArrayMap<>(); + int type; + int depth; + int innerDepth = parser.getDepth() + 1; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { + if (depth > innerDepth || type != XmlPullParser.START_TAG) { + continue; + } + + switch (parser.getName()) { + case TAG_PACKAGE: { + String packageName = parser.getAttributeValue(null, ATTRIBUTE_NAME); + List<RuntimePermissionsState.PermissionState> permissions = parsePermissions( + parser); + packagePermissions.put(packageName, permissions); + break; + } + case TAG_SHARED_USER: { + String sharedUserName = parser.getAttributeValue(null, ATTRIBUTE_NAME); + List<RuntimePermissionsState.PermissionState> permissions = parsePermissions( + parser); + sharedUserPermissions.put(sharedUserName, permissions); + break; + } + } + } + + return new RuntimePermissionsState(version, fingerprint, packagePermissions, + sharedUserPermissions); + } + + @NonNull + private static List<RuntimePermissionsState.PermissionState> parsePermissions( + @NonNull XmlPullParser parser) throws IOException, XmlPullParserException { + List<RuntimePermissionsState.PermissionState> permissions = new ArrayList<>(); + int type; + int depth; + int innerDepth = parser.getDepth() + 1; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { + if (depth > innerDepth || type != XmlPullParser.START_TAG) { + continue; + } + + if (parser.getName().equals(TAG_PERMISSION)) { + String name = parser.getAttributeValue(null, ATTRIBUTE_NAME); + boolean granted = Boolean.parseBoolean(parser.getAttributeValue(null, + ATTRIBUTE_GRANTED)); + int flags = Integer.parseInt(parser.getAttributeValue(null, + ATTRIBUTE_FLAGS), 16); + RuntimePermissionsState.PermissionState permission = + new RuntimePermissionsState.PermissionState(name, granted, flags); + permissions.add(permission); + } + } + return permissions; + } + + @Override + public void writeForUser(@NonNull RuntimePermissionsState runtimePermissions, + @NonNull UserHandle user) { + File file = getFile(user); + AtomicFile atomicFile = new AtomicFile(file); + FileOutputStream outputStream = null; + try { + outputStream = atomicFile.startWrite(); + + XmlSerializer serializer = Xml.newSerializer(); + serializer.setOutput(outputStream, StandardCharsets.UTF_8.name()); + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + serializer.startDocument(null, true); + + serializeRuntimePermissions(serializer, runtimePermissions); + + serializer.endDocument(); + atomicFile.finishWrite(outputStream); + } catch (Exception e) { + Log.wtf(LOG_TAG, "Failed to write runtime-permissions.xml, restoring backup: " + file, + e); + atomicFile.failWrite(outputStream); + } finally { + IoUtils.closeQuietly(outputStream); + } + } + + private static void serializeRuntimePermissions(@NonNull XmlSerializer serializer, + @NonNull RuntimePermissionsState runtimePermissions) throws IOException { + serializer.startTag(null, TAG_RUNTIME_PERMISSIONS); + + int version = runtimePermissions.getVersion(); + serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version)); + String fingerprint = runtimePermissions.getFingerprint(); + if (fingerprint != null) { + serializer.attribute(null, ATTRIBUTE_FINGERPRINT, fingerprint); + } + + for (Map.Entry<String, List<RuntimePermissionsState.PermissionState>> entry + : runtimePermissions.getPackagePermissions().entrySet()) { + String packageName = entry.getKey(); + List<RuntimePermissionsState.PermissionState> permissions = entry.getValue(); + + serializer.startTag(null, TAG_PACKAGE); + serializer.attribute(null, ATTRIBUTE_NAME, packageName); + serializePermissions(serializer, permissions); + serializer.endTag(null, TAG_PACKAGE); + } + + for (Map.Entry<String, List<RuntimePermissionsState.PermissionState>> entry + : runtimePermissions.getSharedUserPermissions().entrySet()) { + String sharedUserName = entry.getKey(); + List<RuntimePermissionsState.PermissionState> permissions = entry.getValue(); + + serializer.startTag(null, TAG_SHARED_USER); + serializer.attribute(null, ATTRIBUTE_NAME, sharedUserName); + serializePermissions(serializer, permissions); + serializer.endTag(null, TAG_SHARED_USER); + } + + serializer.endTag(null, TAG_RUNTIME_PERMISSIONS); + } + + private static void serializePermissions(@NonNull XmlSerializer serializer, + @NonNull List<RuntimePermissionsState.PermissionState> permissions) throws IOException { + int permissionsSize = permissions.size(); + for (int i = 0; i < permissionsSize; i++) { + RuntimePermissionsState.PermissionState permissionState = permissions.get(i); + + serializer.startTag(null, TAG_PERMISSION); + serializer.attribute(null, ATTRIBUTE_NAME, permissionState.getName()); + serializer.attribute(null, ATTRIBUTE_GRANTED, Boolean.toString( + permissionState.isGranted() && (permissionState.getFlags() + & PackageManager.FLAG_PERMISSION_ONE_TIME) == 0)); + serializer.attribute(null, ATTRIBUTE_FLAGS, Integer.toHexString( + permissionState.getFlags())); + serializer.endTag(null, TAG_PERMISSION); + } + } + + @Override + public void deleteForUser(@NonNull UserHandle user) { + getFile(user).delete(); + } + + @NonNull + private static File getFile(@NonNull UserHandle user) { + ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME); + File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user); + return new File(dataDirectory, RUNTIME_PERMISSIONS_FILE_NAME); + } +} diff --git a/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsState.java b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsState.java new file mode 100644 index 000000000000..c6bfc6d32989 --- /dev/null +++ b/apex/permission/service/java/com/android/permission/persistence/RuntimePermissionsState.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permission.persistence; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.SystemApi.Client; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * State of all runtime permissions. + * + * TODO(b/147914847): Remove @hide when it becomes the default. + * @hide + */ +@SystemApi(client = Client.SYSTEM_SERVER) +public final class RuntimePermissionsState { + + /** + * Special value for {@link #mVersion} to indicate that no version was read. + */ + public static final int NO_VERSION = -1; + + /** + * The version of the runtime permissions. + */ + private final int mVersion; + + /** + * The fingerprint of the runtime permissions. + */ + @Nullable + private final String mFingerprint; + + /** + * The runtime permissions by packages. + */ + @NonNull + private final Map<String, List<PermissionState>> mPackagePermissions; + + /** + * The runtime permissions by shared users. + */ + @NonNull + private final Map<String, List<PermissionState>> mSharedUserPermissions; + + /** + * Create a new instance of this class. + * + * @param version the version of the runtime permissions + * @param fingerprint the fingerprint of the runtime permissions + * @param packagePermissions the runtime permissions by packages + * @param sharedUserPermissions the runtime permissions by shared users + */ + public RuntimePermissionsState(int version, @Nullable String fingerprint, + @NonNull Map<String, List<PermissionState>> packagePermissions, + @NonNull Map<String, List<PermissionState>> sharedUserPermissions) { + mVersion = version; + mFingerprint = fingerprint; + mPackagePermissions = packagePermissions; + mSharedUserPermissions = sharedUserPermissions; + } + + /** + * Get the version of the runtime permissions. + * + * @return the version of the runtime permissions + */ + public int getVersion() { + return mVersion; + } + + /** + * Get the fingerprint of the runtime permissions. + * + * @return the fingerprint of the runtime permissions + */ + @Nullable + public String getFingerprint() { + return mFingerprint; + } + + /** + * Get the runtime permissions by packages. + * + * @return the runtime permissions by packages + */ + @NonNull + public Map<String, List<PermissionState>> getPackagePermissions() { + return mPackagePermissions; + } + + /** + * Get the runtime permissions by shared users. + * + * @return the runtime permissions by shared users + */ + @NonNull + public Map<String, List<PermissionState>> getSharedUserPermissions() { + return mSharedUserPermissions; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + RuntimePermissionsState that = (RuntimePermissionsState) object; + return mVersion == that.mVersion + && Objects.equals(mFingerprint, that.mFingerprint) + && Objects.equals(mPackagePermissions, that.mPackagePermissions) + && Objects.equals(mSharedUserPermissions, that.mSharedUserPermissions); + } + + @Override + public int hashCode() { + return Objects.hash(mVersion, mFingerprint, mPackagePermissions, mSharedUserPermissions); + } + + /** + * State of a single permission. + */ + public static final class PermissionState { + + /** + * The name of the permission. + */ + @NonNull + private final String mName; + + /** + * Whether the permission is granted. + */ + private final boolean mGranted; + + /** + * The flags of the permission. + */ + private final int mFlags; + + /** + * Create a new instance of this class. + * + * @param name the name of the permission + * @param granted whether the permission is granted + * @param flags the flags of the permission + */ + public PermissionState(@NonNull String name, boolean granted, int flags) { + mName = name; + mGranted = granted; + mFlags = flags; + } + + /** + * Get the name of the permission. + * + * @return the name of the permission + */ + @NonNull + public String getName() { + return mName; + } + + /** + * Get whether the permission is granted. + * + * @return whether the permission is granted + */ + public boolean isGranted() { + return mGranted; + } + + /** + * Get the flags of the permission. + * + * @return the flags of the permission + */ + public int getFlags() { + return mFlags; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + PermissionState that = (PermissionState) object; + return mGranted == that.mGranted + && mFlags == that.mFlags + && Objects.equals(mName, that.mName); + } + + @Override + public int hashCode() { + return Objects.hash(mName, mGranted, mFlags); + } + } +} diff --git a/apex/permission/service/java/com/android/role/persistence/RolesPersistence.java b/apex/permission/service/java/com/android/role/persistence/RolesPersistence.java new file mode 100644 index 000000000000..2e5a28aa1d6a --- /dev/null +++ b/apex/permission/service/java/com/android/role/persistence/RolesPersistence.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.role.persistence; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.SystemApi.Client; +import android.os.UserHandle; + +/** + * Persistence for roles. + * + * TODO(b/147914847): Remove @hide when it becomes the default. + * @hide + */ +@SystemApi(client = Client.SYSTEM_SERVER) +public interface RolesPersistence { + + /** + * Read the roles from persistence. + * + * This will perform I/O operations synchronously. + * + * @param user the user to read for + * @return the roles read + */ + @Nullable + RolesState readForUser(@NonNull UserHandle user); + + /** + * Write the roles to persistence. + * + * This will perform I/O operations synchronously. + * + * @param roles the roles to write + * @param user the user to write for + */ + void writeForUser(@NonNull RolesState roles, @NonNull UserHandle user); + + /** + * Delete the roles from persistence. + * + * This will perform I/O operations synchronously. + * + * @param user the user to delete for + */ + void deleteForUser(@NonNull UserHandle user); + + /** + * Create a new instance of {@link RolesPersistence} implementation. + * + * @return the new instance. + */ + @NonNull + static RolesPersistence createInstance() { + return new RolesPersistenceImpl(); + } +} diff --git a/apex/permission/service/java/com/android/role/persistence/RolesPersistenceImpl.java b/apex/permission/service/java/com/android/role/persistence/RolesPersistenceImpl.java new file mode 100644 index 000000000000..f66257f13ef6 --- /dev/null +++ b/apex/permission/service/java/com/android/role/persistence/RolesPersistenceImpl.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.role.persistence; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ApexEnvironment; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.Log; +import android.util.Xml; + +import com.android.permission.persistence.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Set; + +/** + * Persistence implementation for roles. + * + * TODO(b/147914847): Remove @hide when it becomes the default. + * @hide + */ +public class RolesPersistenceImpl implements RolesPersistence { + + private static final String LOG_TAG = RolesPersistenceImpl.class.getSimpleName(); + + private static final String APEX_MODULE_NAME = "com.android.permission"; + + private static final String ROLES_FILE_NAME = "roles.xml"; + + private static final String TAG_ROLES = "roles"; + private static final String TAG_ROLE = "role"; + private static final String TAG_HOLDER = "holder"; + + private static final String ATTRIBUTE_VERSION = "version"; + private static final String ATTRIBUTE_NAME = "name"; + private static final String ATTRIBUTE_PACKAGES_HASH = "packagesHash"; + + @Nullable + @Override + public RolesState readForUser(@NonNull UserHandle user) { + File file = getFile(user); + try (FileInputStream inputStream = new AtomicFile(file).openRead()) { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(inputStream, null); + return parseXml(parser); + } catch (FileNotFoundException e) { + Log.i(LOG_TAG, "roles.xml not found"); + return null; + } catch (XmlPullParserException | IOException e) { + throw new IllegalStateException("Failed to read roles.xml: " + file , e); + } + } + + @NonNull + private static RolesState parseXml(@NonNull XmlPullParser parser) + throws IOException, XmlPullParserException { + int type; + int depth; + int innerDepth = parser.getDepth() + 1; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { + if (depth > innerDepth || type != XmlPullParser.START_TAG) { + continue; + } + + if (parser.getName().equals(TAG_ROLES)) { + return parseRoles(parser); + } + } + throw new IllegalStateException("Missing <" + TAG_ROLES + "> in roles.xml"); + } + + @NonNull + private static RolesState parseRoles(@NonNull XmlPullParser parser) + throws IOException, XmlPullParserException { + int version = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_VERSION)); + String packagesHash = parser.getAttributeValue(null, ATTRIBUTE_PACKAGES_HASH); + + Map<String, Set<String>> roles = new ArrayMap<>(); + int type; + int depth; + int innerDepth = parser.getDepth() + 1; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { + if (depth > innerDepth || type != XmlPullParser.START_TAG) { + continue; + } + + if (parser.getName().equals(TAG_ROLE)) { + String roleName = parser.getAttributeValue(null, ATTRIBUTE_NAME); + Set<String> roleHolders = parseRoleHolders(parser); + roles.put(roleName, roleHolders); + } + } + + return new RolesState(version, packagesHash, roles); + } + + @NonNull + private static Set<String> parseRoleHolders(@NonNull XmlPullParser parser) + throws IOException, XmlPullParserException { + Set<String> roleHolders = new ArraySet<>(); + int type; + int depth; + int innerDepth = parser.getDepth() + 1; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { + if (depth > innerDepth || type != XmlPullParser.START_TAG) { + continue; + } + + if (parser.getName().equals(TAG_HOLDER)) { + String roleHolder = parser.getAttributeValue(null, ATTRIBUTE_NAME); + roleHolders.add(roleHolder); + } + } + return roleHolders; + } + + @Override + public void writeForUser(@NonNull RolesState roles, @NonNull UserHandle user) { + File file = getFile(user); + AtomicFile atomicFile = new AtomicFile(file); + FileOutputStream outputStream = null; + try { + outputStream = atomicFile.startWrite(); + + XmlSerializer serializer = Xml.newSerializer(); + serializer.setOutput(outputStream, StandardCharsets.UTF_8.name()); + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + serializer.startDocument(null, true); + + serializeRoles(serializer, roles); + + serializer.endDocument(); + atomicFile.finishWrite(outputStream); + } catch (Exception e) { + Log.wtf(LOG_TAG, "Failed to write roles.xml, restoring backup: " + file, + e); + atomicFile.failWrite(outputStream); + } finally { + IoUtils.closeQuietly(outputStream); + } + } + + private static void serializeRoles(@NonNull XmlSerializer serializer, + @NonNull RolesState roles) throws IOException { + serializer.startTag(null, TAG_ROLES); + + int version = roles.getVersion(); + serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version)); + String packagesHash = roles.getPackagesHash(); + if (packagesHash != null) { + serializer.attribute(null, ATTRIBUTE_PACKAGES_HASH, packagesHash); + } + + for (Map.Entry<String, Set<String>> entry : roles.getRoles().entrySet()) { + String roleName = entry.getKey(); + Set<String> roleHolders = entry.getValue(); + + serializer.startTag(null, TAG_ROLE); + serializer.attribute(null, ATTRIBUTE_NAME, roleName); + serializeRoleHolders(serializer, roleHolders); + serializer.endTag(null, TAG_ROLE); + } + + serializer.endTag(null, TAG_ROLES); + } + + private static void serializeRoleHolders(@NonNull XmlSerializer serializer, + @NonNull Set<String> roleHolders) throws IOException { + for (String roleHolder : roleHolders) { + serializer.startTag(null, TAG_HOLDER); + serializer.attribute(null, ATTRIBUTE_NAME, roleHolder); + serializer.endTag(null, TAG_HOLDER); + } + } + + @Override + public void deleteForUser(@NonNull UserHandle user) { + getFile(user).delete(); + } + + @NonNull + private static File getFile(@NonNull UserHandle user) { + ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME); + File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user); + return new File(dataDirectory, ROLES_FILE_NAME); + } +} diff --git a/apex/permission/service/java/com/android/role/persistence/RolesState.java b/apex/permission/service/java/com/android/role/persistence/RolesState.java new file mode 100644 index 000000000000..f61efa0e840d --- /dev/null +++ b/apex/permission/service/java/com/android/role/persistence/RolesState.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.role.persistence; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.SystemApi.Client; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * State of all roles. + * + * TODO(b/147914847): Remove @hide when it becomes the default. + * @hide + */ +@SystemApi(client = Client.SYSTEM_SERVER) +public final class RolesState { + + /** + * The version of the roles. + */ + private final int mVersion; + + /** + * The hash of all packages in the system. + */ + @Nullable + private final String mPackagesHash; + + /** + * The roles. + */ + @NonNull + private final Map<String, Set<String>> mRoles; + + /** + * Create a new instance of this class. + * + * @param version the version of the roles + * @param packagesHash the hash of all packages in the system + * @param roles the roles + */ + public RolesState(int version, @Nullable String packagesHash, + @NonNull Map<String, Set<String>> roles) { + mVersion = version; + mPackagesHash = packagesHash; + mRoles = roles; + } + + /** + * Get the version of the roles. + * + * @return the version of the roles + */ + public int getVersion() { + return mVersion; + } + + /** + * Get the hash of all packages in the system. + * + * @return the hash of all packages in the system + */ + @Nullable + public String getPackagesHash() { + return mPackagesHash; + } + + /** + * Get the roles. + * + * @return the roles + */ + @NonNull + public Map<String, Set<String>> getRoles() { + return mRoles; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + RolesState that = (RolesState) object; + return mVersion == that.mVersion + && Objects.equals(mPackagesHash, that.mPackagesHash) + && Objects.equals(mRoles, that.mRoles); + } + + @Override + public int hashCode() { + return Objects.hash(mVersion, mPackagesHash, mRoles); + } +} diff --git a/apex/permission/testing/Android.bp b/apex/permission/testing/Android.bp new file mode 100644 index 000000000000..63bf0a08e956 --- /dev/null +++ b/apex/permission/testing/Android.bp @@ -0,0 +1,25 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +apex_test { + name: "test_com.android.permission", + visibility: [ + "//system/apex/tests", + ], + defaults: ["com.android.permission-defaults"], + manifest: "test_manifest.json", + file_contexts: ":com.android.permission-file_contexts", + // Test APEX, should never be installed + installable: false, +} diff --git a/apex/permission/testing/test_manifest.json b/apex/permission/testing/test_manifest.json new file mode 100644 index 000000000000..bc19a9ea0172 --- /dev/null +++ b/apex/permission/testing/test_manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.permission", + "version": 2147483647 +} diff --git a/apex/permission/tests/Android.bp b/apex/permission/tests/Android.bp new file mode 100644 index 000000000000..271e328c1139 --- /dev/null +++ b/apex/permission/tests/Android.bp @@ -0,0 +1,37 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +android_test { + name: "PermissionApexTests", + sdk_version: "test_current", + srcs: [ + "java/**/*.kt", + ], + static_libs: [ + "service-permission.impl", + "androidx.test.rules", + "androidx.test.ext.junit", + "androidx.test.ext.truth", + "mockito-target-extended-minus-junit4", + ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + compile_multilib: "both", + test_suites: [ + "general-tests", + "mts", + ], +} diff --git a/apex/permission/tests/AndroidManifest.xml b/apex/permission/tests/AndroidManifest.xml new file mode 100644 index 000000000000..57ee6417aeb3 --- /dev/null +++ b/apex/permission/tests/AndroidManifest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.permission.test"> + + <!-- The application has to be debuggable for static mocking to work. --> + <application android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.permission.test" + android:label="Permission APEX Tests" /> +</manifest> diff --git a/apex/permission/tests/java/com/android/permission/persistence/RuntimePermissionsPersistenceTest.kt b/apex/permission/tests/java/com/android/permission/persistence/RuntimePermissionsPersistenceTest.kt new file mode 100644 index 000000000000..2987da087e51 --- /dev/null +++ b/apex/permission/tests/java/com/android/permission/persistence/RuntimePermissionsPersistenceTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permission.persistence + +import android.content.ApexEnvironment +import android.content.Context +import android.os.Process +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations.initMocks +import org.mockito.MockitoSession +import org.mockito.quality.Strictness +import java.io.File + +@RunWith(AndroidJUnit4::class) +class RuntimePermissionsPersistenceTest { + private val context = InstrumentationRegistry.getInstrumentation().context + + private lateinit var mockDataDirectory: File + + private lateinit var mockitoSession: MockitoSession + @Mock + lateinit var apexEnvironment: ApexEnvironment + + private val persistence = RuntimePermissionsPersistence.createInstance() + private val permissionState = RuntimePermissionsState.PermissionState("permission", true, 3) + private val state = RuntimePermissionsState( + 1, "fingerprint", mapOf("package" to listOf(permissionState)), + mapOf("sharedUser" to listOf(permissionState)) + ) + private val user = Process.myUserHandle() + + @Before + fun createMockDataDirectory() { + mockDataDirectory = context.getDir("mock_data", Context.MODE_PRIVATE) + mockDataDirectory.listFiles()!!.forEach { assertThat(it.deleteRecursively()).isTrue() } + } + + @Before + fun mockApexEnvironment() { + initMocks(this) + mockitoSession = mockitoSession() + .mockStatic(ApexEnvironment::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + `when`(ApexEnvironment.getApexEnvironment(eq(APEX_MODULE_NAME))).thenReturn(apexEnvironment) + `when`(apexEnvironment.getDeviceProtectedDataDirForUser(any(UserHandle::class.java))).then { + File(mockDataDirectory, it.arguments[0].toString()).also { it.mkdirs() } + } + } + + @After + fun finishMockingApexEnvironment() { + mockitoSession.finishMocking() + } + + @Test + fun testReadWrite() { + persistence.writeForUser(state, user) + val persistedState = persistence.readForUser(user) + + assertThat(persistedState).isEqualTo(state) + assertThat(persistedState!!.version).isEqualTo(state.version) + assertThat(persistedState.fingerprint).isEqualTo(state.fingerprint) + assertThat(persistedState.packagePermissions).isEqualTo(state.packagePermissions) + val persistedPermissionState = persistedState.packagePermissions.values.first().first() + assertThat(persistedPermissionState.name).isEqualTo(permissionState.name) + assertThat(persistedPermissionState.isGranted).isEqualTo(permissionState.isGranted) + assertThat(persistedPermissionState.flags).isEqualTo(permissionState.flags) + assertThat(persistedState.sharedUserPermissions).isEqualTo(state.sharedUserPermissions) + } + + @Test + fun testDelete() { + persistence.writeForUser(state, user) + persistence.deleteForUser(user) + val persistedState = persistence.readForUser(user) + + assertThat(persistedState).isNull() + } + + companion object { + private const val APEX_MODULE_NAME = "com.android.permission" + } +} diff --git a/apex/permission/tests/java/com/android/role/persistence/RolesPersistenceTest.kt b/apex/permission/tests/java/com/android/role/persistence/RolesPersistenceTest.kt new file mode 100644 index 000000000000..f9d9d5afb25d --- /dev/null +++ b/apex/permission/tests/java/com/android/role/persistence/RolesPersistenceTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.role.persistence + +import android.content.ApexEnvironment +import android.content.Context +import android.os.Process +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations.initMocks +import org.mockito.MockitoSession +import org.mockito.quality.Strictness +import java.io.File + +@RunWith(AndroidJUnit4::class) +class RolesPersistenceTest { + private val context = InstrumentationRegistry.getInstrumentation().context + + private lateinit var mockDataDirectory: File + + private lateinit var mockitoSession: MockitoSession + @Mock + lateinit var apexEnvironment: ApexEnvironment + + private val persistence = RolesPersistence.createInstance() + private val state = RolesState(1, "packagesHash", mapOf("role" to setOf("holder1", "holder2"))) + private val user = Process.myUserHandle() + + @Before + fun createMockDataDirectory() { + mockDataDirectory = context.getDir("mock_data", Context.MODE_PRIVATE) + mockDataDirectory.listFiles()!!.forEach { assertThat(it.deleteRecursively()).isTrue() } + } + + @Before + fun mockApexEnvironment() { + initMocks(this) + mockitoSession = mockitoSession() + .mockStatic(ApexEnvironment::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + `when`(ApexEnvironment.getApexEnvironment(eq(APEX_MODULE_NAME))).thenReturn(apexEnvironment) + `when`(apexEnvironment.getDeviceProtectedDataDirForUser(any(UserHandle::class.java))).then { + File(mockDataDirectory, it.arguments[0].toString()).also { it.mkdirs() } + } + } + + @After + fun finishMockingApexEnvironment() { + mockitoSession.finishMocking() + } + + @Test + fun testReadWrite() { + persistence.writeForUser(state, user) + val persistedState = persistence.readForUser(user) + + assertThat(persistedState).isEqualTo(state) + assertThat(persistedState!!.version).isEqualTo(state.version) + assertThat(persistedState.packagesHash).isEqualTo(state.packagesHash) + assertThat(persistedState.roles).isEqualTo(state.roles) + } + + @Test + fun testDelete() { + persistence.writeForUser(state, user) + persistence.deleteForUser(user) + val persistedState = persistence.readForUser(user) + + assertThat(persistedState).isNull() + } + + companion object { + private const val APEX_MODULE_NAME = "com.android.permission" + } +} diff --git a/apex/statsd/.clang-format b/apex/statsd/.clang-format new file mode 100644 index 000000000000..cead3a079435 --- /dev/null +++ b/apex/statsd/.clang-format @@ -0,0 +1,17 @@ +BasedOnStyle: Google +AllowShortIfStatementsOnASingleLine: true +AllowShortFunctionsOnASingleLine: false +AllowShortLoopsOnASingleLine: true +BinPackArguments: true +BinPackParameters: true +ColumnLimit: 100 +CommentPragmas: NOLINT:.* +ContinuationIndentWidth: 8 +DerivePointerAlignment: false +IndentWidth: 4 +PointerAlignment: Left +TabWidth: 4 +AccessModifierOffset: -4 +IncludeCategories: + - Regex: '^"Log\.h"' + Priority: -1 diff --git a/apex/statsd/Android.bp b/apex/statsd/Android.bp new file mode 100644 index 000000000000..ede8852c5905 --- /dev/null +++ b/apex/statsd/Android.bp @@ -0,0 +1,83 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +apex { + name: "com.android.os.statsd", + defaults: ["com.android.os.statsd-defaults"], + manifest: "apex_manifest.json", +} + +apex_defaults { + jni_libs: [ + "libstats_jni", + ], + native_shared_libs: [ + "libstatspull", + "libstatssocket", + ], + binaries: ["statsd"], + java_libs: [ + "framework-statsd", + "service-statsd", + ], + compile_multilib: "both", + prebuilts: ["com.android.os.statsd.init.rc"], + name: "com.android.os.statsd-defaults", + updatable: true, + min_sdk_version: "R", + key: "com.android.os.statsd.key", + certificate: ":com.android.os.statsd.certificate", +} + +apex_key { + name: "com.android.os.statsd.key", + public_key: "com.android.os.statsd.avbpubkey", + private_key: "com.android.os.statsd.pem", +} + +android_app_certificate { + name: "com.android.os.statsd.certificate", + // This will use com.android.os.statsd.x509.pem (the cert) and + // com.android.os.statsd.pk8 (the private key) + certificate: "com.android.os.statsd", +} + +prebuilt_etc { + name: "com.android.os.statsd.init.rc", + src: "statsd.rc", + filename: "init.rc", + installable: false, +} + +// JNI library for StatsLog.write +cc_library_shared { + name: "libstats_jni", + srcs: ["jni/**/*.cpp"], + header_libs: ["libnativehelper_header_only"], + shared_libs: [ + "liblog", // Has a stable abi - should not be copied into apex. + "libstatssocket", + ], + stl: "libc++_static", + cflags: [ + "-Wall", + "-Werror", + "-Wextra", + "-Wno-unused-parameter", + ], + apex_available: [ + "com.android.os.statsd", + "test_com.android.os.statsd", + ], +} diff --git a/apex/statsd/OWNERS b/apex/statsd/OWNERS new file mode 100644 index 000000000000..bed9600bc955 --- /dev/null +++ b/apex/statsd/OWNERS @@ -0,0 +1,9 @@ +jeffreyhuang@google.com +joeo@google.com +jtnguyen@google.com +muhammadq@google.com +ruchirr@google.com +singhtejinder@google.com +tsaichristine@google.com +yaochen@google.com +yro@google.com
\ No newline at end of file diff --git a/apex/statsd/TEST_MAPPING b/apex/statsd/TEST_MAPPING new file mode 100644 index 000000000000..93f108707d9e --- /dev/null +++ b/apex/statsd/TEST_MAPPING @@ -0,0 +1,10 @@ +{ + "presubmit" : [ + { + "name" : "FrameworkStatsdTest" + }, + { + "name" : "LibStatsPullTests" + } + ] +} diff --git a/apex/statsd/aidl/Android.bp b/apex/statsd/aidl/Android.bp new file mode 100644 index 000000000000..04339e67d799 --- /dev/null +++ b/apex/statsd/aidl/Android.bp @@ -0,0 +1,51 @@ +// +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +filegroup { + name: "framework-statsd-aidl-sources", + srcs: ["**/*.aidl"], +} + +aidl_interface { + name: "statsd-aidl", + unstable: true, + srcs: [ + "android/os/IPendingIntentRef.aidl", + "android/os/IPullAtomCallback.aidl", + "android/os/IPullAtomResultReceiver.aidl", + "android/os/IStatsCompanionService.aidl", + "android/os/IStatsd.aidl", + "android/os/StatsDimensionsValueParcel.aidl", + "android/util/StatsEventParcel.aidl", + ], + backend: { + java: { + enabled: false, // framework-statsd and service-statsd use framework-statsd-aidl-sources + }, + cpp: { + enabled: false, + }, + ndk: { + enabled: true, + apex_available: [ + // TODO(b/145923087): Remove this once statsd binary is in apex + "//apex_available:platform", + + "com.android.os.statsd", + "test_com.android.os.statsd", + ], + }, + } +} diff --git a/apex/statsd/aidl/android/os/IPendingIntentRef.aidl b/apex/statsd/aidl/android/os/IPendingIntentRef.aidl new file mode 100644 index 000000000000..000a69992a49 --- /dev/null +++ b/apex/statsd/aidl/android/os/IPendingIntentRef.aidl @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.os.StatsDimensionsValueParcel; + +/** + * Binder interface to hold a PendingIntent for StatsCompanionService. + * {@hide} + */ +interface IPendingIntentRef { + + /** + * Sends a broadcast to the specified PendingIntent that it should getData now. + * This should be only called from StatsCompanionService. + */ + oneway void sendDataBroadcast(long lastReportTimeNs); + + /** + * Send a broadcast to the specified PendingIntent notifying it that the list of active configs + * has changed. This should be only called from StatsCompanionService. + */ + oneway void sendActiveConfigsChangedBroadcast(in long[] configIds); + + /** + * Send a broadcast to the specified PendingIntent, along with the other information + * specified. This should only be called from StatsCompanionService. + */ + oneway void sendSubscriberBroadcast(long configUid, long configId, long subscriptionId, + long subscriptionRuleId, in String[] cookies, + in StatsDimensionsValueParcel dimensionsValueParcel); +} diff --git a/apex/statsd/aidl/android/os/IPullAtomCallback.aidl b/apex/statsd/aidl/android/os/IPullAtomCallback.aidl new file mode 100644 index 000000000000..ff0b97bb5b84 --- /dev/null +++ b/apex/statsd/aidl/android/os/IPullAtomCallback.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.os.IPullAtomResultReceiver; + +/** + * Binder interface to pull atoms for the stats service. + * {@hide} + */ +interface IPullAtomCallback { + /** + * Initiate a request for a pull for an atom. + */ + oneway void onPullAtom(int atomTag, IPullAtomResultReceiver resultReceiver); + +} diff --git a/apex/statsd/aidl/android/os/IPullAtomResultReceiver.aidl b/apex/statsd/aidl/android/os/IPullAtomResultReceiver.aidl new file mode 100644 index 000000000000..00d026e25df3 --- /dev/null +++ b/apex/statsd/aidl/android/os/IPullAtomResultReceiver.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.util.StatsEventParcel; + +/** + * Binder interface to pull atoms for the stats service. + * {@hide} + */ +interface IPullAtomResultReceiver { + + /** + * Indicate that a pull request for an atom is complete. + */ + oneway void pullFinished(int atomTag, boolean success, in StatsEventParcel[] output); + +} diff --git a/apex/statsd/aidl/android/os/IStatsCompanionService.aidl b/apex/statsd/aidl/android/os/IStatsCompanionService.aidl new file mode 100644 index 000000000000..5cdb3249501b --- /dev/null +++ b/apex/statsd/aidl/android/os/IStatsCompanionService.aidl @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +/** + * Binder interface to communicate with the Java-based statistics service helper. + * {@hide} + */ +interface IStatsCompanionService { + /** + * Tell statscompanion that stastd is up and running. + */ + oneway void statsdReady(); + + /** + * Register an alarm for anomaly detection to fire at the given timestamp (ms since epoch). + * If anomaly alarm had already been registered, it will be replaced with the new timestamp. + * Uses AlarmManager.set API, so if the timestamp is in the past, alarm fires immediately, and + * alarm is inexact. + */ + oneway void setAnomalyAlarm(long timestampMs); + + /** Cancel any anomaly detection alarm. */ + oneway void cancelAnomalyAlarm(); + + /** + * Register a repeating alarm for pulling to fire at the given timestamp and every + * intervalMs thereafter (in ms since epoch). + * If polling alarm had already been registered, it will be replaced by new one. + * Uses AlarmManager.setRepeating API, so if the timestamp is in past, alarm fires immediately, + * and alarm is inexact. + */ + oneway void setPullingAlarm(long nextPullTimeMs); + + /** Cancel any repeating pulling alarm. */ + oneway void cancelPullingAlarm(); + + /** + * Register an alarm when we want to trigger subscribers at the given + * timestamp (in ms since epoch). + * If an alarm had already been registered, it will be replaced by new one. + */ + oneway void setAlarmForSubscriberTriggering(long timestampMs); + + /** Cancel any alarm for the purpose of subscriber triggering. */ + oneway void cancelAlarmForSubscriberTriggering(); + + /** + * Ask StatsCompanionService if the given permission is allowed for a particular process + * and user ID. statsd is incapable of doing this check itself because checkCallingPermission + * is not currently supported by libbinder_ndk. + */ + boolean checkPermission(String permission, int pid, int uid); +} diff --git a/apex/statsd/aidl/android/os/IStatsManagerService.aidl b/apex/statsd/aidl/android/os/IStatsManagerService.aidl new file mode 100644 index 000000000000..b59a97e25bd0 --- /dev/null +++ b/apex/statsd/aidl/android/os/IStatsManagerService.aidl @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.app.PendingIntent; +import android.os.IPullAtomCallback; + +/** + * Binder interface to communicate with the Java-based statistics service helper. + * Contains parcelable objects available only in Java. + * {@hide} + */ +interface IStatsManagerService { + + /** + * Registers the given pending intent for this config key. This intent is invoked when the + * memory consumed by the metrics for this configuration approach the pre-defined limits. There + * can be at most one listener per config key. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + void setDataFetchOperation(long configId, in PendingIntent pendingIntent, + in String packageName); + + /** + * Removes the data fetch operation for the specified configuration. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + void removeDataFetchOperation(long configId, in String packageName); + + /** + * Registers the given pending intent for this packagename. This intent is invoked when the + * active status of any of the configs sent by this package changes and will contain a list of + * config ids that are currently active. It also returns the list of configs that are currently + * active. There can be at most one active configs changed listener per package. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + long[] setActiveConfigsChangedOperation(in PendingIntent pendingIntent, in String packageName); + + /** + * Removes the active configs changed operation for the specified package name. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + void removeActiveConfigsChangedOperation(in String packageName); + + /** + * Set the PendingIntent to be used when broadcasting subscriber + * information to the given subscriberId within the given config. + * + * Suppose that the calling uid has added a config with key configKey, and that in this config + * it is specified that when a particular anomaly is detected, a broadcast should be sent to + * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with + * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast + * when the anomaly is detected. + * + * This function can only be called by the owner (uid) of the config. It must be called each + * time statsd starts. Later calls overwrite previous calls; only one PendingIntent is stored. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + void setBroadcastSubscriber(long configKey, long subscriberId, in PendingIntent pendingIntent, + in String packageName); + + /** + * Undoes setBroadcastSubscriber() for the (configKey, subscriberId) pair. + * Any broadcasts associated with subscriberId will henceforth not be sent. + * No-op if this (configKey, subscriberId) pair was not associated with an PendingIntent. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + void unsetBroadcastSubscriber(long configKey, long subscriberId, in String packageName); + + /** + * Returns the most recently registered experiment IDs. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + long[] getRegisteredExperimentIds(); + + /** + * Fetches metadata across statsd. Returns byte array representing wire-encoded proto. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + byte[] getMetadata(in String packageName); + + /** + * Fetches data for the specified configuration key. Returns a byte array representing proto + * wire-encoded of ConfigMetricsReportList. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + byte[] getData(in long key, in String packageName); + + /** + * Sets a configuration with the specified config id and subscribes to updates for this + * configuration id. Broadcasts will be sent if this configuration needs to be collected. + * The configuration must be a wire-encoded StatsdConfig. The receiver for this data is + * registered in a separate function. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + void addConfiguration(in long configId, in byte[] config, in String packageName); + + /** + * Removes the configuration with the matching config id. No-op if this config id does not + * exist. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + void removeConfiguration(in long configId, in String packageName); + + /** Tell StatsManagerService to register a puller for the given atom tag with statsd. */ + oneway void registerPullAtomCallback(int atomTag, long coolDownMillis, long timeoutMillis, + in int[] additiveFields, IPullAtomCallback pullerCallback); + + /** Tell StatsManagerService to unregister the pulller for the given atom tag from statsd. */ + oneway void unregisterPullAtomCallback(int atomTag); +} diff --git a/apex/statsd/aidl/android/os/IStatsd.aidl b/apex/statsd/aidl/android/os/IStatsd.aidl new file mode 100644 index 000000000000..0d3f4208a2ab --- /dev/null +++ b/apex/statsd/aidl/android/os/IStatsd.aidl @@ -0,0 +1,237 @@ +/** + * Copyright (c) 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.os.IPendingIntentRef; +import android.os.IPullAtomCallback; +import android.os.ParcelFileDescriptor; + +/** + * Binder interface to communicate with the statistics management service. + * {@hide} + */ +interface IStatsd { + /** + * Tell the stats daemon that the android system server is up and running. + */ + oneway void systemRunning(); + + /** + * Tell the stats daemon that the android system has finished booting. + */ + oneway void bootCompleted(); + + /** + * Tell the stats daemon that the StatsCompanionService is up and running. + * Two-way binder call so that caller knows message received. + */ + void statsCompanionReady(); + + /** + * Tells statsd that an anomaly may have occurred, so statsd can check whether this is so and + * act accordingly. + * Two-way binder call so that caller's method (and corresponding wakelocks) will linger. + */ + void informAnomalyAlarmFired(); + + /** + * Tells statsd that it is time to poll some stats. Statsd will be responsible for determing + * what stats to poll and initiating the polling. + * Two-way binder call so that caller's method (and corresponding wakelocks) will linger. + */ + void informPollAlarmFired(); + + /** + * Tells statsd that it is time to handle periodic alarms. Statsd will be responsible for + * determing what alarm subscriber to trigger. + * Two-way binder call so that caller's method (and corresponding wakelocks) will linger. + */ + void informAlarmForSubscriberTriggeringFired(); + + /** + * Tells statsd that the device is about to shutdown. + */ + void informDeviceShutdown(); + + /** + * Inform statsd about a file descriptor for a pipe through which we will pipe version + * and package information for each uid. + * Versions and package information are supplied via UidData proto where info for each app + * is captured in its own element of a repeated ApplicationInfo message. + */ + oneway void informAllUidData(in ParcelFileDescriptor fd); + + /** + * Inform statsd what the uid, version, version_string, and installer are for one app that was + * updated. + */ + oneway void informOnePackage(in String app, in int uid, in long version, + in String version_string, in String installer); + + /** + * Inform stats that an app was removed. + */ + oneway void informOnePackageRemoved(in String app, in int uid); + + /** + * Fetches data for the specified configuration key. Returns a byte array representing proto + * wire-encoded of ConfigMetricsReportList. + * + * Requires Manifest.permission.DUMP. + */ + byte[] getData(in long key, int callingUid); + + /** + * Fetches metadata across statsd. Returns byte array representing wire-encoded proto. + * + * Requires Manifest.permission.DUMP. + */ + byte[] getMetadata(); + + /** + * Sets a configuration with the specified config id and subscribes to updates for this + * configuration key. Broadcasts will be sent if this configuration needs to be collected. + * The configuration must be a wire-encoded StatsdConfig. The receiver for this data is + * registered in a separate function. + * + * Requires Manifest.permission.DUMP. + */ + void addConfiguration(in long configId, in byte[] config, in int callingUid); + + /** + * Registers the given pending intent for this config key. This intent is invoked when the + * memory consumed by the metrics for this configuration approach the pre-defined limits. There + * can be at most one listener per config key. + * + * Requires Manifest.permission.DUMP. + */ + void setDataFetchOperation(long configId, in IPendingIntentRef pendingIntentRef, + int callingUid); + + /** + * Removes the data fetch operation for the specified configuration. + * + * Requires Manifest.permission.DUMP. + */ + void removeDataFetchOperation(long configId, int callingUid); + + /** + * Registers the given pending intent for this packagename. This intent is invoked when the + * active status of any of the configs sent by this package changes and will contain a list of + * config ids that are currently active. It also returns the list of configs that are currently + * active. There can be at most one active configs changed listener per package. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + long[] setActiveConfigsChangedOperation(in IPendingIntentRef pendingIntentRef, int callingUid); + + /** + * Removes the active configs changed operation for the specified package name. + * + * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS. + */ + void removeActiveConfigsChangedOperation(int callingUid); + + /** + * Removes the configuration with the matching config id. No-op if this config id does not + * exist. + * + * Requires Manifest.permission.DUMP. + */ + void removeConfiguration(in long configId, in int callingUid); + + /** + * Set the PendingIntentRef to be used when broadcasting subscriber + * information to the given subscriberId within the given config. + * + * Suppose that the calling uid has added a config with key configId, and that in this config + * it is specified that when a particular anomaly is detected, a broadcast should be sent to + * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with + * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast + * when the anomaly is detected. + * + * This function can only be called by the owner (uid) of the config. It must be called each + * time statsd starts. Later calls overwrite previous calls; only one pendingIntent is stored. + * + * Requires Manifest.permission.DUMP. + */ + void setBroadcastSubscriber(long configId, long subscriberId, in IPendingIntentRef pir, + int callingUid); + + /** + * Undoes setBroadcastSubscriber() for the (configId, subscriberId) pair. + * Any broadcasts associated with subscriberId will henceforth not be sent. + * No-op if this (configKey, subscriberId) pair was not associated with an PendingIntentRef. + * + * Requires Manifest.permission.DUMP. + */ + void unsetBroadcastSubscriber(long configId, long subscriberId, int callingUid); + + /** + * Tell the stats daemon that all the pullers registered during boot have been sent. + */ + oneway void allPullersFromBootRegistered(); + + /** + * Registers a puller callback function that, when invoked, pulls the data + * for the specified atom tag. + */ + oneway void registerPullAtomCallback(int uid, int atomTag, long coolDownMillis, + long timeoutMillis,in int[] additiveFields, + IPullAtomCallback pullerCallback); + + /** + * Registers a puller callback function that, when invoked, pulls the data + * for the specified atom tag. + * + * Enforces the REGISTER_STATS_PULL_ATOM permission. + */ + oneway void registerNativePullAtomCallback(int atomTag, long coolDownMillis, long timeoutMillis, + in int[] additiveFields, IPullAtomCallback pullerCallback); + + /** + * Unregisters any pullAtomCallback for the given uid/atom. + */ + oneway void unregisterPullAtomCallback(int uid, int atomTag); + + /** + * Unregisters any pullAtomCallback for the given atom + caller. + * + * Enforces the REGISTER_STATS_PULL_ATOM permission. + */ + oneway void unregisterNativePullAtomCallback(int atomTag); + + /** + * The install requires staging. + */ + const int FLAG_REQUIRE_STAGING = 0x01; + + /** + * Rollback is enabled with this install. + */ + const int FLAG_ROLLBACK_ENABLED = 0x02; + + /** + * Requires low latency monitoring. + */ + const int FLAG_REQUIRE_LOW_LATENCY_MONITOR = 0x04; + + /** + * Returns the most recently registered experiment IDs. + */ + long[] getRegisteredExperimentIds(); +} diff --git a/apex/statsd/aidl/android/os/StatsDimensionsValueParcel.aidl b/apex/statsd/aidl/android/os/StatsDimensionsValueParcel.aidl new file mode 100644 index 000000000000..05f78d00348e --- /dev/null +++ b/apex/statsd/aidl/android/os/StatsDimensionsValueParcel.aidl @@ -0,0 +1,21 @@ +package android.os; + +/** + * @hide + */ +parcelable StatsDimensionsValueParcel { + // Field equals atomTag for top level StatsDimensionsValueParcels or + // positions in depth (1-indexed) for lower level parcels. + int field; + + // Indicator for which type of value is stored. Should be set to one + // of the constants in StatsDimensionsValue.java. + int valueType; + + String stringValue; + int intValue; + long longValue; + boolean boolValue; + float floatValue; + StatsDimensionsValueParcel[] tupleValue; +} diff --git a/apex/statsd/aidl/android/util/StatsEventParcel.aidl b/apex/statsd/aidl/android/util/StatsEventParcel.aidl new file mode 100644 index 000000000000..add8bfb47b1a --- /dev/null +++ b/apex/statsd/aidl/android/util/StatsEventParcel.aidl @@ -0,0 +1,8 @@ +package android.util; + +/** + * @hide + */ +parcelable StatsEventParcel { + byte[] buffer; +} diff --git a/apex/statsd/apex_manifest.json b/apex/statsd/apex_manifest.json new file mode 100644 index 000000000000..e2972e700880 --- /dev/null +++ b/apex/statsd/apex_manifest.json @@ -0,0 +1,5 @@ +{ + "name": "com.android.os.statsd", + "version": 300000000 +} + diff --git a/apex/statsd/com.android.os.statsd.avbpubkey b/apex/statsd/com.android.os.statsd.avbpubkey Binary files differnew file mode 100644 index 000000000000..d78af8b8bef2 --- /dev/null +++ b/apex/statsd/com.android.os.statsd.avbpubkey diff --git a/apex/statsd/com.android.os.statsd.pem b/apex/statsd/com.android.os.statsd.pem new file mode 100644 index 000000000000..558e17fd6864 --- /dev/null +++ b/apex/statsd/com.android.os.statsd.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEA893bbpkivKEiNgfknYBSlzC0csaKU/ddBm5Pb4ZFuab+LQSR +9DDc5JrsmxyrsrvuwL/zAtMbkyYWzEiUxJtx/w0bw8rC90GoPRSCmxyI0ZK8FuPy +IAQ7UeNfTWZ485mAUaTSasGIfQ3DY4F0P+aUSijeG3NUY02nALHDMqJX7lXR+mL1 +DUYDg05KB0jxQwlYqBeujTPPiAzEqm3PlBoHuan8/qgK2wdQMTVg/fieUD3lupmV +Wj2dRZgqfBPA16ZbV4Uo0j0bZSf+fQLiXlU2VJGb5i/FQfjLqMKGABDI0MgK7Sc2 +m4ySpV4g4XKDv/vw6Dw4kwWC7mATEVAkH+q6V7uiZeN6a7w30UMtPI8fPaUvAP3L +VBjCBIv/3m+CKkWcNxOZ3sQBQl5bS05dxcfiVsBvBLYbvQgC+Wy0Sc3b+1pXFT/E +uAsbZ4CyVsi1+PAdx3h5e2QAyNCXgZDOcvTUyxY6JLTE0LOVHmI4fJEujBex//Oz +PCRHvC8K+KiljyQWf/NYrLSD3QGYAjVMtQh7yu2yhzWzgBUxyhuv3rY4ATXsN3bJ +wW4w7/L/RSLSW5+lp/NoJOD9utbsKTyGMHOY6K8JLOmhv3ORoAEmLYlFTI+FqBi9 +AH1HQEKCyh8Z/bYHLUzGWl6FqAMtcnuintv40BbKyt0/D1ItdbSNKmOZ5rkCAwEA +AQKCAgAY7ll8mRNADYkd1Pi+UVwgMM6B3WJO6z8LZUOhtyxxqmzZ1VnGiShMBrqh +sPCsuSHTeswxQbvT81TpVZI/91RUKtbn0VbVSFUWyX4AtY4XPtUT0gHy2/vkh0Y6 +93ruDIdd0Wfhmh+GCV4sUhO8ZKpMWpk6XTQHYuzr2UCHcKlkqElrO6qpzLqXNe3D +iOWBYPc7WBB0RxO0aPnCIq/SCEc55/MBZdSWR80e+sILtNsagPl3djQaoanub3wI +a0yPv2YfMHHX7H9cfBY8WYsi8bs4MhqqEcAs2m6XtitU3mJpVcooLJYcmOZ1GYZr +BfYKLouWcnGmNi4IiLHqVzMaQDkEhAZsRaAXCkoVVrFBedLlmLPpiUIQlINF4vxe +3IcekTKWyMzkU6h+K8T15MU5mLSqeL2Gji1JIwKJno51FZ9uc++pUJVtfYQmNny8 +8RKvQ1hv/S5yLQKgN+VkNbaWlUoMP73dtUe3m/At71/2Dj7xB0KtcgT1lEMrM1GR +oynJAJLz/d0n5RUUREwkZZMcA4fQVC7Db6vpK69jPiQMShpZ3JKCEjfYLUuN0slt +FPhjiR175E0vTRuLoIj4kXNwLLswH0c9zqrKM2S92SCxAV3E4JJGKhUZalvT9s1g +LrPhMCl6CsOES98T87d3RyAIK0iVRCnRUG3bc+8rzyRd4fzkAQKCAQEA/UjmCSm3 +H46t/1w7YBZPew7SFQOAJe81iDzbonc3fNPD2R8lxtD3MwdvrQ5f9fhT4+uveWNr +dyBX7ppnissyM3LZRN+7BdeIVVeIPVen6Ou9W2i7q18ZoQx9IpRcZEw5tGJFZaGx +EmyPN4i1K0ccUkGbBvbXXQ/tcG3wElRpBAc5/TQ8vrpUgHll2/MbYhowx6P9uHv5 +thoyG98X+7Fbg8ikzw5GtyuedXfyX1CpJ7yUQVS2PEaOMXOkZdx2bbWRAYYCpsqB +dMmjs2PsFhZHu6CpLhlocHbfUiRztCUCaMZJPQXFSVmy8QDMvZEdVLvad9Poi8ny +lmHVRgxaNbAtIQKCAQEA9nscqRaaO7hIX9bOUxcDbI0486Ws4H0hAFApIN+6/LP4 +hkxey3xWArTYWrvSG1d5GkJAdn99ayWzo2PevmJlrhIJiO1QqYBAk+87cnhwSCmB +kb0sGkNWcc/xNRy7eqdhyCmVhaUnIbORee+cD6qiu/l2BAclTf2ZARFOGXjhQkvt +cDbc/9ZR467ceXbiTIU34Be4xnNAY1mo59jvwl9eqxgpefYTqPhcZ7OmlDli77Hd +XuRfuxLZCscv7A9M5Enc2zwOEP5VwRNwYzYtMm2Yh9CQZxNWC7JVh1Gw5MPFzsGl +sgEdb4WGneN6PPLQHK7NF0f7wYSNnF0i3XSME9MumQKCAQEA0qMbWydr+TyJC0LC +xigHtUkgAQXGPsXuePxTk4sdhBwAVcKHgg4qZi+a+gpoV4BLE9LfPU4nAwzM08to +rI5Lk2nBsnt1Z2hVItQGoy0QoK3b7fbti5ktETf3oRhMtcSGgLLxD5ImVjId8Isq +T3F15hpVOLdzZxtl1Qg4jKXSJ91yplYY5mzC9Yz/3qkQbsdlJcIFsLS5eG3UmkUw +Bsr6VmA4X1F6Eb6eqwYzdHz6D+fOS36NhxcODaYkY+myO46xptixv8/NVTiTgQ5q +OfwRb8Iur/3FUzIoioFyD7Bvjn7ITY1NArEsFS0bF9Nk1yDakKiUThyGN/Xojbac +FuYKwQKCAQEAxOWJ+qU8phJLdowBHC0ZJiEWasRhep9auoZOpJ01IWOfV6EwZLs5 +dkYDQ1Agwoi5DDn6hu7HQM3IV/CS4mF2OnzcMw7ozc7PR53nTkVZ5LuLbuHAlmZO +avKjDDucpJmLqjtV34IT5X8t6kt3zqgQAbuBBCy1Jz07ebfaPMzsnWpMDcU1/AW4 +OvrX0wweMOSGwzQP/i/ZMsRQAo2w0gQfeuv9Thk+kU99ebXwjx3co//hCEnFE4s1 +6L8/0AJU+VTr4hJyZi7WUDt4HzkLF+qm22/Hux+eMA/Q9R1UAxtFLCpTdAQiAJGY +/Q3X+1I434DgAwYU3f1Gpq9cB65vq/KamQKCAQEAjIub5wde/ttHlLALvnOXrbqe +nUIfWHExMzhul/rkr8fFEJwij2nZUuN2EWUGzBWQQoNXw5QKHLZyPsyFUOa/P2BS +osnffAa+sumL4k36E71xFdTVV5ExyTXZVB49sPmUpivP9gEucFFqDHKjGsF45dBF ++DZdykLUIv+/jQUzXGkZ5Wv/r52YUNho4EZdwnlJ2so7cxnsYnjW+c1nlp17tkq5 +DfwktkeD9iFzlaZ66vLoO44luaBm+lC3xM2sHinOTwbk0gvhJAIoLfkOYhpmGc8A +4W/E1OHfVz6xqVDsMBFhRbQpHNkf8XZNqkIoqHVMTaMOJJlM+lb0+A9B8Bm/XA== +-----END RSA PRIVATE KEY----- diff --git a/apex/statsd/com.android.os.statsd.pk8 b/apex/statsd/com.android.os.statsd.pk8 Binary files differnew file mode 100644 index 000000000000..49910f80a05c --- /dev/null +++ b/apex/statsd/com.android.os.statsd.pk8 diff --git a/apex/statsd/com.android.os.statsd.x509.pem b/apex/statsd/com.android.os.statsd.x509.pem new file mode 100644 index 000000000000..e7b16b2048cb --- /dev/null +++ b/apex/statsd/com.android.os.statsd.x509.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFDTCCAvWgAwIBAgIUCnta1LAl5fMMLLQx//4zWz9A2A8wDQYJKoZIhvcNAQEL +BQAwFTETMBEGA1UECgwKR29vZ2xlIExMQzAgFw0xOTA4MTIyMjM5MzBaGA80NzU3 +MDcwODIyMzkzMFowFTETMBEGA1UECgwKR29vZ2xlIExMQzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAOranWZ19jkXCF9WIlXv01tUUvLKMHWKV7X9Earw +cL7/aax0pFbNJutgyBUiOszbR+0T7quZxz6jACu+6y7iaMJnvMluZsfTi+p2UvQt +y6Ql7ZUOQ7bVluCFIW5hZ+8d9RrLmZdvX1r4YfF6HufDBkAbj+os+y6407OezJAV +8EATpemc9gsCC4RJZpwzTs1RUXMD4UoNrLZAE8+7iaJZeBxmz0MAPj92pYc9M7/d +xInzYvOR08/uEpHt8jlMdVgSQS/FaRlIOIqcGBk3cjkjDlpVATQ4Hyjy+IPQPjTD +bJUmDJiYeBCyY/pYZQvTQjl8s+fvykTsF9Lfb+E+PhZ0+N8pRi7sUSpisZHSiqaN +W3oxYWc0YQSuzygHHog8HH/azHX5L805g/+Rwfb/cUF9eJgjq0vrkFnsz4UKgKNV +hHL90mfqpbc2UvJ8VY8BvIjbsHQ77LrBKlqI9VMPorttpTOuwHHJPKsyN972F0Ul +lRB6CwFE8csVGWXoNaDZWBv7xTDdbdirmlKDNueg9pw6ksYV2Is9Dv8PxmsZvb+4 +oftC/hb4X1Pudn01PPs9Tx44CwHuVLENUwlDEVzG5zNetsv9kAuCYt3VRVF+NYqj +NAfLbxCKLe25wGzJrZUEJ1YrYIjpUbfwnttEad/9Pu13DAS7HZwn5vwqEKB/1LlT +NSUXAgMBAAGjUzBRMB0GA1UdDgQWBBSKElkhJSbzgh8+iysye8SrkmJ62DAfBgNV +HSMEGDAWgBSKElkhJSbzgh8+iysye8SrkmJ62DAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQANFGnc2wJBrFbh2nzhl06g4TjPKGRCw365vZ1A3T9O +jXP0lToHDxB33TpKk6d7zszR1uPphQQxgzhSVZB/jx8q4kWSSoKoF9Dlx7h8rAt+ +2TM5DaBvxrwu5mqOALwQuF81wap1Pl2L2fFHvygCm8b+Ci4iS5vcr0axNnp1rK1b +vUtRWY4mfxTjJYcgeCVUGskqTb+cCxQZ6Icno6VTKajT1FybRmD3KZJaUuLbNEN+ +IE4nGTMG2WZ5Hl2vR8JJp1sYYn8T3ElMAb0MSNFkqsfI+tToEwGsuJDgYEdtEnzf +lTycQvn5NhrIZRRN3pqSyWpAU7p9mmyTK0PHMz2D/Rtfb7lE692vXzxCmZND51mc +YXCCoanV6eZZ7Sbqzh60+5QV38hgFBst5l8CcFaWWSFK9nBWdzS5lhs9lmQ4aiYd +IE0qsNZgMob+TTP1VW39hu4EDjNmOrKfimM9J2tcPZ5QP01DgETPvAsB7vn2Xz9J +HGt5ntiSV4W2izDP8viQ1M5NvfdBaUhcnNsE6/sxfU0USRs2hrEp1oiqrv4p6V0P +qOt7C2/YtJzkrxfsHZAxBUSRHa7LwtzgeiJDUivHn94VnAzSAH8MLx6CzDPQ8HWN +NiZFxTKfMVyjEmbQ2PalHWB8pWtpdEh7X4rzaqhnLBTis3pGssASgo3ArLIYleAU ++g== +-----END CERTIFICATE----- diff --git a/apex/statsd/framework/Android.bp b/apex/statsd/framework/Android.bp new file mode 100644 index 000000000000..bf4323ddfb0b --- /dev/null +++ b/apex/statsd/framework/Android.bp @@ -0,0 +1,81 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_visibility: [ ":__pkg__" ] +} + +genrule { + name: "statslog-statsd-java-gen", + tools: ["stats-log-api-gen"], + cmd: "$(location stats-log-api-gen) --java $(out) --module statsd" + + " --javaPackage com.android.internal.statsd --javaClass StatsdStatsLog", + out: ["com/android/internal/statsd/StatsdStatsLog.java"], +} + +java_library_static { + name: "statslog-statsd", + srcs: [ + ":statslog-statsd-java-gen", + ], + visibility: [ + "//cts/hostsidetests/statsd/apps:__subpackages__", + "//vendor:__subpackages__", + ], +} + +filegroup { + name: "framework-statsd-sources", + srcs: [ + "java/**/*.java", + ":framework-statsd-aidl-sources", + ":statslog-statsd-java-gen", + ], + visibility: [ + "//frameworks/base", // For the "global" stubs. + "//frameworks/base/apex/statsd:__subpackages__", + ], +} +java_sdk_library { + name: "framework-statsd", + defaults: ["framework-module-defaults"], + installable: true, + + srcs: [ + ":framework-statsd-sources", + ], + + permitted_packages: [ + "android.app", + "android.os", + "android.util", + // From :statslog-statsd-java-gen + "com.android.internal.statsd", + ], + + api_packages: [ + "android.app", + "android.os", + "android.util", + ], + + hostdex: true, // for hiddenapi check + + impl_library_visibility: ["//frameworks/base/apex/statsd/framework/test:__subpackages__"], + + apex_available: [ + "com.android.os.statsd", + "test_com.android.os.statsd", + ], +} diff --git a/apex/statsd/framework/api/current.txt b/apex/statsd/framework/api/current.txt new file mode 100644 index 000000000000..a65569347e7d --- /dev/null +++ b/apex/statsd/framework/api/current.txt @@ -0,0 +1,12 @@ +// Signature format: 2.0 +package android.util { + + public final class StatsLog { + method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public static boolean logBinaryPushStateChanged(@NonNull String, long, int, int, @NonNull long[]); + method public static boolean logEvent(int); + method public static boolean logStart(int); + method public static boolean logStop(int); + } + +} + diff --git a/apex/statsd/framework/api/module-lib-current.txt b/apex/statsd/framework/api/module-lib-current.txt new file mode 100644 index 000000000000..8b6e2170002e --- /dev/null +++ b/apex/statsd/framework/api/module-lib-current.txt @@ -0,0 +1,10 @@ +// Signature format: 2.0 +package android.os { + + public class StatsFrameworkInitializer { + method public static void registerServiceWrappers(); + method public static void setStatsServiceManager(@NonNull android.os.StatsServiceManager); + } + +} + diff --git a/apex/statsd/framework/api/module-lib-removed.txt b/apex/statsd/framework/api/module-lib-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/statsd/framework/api/module-lib-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/statsd/framework/api/removed.txt b/apex/statsd/framework/api/removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/statsd/framework/api/removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/statsd/framework/api/system-current.txt b/apex/statsd/framework/api/system-current.txt new file mode 100644 index 000000000000..3ea572450c1c --- /dev/null +++ b/apex/statsd/framework/api/system-current.txt @@ -0,0 +1,111 @@ +// Signature format: 2.0 +package android.app { + + public final class StatsManager { + method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public void addConfig(long, byte[]) throws android.app.StatsManager.StatsUnavailableException; + method @Deprecated @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public boolean addConfiguration(long, byte[]); + method @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM) public void clearPullAtomCallback(int); + method @Deprecated @Nullable @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public byte[] getData(long); + method @Deprecated @Nullable @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public byte[] getMetadata(); + method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public long[] getRegisteredExperimentIds() throws android.app.StatsManager.StatsUnavailableException; + method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public byte[] getReports(long) throws android.app.StatsManager.StatsUnavailableException; + method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public byte[] getStatsMetadata() throws android.app.StatsManager.StatsUnavailableException; + method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public void removeConfig(long) throws android.app.StatsManager.StatsUnavailableException; + method @Deprecated @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public boolean removeConfiguration(long); + method @NonNull @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public long[] setActiveConfigsChangedOperation(@Nullable android.app.PendingIntent) throws android.app.StatsManager.StatsUnavailableException; + method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public void setBroadcastSubscriber(android.app.PendingIntent, long, long) throws android.app.StatsManager.StatsUnavailableException; + method @Deprecated @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public boolean setBroadcastSubscriber(long, long, android.app.PendingIntent); + method @Deprecated @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public boolean setDataFetchOperation(long, android.app.PendingIntent); + method @RequiresPermission(allOf={android.Manifest.permission.DUMP, android.Manifest.permission.PACKAGE_USAGE_STATS}) public void setFetchReportsOperation(android.app.PendingIntent, long) throws android.app.StatsManager.StatsUnavailableException; + method @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM) public void setPullAtomCallback(int, @Nullable android.app.StatsManager.PullAtomMetadata, @NonNull java.util.concurrent.Executor, @NonNull android.app.StatsManager.StatsPullAtomCallback); + field public static final String ACTION_STATSD_STARTED = "android.app.action.STATSD_STARTED"; + field public static final String EXTRA_STATS_ACTIVE_CONFIG_KEYS = "android.app.extra.STATS_ACTIVE_CONFIG_KEYS"; + field public static final String EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES = "android.app.extra.STATS_BROADCAST_SUBSCRIBER_COOKIES"; + field public static final String EXTRA_STATS_CONFIG_KEY = "android.app.extra.STATS_CONFIG_KEY"; + field public static final String EXTRA_STATS_CONFIG_UID = "android.app.extra.STATS_CONFIG_UID"; + field public static final String EXTRA_STATS_DIMENSIONS_VALUE = "android.app.extra.STATS_DIMENSIONS_VALUE"; + field public static final String EXTRA_STATS_SUBSCRIPTION_ID = "android.app.extra.STATS_SUBSCRIPTION_ID"; + field public static final String EXTRA_STATS_SUBSCRIPTION_RULE_ID = "android.app.extra.STATS_SUBSCRIPTION_RULE_ID"; + field public static final int PULL_SKIP = 1; // 0x1 + field public static final int PULL_SUCCESS = 0; // 0x0 + } + + public static class StatsManager.PullAtomMetadata { + method @Nullable public int[] getAdditiveFields(); + method public long getCoolDownMillis(); + method public long getTimeoutMillis(); + } + + public static class StatsManager.PullAtomMetadata.Builder { + ctor public StatsManager.PullAtomMetadata.Builder(); + method @NonNull public android.app.StatsManager.PullAtomMetadata build(); + method @NonNull public android.app.StatsManager.PullAtomMetadata.Builder setAdditiveFields(@NonNull int[]); + method @NonNull public android.app.StatsManager.PullAtomMetadata.Builder setCoolDownMillis(long); + method @NonNull public android.app.StatsManager.PullAtomMetadata.Builder setTimeoutMillis(long); + } + + public static interface StatsManager.StatsPullAtomCallback { + method public int onPullAtom(int, @NonNull java.util.List<android.util.StatsEvent>); + } + + public static class StatsManager.StatsUnavailableException extends android.util.AndroidException { + ctor public StatsManager.StatsUnavailableException(String); + ctor public StatsManager.StatsUnavailableException(String, Throwable); + } + +} + +package android.os { + + public final class StatsDimensionsValue implements android.os.Parcelable { + method public int describeContents(); + method public boolean getBooleanValue(); + method public int getField(); + method public float getFloatValue(); + method public int getIntValue(); + method public long getLongValue(); + method public String getStringValue(); + method public java.util.List<android.os.StatsDimensionsValue> getTupleValueList(); + method public int getValueType(); + method public boolean isValueType(int); + method public void writeToParcel(android.os.Parcel, int); + field public static final int BOOLEAN_VALUE_TYPE = 5; // 0x5 + field @NonNull public static final android.os.Parcelable.Creator<android.os.StatsDimensionsValue> CREATOR; + field public static final int FLOAT_VALUE_TYPE = 6; // 0x6 + field public static final int INT_VALUE_TYPE = 3; // 0x3 + field public static final int LONG_VALUE_TYPE = 4; // 0x4 + field public static final int STRING_VALUE_TYPE = 2; // 0x2 + field public static final int TUPLE_VALUE_TYPE = 7; // 0x7 + } + +} + +package android.util { + + public final class StatsEvent { + method @NonNull public static android.util.StatsEvent.Builder newBuilder(); + } + + public static final class StatsEvent.Builder { + method @NonNull public android.util.StatsEvent.Builder addBooleanAnnotation(byte, boolean); + method @NonNull public android.util.StatsEvent.Builder addIntAnnotation(byte, int); + method @NonNull public android.util.StatsEvent build(); + method @NonNull public android.util.StatsEvent.Builder setAtomId(int); + method @NonNull public android.util.StatsEvent.Builder usePooledBuffer(); + method @NonNull public android.util.StatsEvent.Builder writeAttributionChain(@NonNull int[], @NonNull String[]); + method @NonNull public android.util.StatsEvent.Builder writeBoolean(boolean); + method @NonNull public android.util.StatsEvent.Builder writeByteArray(@NonNull byte[]); + method @NonNull public android.util.StatsEvent.Builder writeFloat(float); + method @NonNull public android.util.StatsEvent.Builder writeInt(int); + method @NonNull public android.util.StatsEvent.Builder writeKeyValuePairs(@Nullable android.util.SparseIntArray, @Nullable android.util.SparseLongArray, @Nullable android.util.SparseArray<java.lang.String>, @Nullable android.util.SparseArray<java.lang.Float>); + method @NonNull public android.util.StatsEvent.Builder writeLong(long); + method @NonNull public android.util.StatsEvent.Builder writeString(@NonNull String); + } + + public final class StatsLog { + method public static void write(@NonNull android.util.StatsEvent); + method public static void writeRaw(@NonNull byte[], int); + } + +} + diff --git a/apex/statsd/framework/api/system-removed.txt b/apex/statsd/framework/api/system-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/apex/statsd/framework/api/system-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apex/statsd/framework/java/android/app/StatsManager.java b/apex/statsd/framework/java/android/app/StatsManager.java new file mode 100644 index 000000000000..a7d20572ca96 --- /dev/null +++ b/apex/statsd/framework/java/android/app/StatsManager.java @@ -0,0 +1,725 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app; + +import static android.Manifest.permission.DUMP; +import static android.Manifest.permission.PACKAGE_USAGE_STATS; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.content.Context; +import android.os.Binder; +import android.os.IPullAtomCallback; +import android.os.IPullAtomResultReceiver; +import android.os.IStatsManagerService; +import android.os.RemoteException; +import android.os.StatsFrameworkInitializer; +import android.util.AndroidException; +import android.util.Log; +import android.util.StatsEvent; +import android.util.StatsEventParcel; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * API for statsd clients to send configurations and retrieve data. + * + * @hide + */ +@SystemApi +public final class StatsManager { + private static final String TAG = "StatsManager"; + private static final boolean DEBUG = false; + + private static final Object sLock = new Object(); + private final Context mContext; + + @GuardedBy("sLock") + private IStatsManagerService mStatsManagerService; + + /** + * Long extra of uid that added the relevant stats config. + */ + public static final String EXTRA_STATS_CONFIG_UID = "android.app.extra.STATS_CONFIG_UID"; + /** + * Long extra of the relevant stats config's configKey. + */ + public static final String EXTRA_STATS_CONFIG_KEY = "android.app.extra.STATS_CONFIG_KEY"; + /** + * Long extra of the relevant statsd_config.proto's Subscription.id. + */ + public static final String EXTRA_STATS_SUBSCRIPTION_ID = + "android.app.extra.STATS_SUBSCRIPTION_ID"; + /** + * Long extra of the relevant statsd_config.proto's Subscription.rule_id. + */ + public static final String EXTRA_STATS_SUBSCRIPTION_RULE_ID = + "android.app.extra.STATS_SUBSCRIPTION_RULE_ID"; + /** + * List<String> of the relevant statsd_config.proto's BroadcastSubscriberDetails.cookie. + * Obtain using {@link android.content.Intent#getStringArrayListExtra(String)}. + */ + public static final String EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES = + "android.app.extra.STATS_BROADCAST_SUBSCRIBER_COOKIES"; + /** + * Extra of a {@link android.os.StatsDimensionsValue} representing sliced dimension value + * information. + */ + public static final String EXTRA_STATS_DIMENSIONS_VALUE = + "android.app.extra.STATS_DIMENSIONS_VALUE"; + /** + * Long array extra of the active configs for the uid that added those configs. + */ + public static final String EXTRA_STATS_ACTIVE_CONFIG_KEYS = + "android.app.extra.STATS_ACTIVE_CONFIG_KEYS"; + + /** + * Broadcast Action: Statsd has started. + * Configurations and PendingIntents can now be sent to it. + */ + public static final String ACTION_STATSD_STARTED = "android.app.action.STATSD_STARTED"; + + // Pull atom callback return codes. + /** + * Value indicating that this pull was successful and that the result should be used. + * + **/ + public static final int PULL_SUCCESS = 0; + + /** + * Value indicating that this pull was unsuccessful and that the result should not be used. + **/ + public static final int PULL_SKIP = 1; + + /** + * @hide + **/ + @VisibleForTesting public static final long DEFAULT_COOL_DOWN_MILLIS = 1_000L; // 1 second. + + /** + * @hide + **/ + @VisibleForTesting public static final long DEFAULT_TIMEOUT_MILLIS = 2_000L; // 2 seconds. + + /** + * Constructor for StatsManagerClient. + * + * @hide + */ + public StatsManager(Context context) { + mContext = context; + } + + /** + * Adds the given configuration and associates it with the given configKey. If a config with the + * given configKey already exists for the caller's uid, it is replaced with the new one. + * + * @param configKey An arbitrary integer that allows clients to track the configuration. + * @param config Wire-encoded StatsdConfig proto that specifies metrics (and all + * dependencies eg, conditions and matchers). + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service + * @throws IllegalArgumentException if config is not a wire-encoded StatsdConfig proto + */ + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public void addConfig(long configKey, byte[] config) throws StatsUnavailableException { + synchronized (sLock) { + try { + IStatsManagerService service = getIStatsManagerServiceLocked(); + // can throw IllegalArgumentException + service.addConfiguration(configKey, config, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "Failed to connect to statsmanager when adding configuration"); + throw new StatsUnavailableException("could not connect", e); + } catch (SecurityException e) { + throw new StatsUnavailableException(e.getMessage(), e); + } catch (IllegalStateException e) { + Log.e(TAG, "Failed to addConfig in statsmanager"); + throw new StatsUnavailableException(e.getMessage(), e); + } + } + } + + // TODO: Temporary for backwards compatibility. Remove. + /** + * @deprecated Use {@link #addConfig(long, byte[])} + */ + @Deprecated + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public boolean addConfiguration(long configKey, byte[] config) { + try { + addConfig(configKey, config); + return true; + } catch (StatsUnavailableException | IllegalArgumentException e) { + return false; + } + } + + /** + * Remove a configuration from logging. + * + * @param configKey Configuration key to remove. + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service + */ + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public void removeConfig(long configKey) throws StatsUnavailableException { + synchronized (sLock) { + try { + IStatsManagerService service = getIStatsManagerServiceLocked(); + service.removeConfiguration(configKey, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "Failed to connect to statsmanager when removing configuration"); + throw new StatsUnavailableException("could not connect", e); + } catch (SecurityException e) { + throw new StatsUnavailableException(e.getMessage(), e); + } catch (IllegalStateException e) { + Log.e(TAG, "Failed to removeConfig in statsmanager"); + throw new StatsUnavailableException(e.getMessage(), e); + } + } + } + + // TODO: Temporary for backwards compatibility. Remove. + /** + * @deprecated Use {@link #removeConfig(long)} + */ + @Deprecated + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public boolean removeConfiguration(long configKey) { + try { + removeConfig(configKey); + return true; + } catch (StatsUnavailableException e) { + return false; + } + } + + /** + * Set the PendingIntent to be used when broadcasting subscriber information to the given + * subscriberId within the given config. + * <p> + * Suppose that the calling uid has added a config with key configKey, and that in this config + * it is specified that when a particular anomaly is detected, a broadcast should be sent to + * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with + * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast + * when the anomaly is detected. + * <p> + * When statsd sends the broadcast, the PendingIntent will used to send an intent with + * information of + * {@link #EXTRA_STATS_CONFIG_UID}, + * {@link #EXTRA_STATS_CONFIG_KEY}, + * {@link #EXTRA_STATS_SUBSCRIPTION_ID}, + * {@link #EXTRA_STATS_SUBSCRIPTION_RULE_ID}, + * {@link #EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES}, and + * {@link #EXTRA_STATS_DIMENSIONS_VALUE}. + * <p> + * This function can only be called by the owner (uid) of the config. It must be called each + * time statsd starts. The config must have been added first (via {@link #addConfig}). + * + * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber + * associated with the given subscriberId. May be null, in which case + * it undoes any previous setting of this subscriberId. + * @param configKey The integer naming the config to which this subscriber is attached. + * @param subscriberId ID of the subscriber, as used in the config. + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service + */ + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public void setBroadcastSubscriber( + PendingIntent pendingIntent, long configKey, long subscriberId) + throws StatsUnavailableException { + synchronized (sLock) { + try { + IStatsManagerService service = getIStatsManagerServiceLocked(); + if (pendingIntent != null) { + service.setBroadcastSubscriber(configKey, subscriberId, pendingIntent, + mContext.getOpPackageName()); + } else { + service.unsetBroadcastSubscriber(configKey, subscriberId, + mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to connect to statsmanager when adding broadcast subscriber", + e); + throw new StatsUnavailableException("could not connect", e); + } catch (SecurityException e) { + throw new StatsUnavailableException(e.getMessage(), e); + } + } + } + + // TODO: Temporary for backwards compatibility. Remove. + /** + * @deprecated Use {@link #setBroadcastSubscriber(PendingIntent, long, long)} + */ + @Deprecated + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public boolean setBroadcastSubscriber( + long configKey, long subscriberId, PendingIntent pendingIntent) { + try { + setBroadcastSubscriber(pendingIntent, configKey, subscriberId); + return true; + } catch (StatsUnavailableException e) { + return false; + } + } + + /** + * Registers the operation that is called to retrieve the metrics data. This must be called + * each time statsd starts. The config must have been added first (via {@link #addConfig}, + * although addConfig could have been called on a previous boot). This operation allows + * statsd to send metrics data whenever statsd determines that the metrics in memory are + * approaching the memory limits. The fetch operation should call {@link #getReports} to fetch + * the data, which also deletes the retrieved metrics from statsd's memory. + * + * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber + * associated with the given subscriberId. May be null, in which case + * it removes any associated pending intent with this configKey. + * @param configKey The integer naming the config to which this operation is attached. + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service + */ + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public void setFetchReportsOperation(PendingIntent pendingIntent, long configKey) + throws StatsUnavailableException { + synchronized (sLock) { + try { + IStatsManagerService service = getIStatsManagerServiceLocked(); + if (pendingIntent == null) { + service.removeDataFetchOperation(configKey, mContext.getOpPackageName()); + } else { + service.setDataFetchOperation(configKey, pendingIntent, + mContext.getOpPackageName()); + } + + } catch (RemoteException e) { + Log.e(TAG, "Failed to connect to statsmanager when registering data listener."); + throw new StatsUnavailableException("could not connect", e); + } catch (SecurityException e) { + throw new StatsUnavailableException(e.getMessage(), e); + } + } + } + + /** + * Registers the operation that is called whenever there is a change in which configs are + * active. This must be called each time statsd starts. This operation allows + * statsd to inform clients that they should pull data of the configs that are currently + * active. The activeConfigsChangedOperation should set periodic alarms to pull data of configs + * that are active and stop pulling data of configs that are no longer active. + * + * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber + * associated with the given subscriberId. May be null, in which case + * it removes any associated pending intent for this client. + * @return A list of configs that are currently active for this client. If the pendingIntent is + * null, this will be an empty list. + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service + */ + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public @NonNull long[] setActiveConfigsChangedOperation(@Nullable PendingIntent pendingIntent) + throws StatsUnavailableException { + synchronized (sLock) { + try { + IStatsManagerService service = getIStatsManagerServiceLocked(); + if (pendingIntent == null) { + service.removeActiveConfigsChangedOperation(mContext.getOpPackageName()); + return new long[0]; + } else { + return service.setActiveConfigsChangedOperation(pendingIntent, + mContext.getOpPackageName()); + } + + } catch (RemoteException e) { + Log.e(TAG, "Failed to connect to statsmanager " + + "when registering active configs listener."); + throw new StatsUnavailableException("could not connect", e); + } catch (SecurityException e) { + throw new StatsUnavailableException(e.getMessage(), e); + } + } + } + + // TODO: Temporary for backwards compatibility. Remove. + /** + * @deprecated Use {@link #setFetchReportsOperation(PendingIntent, long)} + */ + @Deprecated + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) { + try { + setFetchReportsOperation(pendingIntent, configKey); + return true; + } catch (StatsUnavailableException e) { + return false; + } + } + + /** + * Request the data collected for the given configKey. + * This getter is destructive - it also clears the retrieved metrics from statsd's memory. + * + * @param configKey Configuration key to retrieve data from. + * @return Serialized ConfigMetricsReportList proto. + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service + */ + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public byte[] getReports(long configKey) throws StatsUnavailableException { + synchronized (sLock) { + try { + IStatsManagerService service = getIStatsManagerServiceLocked(); + return service.getData(configKey, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "Failed to connect to statsmanager when getting data"); + throw new StatsUnavailableException("could not connect", e); + } catch (SecurityException e) { + throw new StatsUnavailableException(e.getMessage(), e); + } catch (IllegalStateException e) { + Log.e(TAG, "Failed to getReports in statsmanager"); + throw new StatsUnavailableException(e.getMessage(), e); + } + } + } + + // TODO: Temporary for backwards compatibility. Remove. + /** + * @deprecated Use {@link #getReports(long)} + */ + @Deprecated + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public @Nullable byte[] getData(long configKey) { + try { + return getReports(configKey); + } catch (StatsUnavailableException e) { + return null; + } + } + + /** + * Clients can request metadata for statsd. Will contain stats across all configurations but not + * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}. + * This getter is not destructive and will not reset any metrics/counters. + * + * @return Serialized StatsdStatsReport proto. + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service + */ + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public byte[] getStatsMetadata() throws StatsUnavailableException { + synchronized (sLock) { + try { + IStatsManagerService service = getIStatsManagerServiceLocked(); + return service.getMetadata(mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "Failed to connect to statsmanager when getting metadata"); + throw new StatsUnavailableException("could not connect", e); + } catch (SecurityException e) { + throw new StatsUnavailableException(e.getMessage(), e); + } catch (IllegalStateException e) { + Log.e(TAG, "Failed to getStatsMetadata in statsmanager"); + throw new StatsUnavailableException(e.getMessage(), e); + } + } + } + + // TODO: Temporary for backwards compatibility. Remove. + /** + * @deprecated Use {@link #getStatsMetadata()} + */ + @Deprecated + @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS }) + public @Nullable byte[] getMetadata() { + try { + return getStatsMetadata(); + } catch (StatsUnavailableException e) { + return null; + } + } + + /** + * Returns the experiments IDs registered with statsd, or an empty array if there aren't any. + * + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service + */ + @RequiresPermission(allOf = {DUMP, PACKAGE_USAGE_STATS}) + public long[] getRegisteredExperimentIds() + throws StatsUnavailableException { + synchronized (sLock) { + try { + IStatsManagerService service = getIStatsManagerServiceLocked(); + return service.getRegisteredExperimentIds(); + } catch (RemoteException e) { + if (DEBUG) { + Log.d(TAG, + "Failed to connect to StatsManagerService when getting " + + "registered experiment IDs"); + } + throw new StatsUnavailableException("could not connect", e); + } catch (IllegalStateException e) { + Log.e(TAG, "Failed to getRegisteredExperimentIds in statsmanager"); + throw new StatsUnavailableException(e.getMessage(), e); + } + } + } + + /** + * Sets a callback for an atom when that atom is to be pulled. The stats service will + * invoke pullData in the callback when the stats service determines that this atom needs to be + * pulled. This method should not be called by third-party apps. + * + * @param atomTag The tag of the atom for this puller callback. + * @param metadata Optional metadata specifying the timeout, cool down time, and + * additive fields for mapping isolated to host uids. + * @param executor The executor in which to run the callback. + * @param callback The callback to be invoked when the stats service pulls the atom. + * + */ + @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM) + public void setPullAtomCallback(int atomTag, @Nullable PullAtomMetadata metadata, + @NonNull @CallbackExecutor Executor executor, + @NonNull StatsPullAtomCallback callback) { + long coolDownMillis = + metadata == null ? DEFAULT_COOL_DOWN_MILLIS : metadata.mCoolDownMillis; + long timeoutMillis = metadata == null ? DEFAULT_TIMEOUT_MILLIS : metadata.mTimeoutMillis; + int[] additiveFields = metadata == null ? new int[0] : metadata.mAdditiveFields; + if (additiveFields == null) { + additiveFields = new int[0]; + } + + synchronized (sLock) { + try { + IStatsManagerService service = getIStatsManagerServiceLocked(); + PullAtomCallbackInternal rec = + new PullAtomCallbackInternal(atomTag, callback, executor); + service.registerPullAtomCallback( + atomTag, coolDownMillis, timeoutMillis, additiveFields, rec); + } catch (RemoteException e) { + throw new RuntimeException("Unable to register pull callback", e); + } + } + } + + /** + * Clears a callback for an atom when that atom is to be pulled. Note that any ongoing + * pulls will still occur. This method should not be called by third-party apps. + * + * @param atomTag The tag of the atom of which to unregister + * + */ + @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM) + public void clearPullAtomCallback(int atomTag) { + synchronized (sLock) { + try { + IStatsManagerService service = getIStatsManagerServiceLocked(); + service.unregisterPullAtomCallback(atomTag); + } catch (RemoteException e) { + throw new RuntimeException("Unable to unregister pull atom callback"); + } + } + } + + private static class PullAtomCallbackInternal extends IPullAtomCallback.Stub { + public final int mAtomId; + public final StatsPullAtomCallback mCallback; + public final Executor mExecutor; + + PullAtomCallbackInternal(int atomId, StatsPullAtomCallback callback, Executor executor) { + mAtomId = atomId; + mCallback = callback; + mExecutor = executor; + } + + @Override + public void onPullAtom(int atomTag, IPullAtomResultReceiver resultReceiver) { + long token = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> { + List<StatsEvent> data = new ArrayList<>(); + int successInt = mCallback.onPullAtom(atomTag, data); + boolean success = successInt == PULL_SUCCESS; + StatsEventParcel[] parcels = new StatsEventParcel[data.size()]; + for (int i = 0; i < data.size(); i++) { + parcels[i] = new StatsEventParcel(); + parcels[i].buffer = data.get(i).getBytes(); + } + try { + resultReceiver.pullFinished(atomTag, success, parcels); + } catch (RemoteException e) { + Log.w(TAG, "StatsPullResultReceiver failed for tag " + mAtomId + + " due to TransactionTooLarge. Calling pullFinish with no data"); + StatsEventParcel[] emptyData = new StatsEventParcel[0]; + try { + resultReceiver.pullFinished(atomTag, /*success=*/false, emptyData); + } catch (RemoteException nestedException) { + Log.w(TAG, "StatsPullResultReceiver failed for tag " + mAtomId + + " with empty payload"); + } + } + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + + /** + * Metadata required for registering a StatsPullAtomCallback. + * All fields are optional, and defaults will be used for fields that are unspecified. + * + */ + public static class PullAtomMetadata { + private final long mCoolDownMillis; + private final long mTimeoutMillis; + private final int[] mAdditiveFields; + + // Private Constructor for builder + private PullAtomMetadata(long coolDownMillis, long timeoutMillis, int[] additiveFields) { + mCoolDownMillis = coolDownMillis; + mTimeoutMillis = timeoutMillis; + mAdditiveFields = additiveFields; + } + + /** + * Builder for PullAtomMetadata. + */ + public static class Builder { + private long mCoolDownMillis; + private long mTimeoutMillis; + private int[] mAdditiveFields; + + /** + * Returns a new PullAtomMetadata.Builder object for constructing PullAtomMetadata for + * StatsManager#registerPullAtomCallback + */ + public Builder() { + mCoolDownMillis = DEFAULT_COOL_DOWN_MILLIS; + mTimeoutMillis = DEFAULT_TIMEOUT_MILLIS; + mAdditiveFields = null; + } + + /** + * Set the cool down time of the pull in milliseconds. If two successive pulls are + * issued within the cool down, a cached version of the first pull will be used for the + * second pull. The minimum allowed cool down is 1 second. + */ + @NonNull + public Builder setCoolDownMillis(long coolDownMillis) { + mCoolDownMillis = coolDownMillis; + return this; + } + + /** + * Set the maximum time the pull can take in milliseconds. The maximum allowed timeout + * is 10 seconds. + */ + @NonNull + public Builder setTimeoutMillis(long timeoutMillis) { + mTimeoutMillis = timeoutMillis; + return this; + } + + /** + * Set the additive fields of this pulled atom. + * + * This is only applicable for atoms which have a uid field. When tasks are run in + * isolated processes, the data will be attributed to the host uid. Additive fields + * will be combined when the non-additive fields are the same. + */ + @NonNull + public Builder setAdditiveFields(@NonNull int[] additiveFields) { + mAdditiveFields = additiveFields; + return this; + } + + /** + * Builds and returns a PullAtomMetadata object with the values set in the builder and + * defaults for unset fields. + */ + @NonNull + public PullAtomMetadata build() { + return new PullAtomMetadata(mCoolDownMillis, mTimeoutMillis, mAdditiveFields); + } + } + + /** + * Return the cool down time of this pull in milliseconds. + */ + public long getCoolDownMillis() { + return mCoolDownMillis; + } + + /** + * Return the maximum amount of time this pull can take in milliseconds. + */ + public long getTimeoutMillis() { + return mTimeoutMillis; + } + + /** + * Return the additive fields of this pulled atom. + * + * This is only applicable for atoms that have a uid field. When tasks are run in + * isolated processes, the data will be attributed to the host uid. Additive fields + * will be combined when the non-additive fields are the same. + */ + @Nullable + public int[] getAdditiveFields() { + return mAdditiveFields; + } + } + + /** + * Callback interface for pulling atoms requested by the stats service. + * + */ + public interface StatsPullAtomCallback { + /** + * Pull data for the specified atom tag, filling in the provided list of StatsEvent data. + * @return {@link #PULL_SUCCESS} if the pull was successful, or {@link #PULL_SKIP} if not. + */ + int onPullAtom(int atomTag, @NonNull List<StatsEvent> data); + } + + @GuardedBy("sLock") + private IStatsManagerService getIStatsManagerServiceLocked() { + if (mStatsManagerService != null) { + return mStatsManagerService; + } + mStatsManagerService = IStatsManagerService.Stub.asInterface( + StatsFrameworkInitializer + .getStatsServiceManager() + .getStatsManagerServiceRegisterer() + .get()); + return mStatsManagerService; + } + + /** + * Exception thrown when communication with the stats service fails (eg if it is not available). + * This might be thrown early during boot before the stats service has started or if it crashed. + */ + public static class StatsUnavailableException extends AndroidException { + public StatsUnavailableException(String reason) { + super("Failed to connect to statsd: " + reason); + } + + public StatsUnavailableException(String reason, Throwable e) { + super("Failed to connect to statsd: " + reason, e); + } + } +} diff --git a/apex/statsd/framework/java/android/os/StatsDimensionsValue.java b/apex/statsd/framework/java/android/os/StatsDimensionsValue.java new file mode 100644 index 000000000000..7d9349cefa48 --- /dev/null +++ b/apex/statsd/framework/java/android/os/StatsDimensionsValue.java @@ -0,0 +1,317 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.os; + +import android.annotation.SystemApi; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Container for statsd dimension value information, corresponding to a + * stats_log.proto's DimensionValue. + * + * This consists of a field (an int representing a statsd atom field) + * and a value (which may be one of a number of types). + * + * <p> + * Only a single value is held, and it is necessarily one of the following types: + * {@link String}, int, long, boolean, float, + * or tuple (i.e. {@link List} of {@code StatsDimensionsValue}). + * + * The type of value held can be retrieved using {@link #getValueType()}, which returns one of the + * following ints, depending on the type of value: + * <ul> + * <li>{@link #STRING_VALUE_TYPE}</li> + * <li>{@link #INT_VALUE_TYPE}</li> + * <li>{@link #LONG_VALUE_TYPE}</li> + * <li>{@link #BOOLEAN_VALUE_TYPE}</li> + * <li>{@link #FLOAT_VALUE_TYPE}</li> + * <li>{@link #TUPLE_VALUE_TYPE}</li> + * </ul> + * Alternatively, this can be determined using {@link #isValueType(int)} with one of these constants + * as a parameter. + * The value itself can be retrieved using the correct get...Value() function for its type. + * + * <p> + * The field is always an int, and always exists; it can be obtained using {@link #getField()}. + * + * + * @hide + */ +@SystemApi +public final class StatsDimensionsValue implements Parcelable { + private static final String TAG = "StatsDimensionsValue"; + + // Values of the value type correspond to stats_log.proto's DimensionValue fields. + // Keep constants in sync with frameworks/base/cmds/statsd/src/HashableDimensionKey.cpp. + /** Indicates that this holds a String. */ + public static final int STRING_VALUE_TYPE = 2; + /** Indicates that this holds an int. */ + public static final int INT_VALUE_TYPE = 3; + /** Indicates that this holds a long. */ + public static final int LONG_VALUE_TYPE = 4; + /** Indicates that this holds a boolean. */ + public static final int BOOLEAN_VALUE_TYPE = 5; + /** Indicates that this holds a float. */ + public static final int FLOAT_VALUE_TYPE = 6; + /** Indicates that this holds a List of StatsDimensionsValues. */ + public static final int TUPLE_VALUE_TYPE = 7; + + private final StatsDimensionsValueParcel mInner; + + /** + * Creates a {@code StatsDimensionValue} from a parcel. + * + * @hide + */ + public StatsDimensionsValue(Parcel in) { + mInner = StatsDimensionsValueParcel.CREATOR.createFromParcel(in); + } + + /** + * Creates a {@code StatsDimensionsValue} from a StatsDimensionsValueParcel + * + * @hide + */ + public StatsDimensionsValue(StatsDimensionsValueParcel parcel) { + mInner = parcel; + } + + /** + * Return the field, i.e. the tag of a statsd atom. + * + * @return the field + */ + public int getField() { + return mInner.field; + } + + /** + * Retrieve the String held, if any. + * + * @return the {@link String} held if {@link #getValueType()} == {@link #STRING_VALUE_TYPE}, + * null otherwise + */ + public String getStringValue() { + if (mInner.valueType == STRING_VALUE_TYPE) { + return mInner.stringValue; + } else { + Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not string."); + return null; + } + } + + /** + * Retrieve the int held, if any. + * + * @return the int held if {@link #getValueType()} == {@link #INT_VALUE_TYPE}, 0 otherwise + */ + public int getIntValue() { + if (mInner.valueType == INT_VALUE_TYPE) { + return mInner.intValue; + } else { + Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not int."); + return 0; + } + } + + /** + * Retrieve the long held, if any. + * + * @return the long held if {@link #getValueType()} == {@link #LONG_VALUE_TYPE}, 0 otherwise + */ + public long getLongValue() { + if (mInner.valueType == LONG_VALUE_TYPE) { + return mInner.longValue; + } else { + Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not long."); + return 0; + } + } + + /** + * Retrieve the boolean held, if any. + * + * @return the boolean held if {@link #getValueType()} == {@link #BOOLEAN_VALUE_TYPE}, + * false otherwise + */ + public boolean getBooleanValue() { + if (mInner.valueType == BOOLEAN_VALUE_TYPE) { + return mInner.boolValue; + } else { + Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not boolean."); + return false; + } + } + + /** + * Retrieve the float held, if any. + * + * @return the float held if {@link #getValueType()} == {@link #FLOAT_VALUE_TYPE}, 0 otherwise + */ + public float getFloatValue() { + if (mInner.valueType == FLOAT_VALUE_TYPE) { + return mInner.floatValue; + } else { + Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not float."); + return 0; + } + } + + /** + * Retrieve the tuple, in the form of a {@link List} of {@link StatsDimensionsValue}, held, + * if any. + * + * @return the {@link List} of {@link StatsDimensionsValue} held + * if {@link #getValueType()} == {@link #TUPLE_VALUE_TYPE}, + * null otherwise + */ + public List<StatsDimensionsValue> getTupleValueList() { + if (mInner.valueType == TUPLE_VALUE_TYPE) { + int length = (mInner.tupleValue == null) ? 0 : mInner.tupleValue.length; + List<StatsDimensionsValue> tupleValues = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + tupleValues.add(new StatsDimensionsValue(mInner.tupleValue[i])); + } + return tupleValues; + } else { + Log.w(TAG, "Value type is " + getValueTypeAsString() + ", not tuple."); + return null; + } + } + + /** + * Returns the constant representing the type of value stored, namely one of + * <ul> + * <li>{@link #STRING_VALUE_TYPE}</li> + * <li>{@link #INT_VALUE_TYPE}</li> + * <li>{@link #LONG_VALUE_TYPE}</li> + * <li>{@link #BOOLEAN_VALUE_TYPE}</li> + * <li>{@link #FLOAT_VALUE_TYPE}</li> + * <li>{@link #TUPLE_VALUE_TYPE}</li> + * </ul> + * + * @return the constant representing the type of value stored + */ + public int getValueType() { + return mInner.valueType; + } + + /** + * Returns whether the type of value stored is equal to the given type. + * + * @param valueType int representing the type of value stored, as used in {@link #getValueType} + * @return true if {@link #getValueType()} is equal to {@code valueType}. + */ + public boolean isValueType(int valueType) { + return mInner.valueType == valueType; + } + + /** + * Returns a String representing the information in this StatsDimensionValue. + * No guarantees are made about the format of this String. + * + * @return String representation + * + * @hide + */ + // Follows the format of statsd's dimension.h toString. + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(mInner.field); + sb.append(":"); + switch (mInner.valueType) { + case STRING_VALUE_TYPE: + sb.append(mInner.stringValue); + break; + case INT_VALUE_TYPE: + sb.append(String.valueOf(mInner.intValue)); + break; + case LONG_VALUE_TYPE: + sb.append(String.valueOf(mInner.longValue)); + break; + case BOOLEAN_VALUE_TYPE: + sb.append(String.valueOf(mInner.boolValue)); + break; + case FLOAT_VALUE_TYPE: + sb.append(String.valueOf(mInner.floatValue)); + break; + case TUPLE_VALUE_TYPE: + sb.append("{"); + int length = (mInner.tupleValue == null) ? 0 : mInner.tupleValue.length; + for (int i = 0; i < length; i++) { + StatsDimensionsValue child = new StatsDimensionsValue(mInner.tupleValue[i]); + sb.append(child.toString()); + sb.append("|"); + } + sb.append("}"); + break; + default: + Log.w(TAG, "Incorrect value type"); + break; + } + return sb.toString(); + } + + /** + * Parcelable Creator for StatsDimensionsValue. + */ + public static final @android.annotation.NonNull + Parcelable.Creator<StatsDimensionsValue> CREATOR = new + Parcelable.Creator<StatsDimensionsValue>() { + public StatsDimensionsValue createFromParcel(Parcel in) { + return new StatsDimensionsValue(in); + } + + public StatsDimensionsValue[] newArray(int size) { + return new StatsDimensionsValue[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + mInner.writeToParcel(out, flags); + } + + /** + * Returns a string representation of the type of value stored. + */ + private String getValueTypeAsString() { + switch (mInner.valueType) { + case STRING_VALUE_TYPE: + return "string"; + case INT_VALUE_TYPE: + return "int"; + case LONG_VALUE_TYPE: + return "long"; + case BOOLEAN_VALUE_TYPE: + return "boolean"; + case FLOAT_VALUE_TYPE: + return "float"; + case TUPLE_VALUE_TYPE: + return "tuple"; + default: + return "unknown"; + } + } +} diff --git a/apex/statsd/framework/java/android/os/StatsFrameworkInitializer.java b/apex/statsd/framework/java/android/os/StatsFrameworkInitializer.java new file mode 100644 index 000000000000..8dc91239c2e0 --- /dev/null +++ b/apex/statsd/framework/java/android/os/StatsFrameworkInitializer.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.os; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.annotation.SystemApi.Client; +import android.app.StatsManager; +import android.app.SystemServiceRegistry; +import android.content.Context; + +/** + * Class for performing registration for all stats services + * + * @hide + */ +@SystemApi(client = Client.MODULE_LIBRARIES) +public class StatsFrameworkInitializer { + private StatsFrameworkInitializer() { + } + + private static volatile StatsServiceManager sStatsServiceManager; + + /** + * Sets an instance of {@link StatsServiceManager} that allows + * the statsd mainline module to register/obtain stats binder services. This is called + * by the platform during the system initialization. + * + * @param statsServiceManager instance of {@link StatsServiceManager} that allows + * the statsd mainline module to register/obtain statsd binder services. + */ + public static void setStatsServiceManager( + @NonNull StatsServiceManager statsServiceManager) { + if (sStatsServiceManager != null) { + throw new IllegalStateException("setStatsServiceManager called twice!"); + } + + if (statsServiceManager == null) { + throw new NullPointerException("statsServiceManager is null"); + } + + sStatsServiceManager = statsServiceManager; + } + + /** @hide */ + public static StatsServiceManager getStatsServiceManager() { + return sStatsServiceManager; + } + + /** + * Called by {@link SystemServiceRegistry}'s static initializer and registers all statsd + * services to {@link Context}, so that {@link Context#getSystemService} can return them. + * + * @throws IllegalStateException if this is called from anywhere besides + * {@link SystemServiceRegistry} + */ + public static void registerServiceWrappers() { + SystemServiceRegistry.registerContextAwareService( + Context.STATS_MANAGER, + StatsManager.class, + context -> new StatsManager(context) + ); + } +} diff --git a/apex/statsd/framework/java/android/util/StatsEvent.java b/apex/statsd/framework/java/android/util/StatsEvent.java new file mode 100644 index 000000000000..8be5c63f31e3 --- /dev/null +++ b/apex/statsd/framework/java/android/util/StatsEvent.java @@ -0,0 +1,879 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.SystemClock; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Arrays; + +/** + * StatsEvent builds and stores the buffer sent over the statsd socket. + * This class defines and encapsulates the socket protocol. + * + * <p>Usage:</p> + * <pre> + * // Pushed event + * StatsEvent statsEvent = StatsEvent.newBuilder() + * .setAtomId(atomId) + * .writeBoolean(false) + * .writeString("annotated String field") + * .addBooleanAnnotation(annotationId, true) + * .usePooledBuffer() + * .build(); + * StatsLog.write(statsEvent); + * + * // Pulled event + * StatsEvent statsEvent = StatsEvent.newBuilder() + * .setAtomId(atomId) + * .writeBoolean(false) + * .writeString("annotated String field") + * .addBooleanAnnotation(annotationId, true) + * .build(); + * </pre> + * @hide + **/ +@SystemApi +public final class StatsEvent { + // Type Ids. + /** + * @hide + **/ + @VisibleForTesting + public static final byte TYPE_INT = 0x00; + + /** + * @hide + **/ + @VisibleForTesting + public static final byte TYPE_LONG = 0x01; + + /** + * @hide + **/ + @VisibleForTesting + public static final byte TYPE_STRING = 0x02; + + /** + * @hide + **/ + @VisibleForTesting + public static final byte TYPE_LIST = 0x03; + + /** + * @hide + **/ + @VisibleForTesting + public static final byte TYPE_FLOAT = 0x04; + + /** + * @hide + **/ + @VisibleForTesting + public static final byte TYPE_BOOLEAN = 0x05; + + /** + * @hide + **/ + @VisibleForTesting + public static final byte TYPE_BYTE_ARRAY = 0x06; + + /** + * @hide + **/ + @VisibleForTesting + public static final byte TYPE_OBJECT = 0x07; + + /** + * @hide + **/ + @VisibleForTesting + public static final byte TYPE_KEY_VALUE_PAIRS = 0x08; + + /** + * @hide + **/ + @VisibleForTesting + public static final byte TYPE_ATTRIBUTION_CHAIN = 0x09; + + /** + * @hide + **/ + @VisibleForTesting + public static final byte TYPE_ERRORS = 0x0F; + + // Error flags. + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_NO_TIMESTAMP = 0x1; + + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_NO_ATOM_ID = 0x2; + + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_OVERFLOW = 0x4; + + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_ATTRIBUTION_CHAIN_TOO_LONG = 0x8; + + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_TOO_MANY_KEY_VALUE_PAIRS = 0x10; + + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD = 0x20; + + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_INVALID_ANNOTATION_ID = 0x40; + + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_ANNOTATION_ID_TOO_LARGE = 0x80; + + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_TOO_MANY_ANNOTATIONS = 0x100; + + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_TOO_MANY_FIELDS = 0x200; + + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_ATTRIBUTION_UIDS_TAGS_SIZES_NOT_EQUAL = 0x1000; + + /** + * @hide + **/ + @VisibleForTesting + public static final int ERROR_ATOM_ID_INVALID_POSITION = 0x2000; + + // Size limits. + + /** + * @hide + **/ + @VisibleForTesting + public static final int MAX_ANNOTATION_COUNT = 15; + + /** + * @hide + **/ + @VisibleForTesting + public static final int MAX_ATTRIBUTION_NODES = 127; + + /** + * @hide + **/ + @VisibleForTesting + public static final int MAX_NUM_ELEMENTS = 127; + + /** + * @hide + **/ + @VisibleForTesting + public static final int MAX_KEY_VALUE_PAIRS = 127; + + private static final int LOGGER_ENTRY_MAX_PAYLOAD = 4068; + + // Max payload size is 4 bytes less as 4 bytes are reserved for statsEventTag. + // See android_util_StatsLog.cpp. + private static final int MAX_PUSH_PAYLOAD_SIZE = LOGGER_ENTRY_MAX_PAYLOAD - 4; + + private static final int MAX_PULL_PAYLOAD_SIZE = 50 * 1024; // 50 KB + + private final int mAtomId; + private final byte[] mPayload; + private Buffer mBuffer; + private final int mNumBytes; + + private StatsEvent(final int atomId, @Nullable final Buffer buffer, + @NonNull final byte[] payload, final int numBytes) { + mAtomId = atomId; + mBuffer = buffer; + mPayload = payload; + mNumBytes = numBytes; + } + + /** + * Returns a new StatsEvent.Builder for building StatsEvent object. + **/ + @NonNull + public static StatsEvent.Builder newBuilder() { + return new StatsEvent.Builder(Buffer.obtain()); + } + + /** + * Get the atom Id of the atom encoded in this StatsEvent object. + * + * @hide + **/ + public int getAtomId() { + return mAtomId; + } + + /** + * Get the byte array that contains the encoded payload that can be sent to statsd. + * + * @hide + **/ + @NonNull + public byte[] getBytes() { + return mPayload; + } + + /** + * Get the number of bytes used to encode the StatsEvent payload. + * + * @hide + **/ + public int getNumBytes() { + return mNumBytes; + } + + /** + * Recycle resources used by this StatsEvent object. + * No actions should be taken on this StatsEvent after release() is called. + * + * @hide + **/ + public void release() { + if (mBuffer != null) { + mBuffer.release(); + mBuffer = null; + } + } + + /** + * Builder for constructing a StatsEvent object. + * + * <p>This class defines and encapsulates the socket encoding for the buffer. + * The write methods must be called in the same order as the order of fields in the + * atom definition.</p> + * + * <p>setAtomId() can be called anytime before build().</p> + * + * <p>Example:</p> + * <pre> + * // Atom definition. + * message MyAtom { + * optional int32 field1 = 1; + * optional int64 field2 = 2; + * optional string field3 = 3 [(annotation1) = true]; + * } + * + * // StatsEvent construction for pushed event. + * StatsEvent.newBuilder() + * StatsEvent statsEvent = StatsEvent.newBuilder() + * .setAtomId(atomId) + * .writeInt(3) // field1 + * .writeLong(8L) // field2 + * .writeString("foo") // field 3 + * .addBooleanAnnotation(annotation1Id, true) + * .usePooledBuffer() + * .build(); + * + * // StatsEvent construction for pulled event. + * StatsEvent.newBuilder() + * StatsEvent statsEvent = StatsEvent.newBuilder() + * .setAtomId(atomId) + * .writeInt(3) // field1 + * .writeLong(8L) // field2 + * .writeString("foo") // field 3 + * .addBooleanAnnotation(annotation1Id, true) + * .build(); + * </pre> + **/ + public static final class Builder { + // Fixed positions. + private static final int POS_NUM_ELEMENTS = 1; + private static final int POS_TIMESTAMP_NS = POS_NUM_ELEMENTS + Byte.BYTES; + private static final int POS_ATOM_ID = POS_TIMESTAMP_NS + Byte.BYTES + Long.BYTES; + + private final Buffer mBuffer; + private long mTimestampNs; + private int mAtomId; + private byte mCurrentAnnotationCount; + private int mPos; + private int mPosLastField; + private byte mLastType; + private int mNumElements; + private int mErrorMask; + private boolean mUsePooledBuffer = false; + + private Builder(final Buffer buffer) { + mBuffer = buffer; + mCurrentAnnotationCount = 0; + mAtomId = 0; + mTimestampNs = SystemClock.elapsedRealtimeNanos(); + mNumElements = 0; + + // Set mPos to 0 for writing TYPE_OBJECT at 0th position. + mPos = 0; + writeTypeId(TYPE_OBJECT); + + // Write timestamp. + mPos = POS_TIMESTAMP_NS; + writeLong(mTimestampNs); + } + + /** + * Sets the atom id for this StatsEvent. + * + * This should be called immediately after StatsEvent.newBuilder() + * and should only be called once. + * Not calling setAtomId will result in ERROR_NO_ATOM_ID. + * Calling setAtomId out of order will result in ERROR_ATOM_ID_INVALID_POSITION. + **/ + @NonNull + public Builder setAtomId(final int atomId) { + if (0 == mAtomId) { + mAtomId = atomId; + + if (1 == mNumElements) { // Only timestamp is written so far. + writeInt(atomId); + } else { + // setAtomId called out of order. + mErrorMask |= ERROR_ATOM_ID_INVALID_POSITION; + } + } + + return this; + } + + /** + * Write a boolean field to this StatsEvent. + **/ + @NonNull + public Builder writeBoolean(final boolean value) { + // Write boolean typeId byte followed by boolean byte representation. + writeTypeId(TYPE_BOOLEAN); + mPos += mBuffer.putBoolean(mPos, value); + mNumElements++; + return this; + } + + /** + * Write an integer field to this StatsEvent. + **/ + @NonNull + public Builder writeInt(final int value) { + // Write integer typeId byte followed by 4-byte representation of value. + writeTypeId(TYPE_INT); + mPos += mBuffer.putInt(mPos, value); + mNumElements++; + return this; + } + + /** + * Write a long field to this StatsEvent. + **/ + @NonNull + public Builder writeLong(final long value) { + // Write long typeId byte followed by 8-byte representation of value. + writeTypeId(TYPE_LONG); + mPos += mBuffer.putLong(mPos, value); + mNumElements++; + return this; + } + + /** + * Write a float field to this StatsEvent. + **/ + @NonNull + public Builder writeFloat(final float value) { + // Write float typeId byte followed by 4-byte representation of value. + writeTypeId(TYPE_FLOAT); + mPos += mBuffer.putFloat(mPos, value); + mNumElements++; + return this; + } + + /** + * Write a String field to this StatsEvent. + **/ + @NonNull + public Builder writeString(@NonNull final String value) { + // Write String typeId byte, followed by 4-byte representation of number of bytes + // in the UTF-8 encoding, followed by the actual UTF-8 byte encoding of value. + final byte[] valueBytes = stringToBytes(value); + writeByteArray(valueBytes, TYPE_STRING); + return this; + } + + /** + * Write a byte array field to this StatsEvent. + **/ + @NonNull + public Builder writeByteArray(@NonNull final byte[] value) { + // Write byte array typeId byte, followed by 4-byte representation of number of bytes + // in value, followed by the actual byte array. + writeByteArray(value, TYPE_BYTE_ARRAY); + return this; + } + + private void writeByteArray(@NonNull final byte[] value, final byte typeId) { + writeTypeId(typeId); + final int numBytes = value.length; + mPos += mBuffer.putInt(mPos, numBytes); + mPos += mBuffer.putByteArray(mPos, value); + mNumElements++; + } + + /** + * Write an attribution chain field to this StatsEvent. + * + * The sizes of uids and tags must be equal. The AttributionNode at position i is + * made up of uids[i] and tags[i]. + * + * @param uids array of uids in the attribution nodes. + * @param tags array of tags in the attribution nodes. + **/ + @NonNull + public Builder writeAttributionChain( + @NonNull final int[] uids, @NonNull final String[] tags) { + final byte numUids = (byte) uids.length; + final byte numTags = (byte) tags.length; + + if (numUids != numTags) { + mErrorMask |= ERROR_ATTRIBUTION_UIDS_TAGS_SIZES_NOT_EQUAL; + } else if (numUids > MAX_ATTRIBUTION_NODES) { + mErrorMask |= ERROR_ATTRIBUTION_CHAIN_TOO_LONG; + } else { + // Write attribution chain typeId byte, followed by 1-byte representation of + // number of attribution nodes, followed by encoding of each attribution node. + writeTypeId(TYPE_ATTRIBUTION_CHAIN); + mPos += mBuffer.putByte(mPos, numUids); + for (int i = 0; i < numUids; i++) { + // Each uid is encoded as 4-byte representation of its int value. + mPos += mBuffer.putInt(mPos, uids[i]); + + // Each tag is encoded as 4-byte representation of number of bytes in its + // UTF-8 encoding, followed by the actual UTF-8 bytes. + final byte[] tagBytes = stringToBytes(tags[i]); + mPos += mBuffer.putInt(mPos, tagBytes.length); + mPos += mBuffer.putByteArray(mPos, tagBytes); + } + mNumElements++; + } + return this; + } + + /** + * Write KeyValuePairsAtom entries to this StatsEvent. + * + * @param intMap Integer key-value pairs. + * @param longMap Long key-value pairs. + * @param stringMap String key-value pairs. + * @param floatMap Float key-value pairs. + **/ + @NonNull + public Builder writeKeyValuePairs( + @Nullable final SparseIntArray intMap, + @Nullable final SparseLongArray longMap, + @Nullable final SparseArray<String> stringMap, + @Nullable final SparseArray<Float> floatMap) { + final int intMapSize = null == intMap ? 0 : intMap.size(); + final int longMapSize = null == longMap ? 0 : longMap.size(); + final int stringMapSize = null == stringMap ? 0 : stringMap.size(); + final int floatMapSize = null == floatMap ? 0 : floatMap.size(); + final int totalCount = intMapSize + longMapSize + stringMapSize + floatMapSize; + + if (totalCount > MAX_KEY_VALUE_PAIRS) { + mErrorMask |= ERROR_TOO_MANY_KEY_VALUE_PAIRS; + } else { + writeTypeId(TYPE_KEY_VALUE_PAIRS); + mPos += mBuffer.putByte(mPos, (byte) totalCount); + + for (int i = 0; i < intMapSize; i++) { + final int key = intMap.keyAt(i); + final int value = intMap.valueAt(i); + mPos += mBuffer.putInt(mPos, key); + writeTypeId(TYPE_INT); + mPos += mBuffer.putInt(mPos, value); + } + + for (int i = 0; i < longMapSize; i++) { + final int key = longMap.keyAt(i); + final long value = longMap.valueAt(i); + mPos += mBuffer.putInt(mPos, key); + writeTypeId(TYPE_LONG); + mPos += mBuffer.putLong(mPos, value); + } + + for (int i = 0; i < stringMapSize; i++) { + final int key = stringMap.keyAt(i); + final String value = stringMap.valueAt(i); + mPos += mBuffer.putInt(mPos, key); + writeTypeId(TYPE_STRING); + final byte[] valueBytes = stringToBytes(value); + mPos += mBuffer.putInt(mPos, valueBytes.length); + mPos += mBuffer.putByteArray(mPos, valueBytes); + } + + for (int i = 0; i < floatMapSize; i++) { + final int key = floatMap.keyAt(i); + final float value = floatMap.valueAt(i); + mPos += mBuffer.putInt(mPos, key); + writeTypeId(TYPE_FLOAT); + mPos += mBuffer.putFloat(mPos, value); + } + + mNumElements++; + } + + return this; + } + + /** + * Write a boolean annotation for the last field written. + **/ + @NonNull + public Builder addBooleanAnnotation( + final byte annotationId, final boolean value) { + // Ensure there's a field written to annotate. + if (mNumElements < 2) { + mErrorMask |= ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD; + } else if (mCurrentAnnotationCount >= MAX_ANNOTATION_COUNT) { + mErrorMask |= ERROR_TOO_MANY_ANNOTATIONS; + } else { + mPos += mBuffer.putByte(mPos, annotationId); + mPos += mBuffer.putByte(mPos, TYPE_BOOLEAN); + mPos += mBuffer.putBoolean(mPos, value); + mCurrentAnnotationCount++; + writeAnnotationCount(); + } + + return this; + } + + /** + * Write an integer annotation for the last field written. + **/ + @NonNull + public Builder addIntAnnotation(final byte annotationId, final int value) { + if (mNumElements < 2) { + mErrorMask |= ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD; + } else if (mCurrentAnnotationCount >= MAX_ANNOTATION_COUNT) { + mErrorMask |= ERROR_TOO_MANY_ANNOTATIONS; + } else { + mPos += mBuffer.putByte(mPos, annotationId); + mPos += mBuffer.putByte(mPos, TYPE_INT); + mPos += mBuffer.putInt(mPos, value); + mCurrentAnnotationCount++; + writeAnnotationCount(); + } + + return this; + } + + /** + * Indicates to reuse Buffer's byte array as the underlying payload in StatsEvent. + * This should be called for pushed events to reduce memory allocations and garbage + * collections. + **/ + @NonNull + public Builder usePooledBuffer() { + mUsePooledBuffer = true; + mBuffer.setMaxSize(MAX_PUSH_PAYLOAD_SIZE, mPos); + return this; + } + + /** + * Builds a StatsEvent object with values entered in this Builder. + **/ + @NonNull + public StatsEvent build() { + if (0L == mTimestampNs) { + mErrorMask |= ERROR_NO_TIMESTAMP; + } + if (0 == mAtomId) { + mErrorMask |= ERROR_NO_ATOM_ID; + } + if (mBuffer.hasOverflowed()) { + mErrorMask |= ERROR_OVERFLOW; + } + if (mNumElements > MAX_NUM_ELEMENTS) { + mErrorMask |= ERROR_TOO_MANY_FIELDS; + } + + if (0 == mErrorMask) { + mBuffer.putByte(POS_NUM_ELEMENTS, (byte) mNumElements); + } else { + // Write atom id and error mask. Overwrite any annotations for atom Id. + mPos = POS_ATOM_ID; + mPos += mBuffer.putByte(mPos, TYPE_INT); + mPos += mBuffer.putInt(mPos, mAtomId); + mPos += mBuffer.putByte(mPos, TYPE_ERRORS); + mPos += mBuffer.putInt(mPos, mErrorMask); + mBuffer.putByte(POS_NUM_ELEMENTS, (byte) 3); + } + + final int size = mPos; + + if (mUsePooledBuffer) { + return new StatsEvent(mAtomId, mBuffer, mBuffer.getBytes(), size); + } else { + // Create a copy of the buffer with the required number of bytes. + final byte[] payload = new byte[size]; + System.arraycopy(mBuffer.getBytes(), 0, payload, 0, size); + + // Return Buffer instance to the pool. + mBuffer.release(); + + return new StatsEvent(mAtomId, null, payload, size); + } + } + + private void writeTypeId(final byte typeId) { + mPosLastField = mPos; + mLastType = typeId; + mCurrentAnnotationCount = 0; + final byte encodedId = (byte) (typeId & 0x0F); + mPos += mBuffer.putByte(mPos, encodedId); + } + + private void writeAnnotationCount() { + // Use first 4 bits for annotation count and last 4 bits for typeId. + final byte encodedId = (byte) ((mCurrentAnnotationCount << 4) | (mLastType & 0x0F)); + mBuffer.putByte(mPosLastField, encodedId); + } + + @NonNull + private static byte[] stringToBytes(@Nullable final String value) { + return (null == value ? "" : value).getBytes(UTF_8); + } + } + + private static final class Buffer { + private static Object sLock = new Object(); + + @GuardedBy("sLock") + private static Buffer sPool; + + private byte[] mBytes = new byte[MAX_PUSH_PAYLOAD_SIZE]; + private boolean mOverflow = false; + private int mMaxSize = MAX_PULL_PAYLOAD_SIZE; + + @NonNull + private static Buffer obtain() { + final Buffer buffer; + synchronized (sLock) { + buffer = null == sPool ? new Buffer() : sPool; + sPool = null; + } + buffer.reset(); + return buffer; + } + + private Buffer() { + } + + @NonNull + private byte[] getBytes() { + return mBytes; + } + + private void release() { + // Recycle this Buffer if its size is MAX_PUSH_PAYLOAD_SIZE or under. + if (mBytes.length <= MAX_PUSH_PAYLOAD_SIZE) { + synchronized (sLock) { + if (null == sPool) { + sPool = this; + } + } + } + } + + private void reset() { + mOverflow = false; + mMaxSize = MAX_PULL_PAYLOAD_SIZE; + } + + private void setMaxSize(final int maxSize, final int numBytesWritten) { + mMaxSize = maxSize; + if (numBytesWritten > maxSize) { + mOverflow = true; + } + } + + private boolean hasOverflowed() { + return mOverflow; + } + + /** + * Checks for available space in the byte array. + * + * @param index starting position in the buffer to start the check. + * @param numBytes number of bytes to check from index. + * @return true if space is available, false otherwise. + **/ + private boolean hasEnoughSpace(final int index, final int numBytes) { + final int totalBytesNeeded = index + numBytes; + + if (totalBytesNeeded > mMaxSize) { + mOverflow = true; + return false; + } + + // Expand buffer if needed. + if (mBytes.length < mMaxSize && totalBytesNeeded > mBytes.length) { + int newSize = mBytes.length; + do { + newSize *= 2; + } while (newSize <= totalBytesNeeded); + + if (newSize > mMaxSize) { + newSize = mMaxSize; + } + + mBytes = Arrays.copyOf(mBytes, newSize); + } + + return true; + } + + /** + * Writes a byte into the buffer. + * + * @param index position in the buffer where the byte is written. + * @param value the byte to write. + * @return number of bytes written to buffer from this write operation. + **/ + private int putByte(final int index, final byte value) { + if (hasEnoughSpace(index, Byte.BYTES)) { + mBytes[index] = (byte) (value); + return Byte.BYTES; + } + return 0; + } + + /** + * Writes a boolean into the buffer. + * + * @param index position in the buffer where the boolean is written. + * @param value the boolean to write. + * @return number of bytes written to buffer from this write operation. + **/ + private int putBoolean(final int index, final boolean value) { + return putByte(index, (byte) (value ? 1 : 0)); + } + + /** + * Writes an integer into the buffer. + * + * @param index position in the buffer where the integer is written. + * @param value the integer to write. + * @return number of bytes written to buffer from this write operation. + **/ + private int putInt(final int index, final int value) { + if (hasEnoughSpace(index, Integer.BYTES)) { + // Use little endian byte order. + mBytes[index] = (byte) (value); + mBytes[index + 1] = (byte) (value >> 8); + mBytes[index + 2] = (byte) (value >> 16); + mBytes[index + 3] = (byte) (value >> 24); + return Integer.BYTES; + } + return 0; + } + + /** + * Writes a long into the buffer. + * + * @param index position in the buffer where the long is written. + * @param value the long to write. + * @return number of bytes written to buffer from this write operation. + **/ + private int putLong(final int index, final long value) { + if (hasEnoughSpace(index, Long.BYTES)) { + // Use little endian byte order. + mBytes[index] = (byte) (value); + mBytes[index + 1] = (byte) (value >> 8); + mBytes[index + 2] = (byte) (value >> 16); + mBytes[index + 3] = (byte) (value >> 24); + mBytes[index + 4] = (byte) (value >> 32); + mBytes[index + 5] = (byte) (value >> 40); + mBytes[index + 6] = (byte) (value >> 48); + mBytes[index + 7] = (byte) (value >> 56); + return Long.BYTES; + } + return 0; + } + + /** + * Writes a float into the buffer. + * + * @param index position in the buffer where the float is written. + * @param value the float to write. + * @return number of bytes written to buffer from this write operation. + **/ + private int putFloat(final int index, final float value) { + return putInt(index, Float.floatToIntBits(value)); + } + + /** + * Copies a byte array into the buffer. + * + * @param index position in the buffer where the byte array is copied. + * @param value the byte array to copy. + * @return number of bytes written to buffer from this write operation. + **/ + private int putByteArray(final int index, @NonNull final byte[] value) { + final int numBytes = value.length; + if (hasEnoughSpace(index, numBytes)) { + System.arraycopy(value, 0, mBytes, index, numBytes); + return numBytes; + } + return 0; + } + } +} diff --git a/apex/statsd/framework/java/android/util/StatsLog.java b/apex/statsd/framework/java/android/util/StatsLog.java new file mode 100644 index 000000000000..0a9f4ebabdf0 --- /dev/null +++ b/apex/statsd/framework/java/android/util/StatsLog.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import static android.Manifest.permission.DUMP; +import static android.Manifest.permission.PACKAGE_USAGE_STATS; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.content.Context; +import android.os.IStatsd; +import android.os.Process; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.statsd.StatsdStatsLog; + +/** + * StatsLog provides an API for developers to send events to statsd. The events can be used to + * define custom metrics inside statsd. + */ +public final class StatsLog { + + // Load JNI library + static { + System.loadLibrary("stats_jni"); + } + private static final String TAG = "StatsLog"; + private static final boolean DEBUG = false; + private static final int EXPERIMENT_IDS_FIELD_ID = 1; + + private StatsLog() { + } + + /** + * Logs a start event. + * + * @param label developer-chosen label. + * @return True if the log request was sent to statsd. + */ + public static boolean logStart(int label) { + int callingUid = Process.myUid(); + StatsdStatsLog.write( + StatsdStatsLog.APP_BREADCRUMB_REPORTED, + callingUid, + label, + StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__START); + return true; + } + + /** + * Logs a stop event. + * + * @param label developer-chosen label. + * @return True if the log request was sent to statsd. + */ + public static boolean logStop(int label) { + int callingUid = Process.myUid(); + StatsdStatsLog.write( + StatsdStatsLog.APP_BREADCRUMB_REPORTED, + callingUid, + label, + StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__STOP); + return true; + } + + /** + * Logs an event that does not represent a start or stop boundary. + * + * @param label developer-chosen label. + * @return True if the log request was sent to statsd. + */ + public static boolean logEvent(int label) { + int callingUid = Process.myUid(); + StatsdStatsLog.write( + StatsdStatsLog.APP_BREADCRUMB_REPORTED, + callingUid, + label, + StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__UNSPECIFIED); + return true; + } + + /** + * Logs an event for binary push for module updates. + * + * @param trainName name of install train. + * @param trainVersionCode version code of the train. + * @param options optional flags about this install. + * The last 3 bits indicate options: + * 0x01: FLAG_REQUIRE_STAGING + * 0x02: FLAG_ROLLBACK_ENABLED + * 0x04: FLAG_REQUIRE_LOW_LATENCY_MONITOR + * @param state current install state. Defined as State enums in + * BinaryPushStateChanged atom in + * frameworks/base/cmds/statsd/src/atoms.proto + * @param experimentIds experiment ids. + * @return True if the log request was sent to statsd. + */ + @RequiresPermission(allOf = {DUMP, PACKAGE_USAGE_STATS}) + public static boolean logBinaryPushStateChanged(@NonNull String trainName, + long trainVersionCode, int options, int state, + @NonNull long[] experimentIds) { + ProtoOutputStream proto = new ProtoOutputStream(); + for (long id : experimentIds) { + proto.write( + ProtoOutputStream.FIELD_TYPE_INT64 + | ProtoOutputStream.FIELD_COUNT_REPEATED + | EXPERIMENT_IDS_FIELD_ID, + id); + } + StatsdStatsLog.write(StatsdStatsLog.BINARY_PUSH_STATE_CHANGED, + trainName, + trainVersionCode, + (options & IStatsd.FLAG_REQUIRE_STAGING) > 0, + (options & IStatsd.FLAG_ROLLBACK_ENABLED) > 0, + (options & IStatsd.FLAG_REQUIRE_LOW_LATENCY_MONITOR) > 0, + state, + proto.getBytes(), + 0, + 0, + false); + return true; + } + + /** + * Write an event to stats log using the raw format. + * + * @param buffer The encoded buffer of data to write. + * @param size The number of bytes from the buffer to write. + * @hide + */ + // TODO(b/144935988): Mark deprecated. + @SystemApi + public static void writeRaw(@NonNull byte[] buffer, int size) { + // TODO(b/144935988): make this no-op once clients have migrated to StatsEvent. + writeImpl(buffer, size, 0); + } + + /** + * Write an event to stats log using the raw format. + * + * @param buffer The encoded buffer of data to write. + * @param size The number of bytes from the buffer to write. + * @param atomId The id of the atom to which the event belongs. + */ + private static native void writeImpl(@NonNull byte[] buffer, int size, int atomId); + + /** + * Write an event to stats log using the raw format encapsulated in StatsEvent. + * After writing to stats log, release() is called on the StatsEvent object. + * No further action should be taken on the StatsEvent object following this call. + * + * @param statsEvent The StatsEvent object containing the encoded buffer of data to write. + * @hide + */ + @SystemApi + public static void write(@NonNull final StatsEvent statsEvent) { + writeImpl(statsEvent.getBytes(), statsEvent.getNumBytes(), statsEvent.getAtomId()); + statsEvent.release(); + } + + private static void enforceDumpCallingPermission(Context context) { + context.enforceCallingPermission(android.Manifest.permission.DUMP, "Need DUMP permission."); + } + + private static void enforcesageStatsCallingPermission(Context context) { + context.enforceCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS, + "Need PACKAGE_USAGE_STATS permission."); + } +} diff --git a/apex/statsd/framework/test/Android.bp b/apex/statsd/framework/test/Android.bp new file mode 100644 index 000000000000..b113d595b57c --- /dev/null +++ b/apex/statsd/framework/test/Android.bp @@ -0,0 +1,36 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +android_test { + name: "FrameworkStatsdTest", + platform_apis: true, + srcs: [ + // TODO(b/147705194): Use framework-statsd as a lib dependency instead. + ":framework-statsd-sources", + "**/*.java", + ], + manifest: "AndroidManifest.xml", + static_libs: [ + "androidx.test.rules", + "truth-prebuilt", + ], + libs: [ + "android.test.runner.stubs", + "android.test.base.stubs", + ], + test_suites: [ + "device-tests", + "mts", + ], +}
\ No newline at end of file diff --git a/apex/statsd/framework/test/AndroidManifest.xml b/apex/statsd/framework/test/AndroidManifest.xml new file mode 100644 index 000000000000..8f89d2332b12 --- /dev/null +++ b/apex/statsd/framework/test/AndroidManifest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.os.statsd.framework.test" + > + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.os.statsd.framework.test" + android:label="Framework Statsd Tests" /> + +</manifest> diff --git a/apex/statsd/framework/test/AndroidTest.xml b/apex/statsd/framework/test/AndroidTest.xml new file mode 100644 index 000000000000..fb519150ecd5 --- /dev/null +++ b/apex/statsd/framework/test/AndroidTest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs Tests for Statsd."> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="FrameworkStatsdTest.apk" /> + <option name="install-arg" value="-g" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="mts" /> + <option name="test-tag" value="FrameworkStatsdTest" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.os.statsd.framework.test" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> + + <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> + <option name="mainline-module-package-name" value="com.google.android.os.statsd" /> + </object> +</configuration>
\ No newline at end of file diff --git a/apex/statsd/framework/test/src/android/app/PullAtomMetadataTest.java b/apex/statsd/framework/test/src/android/app/PullAtomMetadataTest.java new file mode 100644 index 000000000000..fd386bd8e32e --- /dev/null +++ b/apex/statsd/framework/test/src/android/app/PullAtomMetadataTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.StatsManager.PullAtomMetadata; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class PullAtomMetadataTest { + + @Test + public void testEmpty() { + PullAtomMetadata metadata = new PullAtomMetadata.Builder().build(); + assertThat(metadata.getTimeoutMillis()).isEqualTo(StatsManager.DEFAULT_TIMEOUT_MILLIS); + assertThat(metadata.getCoolDownMillis()).isEqualTo(StatsManager.DEFAULT_COOL_DOWN_MILLIS); + assertThat(metadata.getAdditiveFields()).isNull(); + } + + @Test + public void testSetTimeoutMillis() { + long timeoutMillis = 500L; + PullAtomMetadata metadata = + new PullAtomMetadata.Builder().setTimeoutMillis(timeoutMillis).build(); + assertThat(metadata.getTimeoutMillis()).isEqualTo(timeoutMillis); + assertThat(metadata.getCoolDownMillis()).isEqualTo(StatsManager.DEFAULT_COOL_DOWN_MILLIS); + assertThat(metadata.getAdditiveFields()).isNull(); + } + + @Test + public void testSetCoolDownMillis() { + long coolDownMillis = 10_000L; + PullAtomMetadata metadata = + new PullAtomMetadata.Builder().setCoolDownMillis(coolDownMillis).build(); + assertThat(metadata.getTimeoutMillis()).isEqualTo(StatsManager.DEFAULT_TIMEOUT_MILLIS); + assertThat(metadata.getCoolDownMillis()).isEqualTo(coolDownMillis); + assertThat(metadata.getAdditiveFields()).isNull(); + } + + @Test + public void testSetAdditiveFields() { + int[] fields = {2, 4, 6}; + PullAtomMetadata metadata = + new PullAtomMetadata.Builder().setAdditiveFields(fields).build(); + assertThat(metadata.getTimeoutMillis()).isEqualTo(StatsManager.DEFAULT_TIMEOUT_MILLIS); + assertThat(metadata.getCoolDownMillis()).isEqualTo(StatsManager.DEFAULT_COOL_DOWN_MILLIS); + assertThat(metadata.getAdditiveFields()).isEqualTo(fields); + } + + @Test + public void testSetAllElements() { + long timeoutMillis = 300L; + long coolDownMillis = 9572L; + int[] fields = {3, 2}; + PullAtomMetadata metadata = new PullAtomMetadata.Builder() + .setTimeoutMillis(timeoutMillis) + .setCoolDownMillis(coolDownMillis) + .setAdditiveFields(fields) + .build(); + assertThat(metadata.getTimeoutMillis()).isEqualTo(timeoutMillis); + assertThat(metadata.getCoolDownMillis()).isEqualTo(coolDownMillis); + assertThat(metadata.getAdditiveFields()).isEqualTo(fields); + } +} diff --git a/apex/statsd/framework/test/src/android/os/StatsDimensionsValueTest.java b/apex/statsd/framework/test/src/android/os/StatsDimensionsValueTest.java new file mode 100644 index 000000000000..db25911e6eee --- /dev/null +++ b/apex/statsd/framework/test/src/android/os/StatsDimensionsValueTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.List; + +@RunWith(JUnit4.class) +public final class StatsDimensionsValueTest { + + @Test + public void testConversionFromStructuredParcel() { + int tupleField = 100; // atom id + String stringValue = "Hello"; + int intValue = 123; + long longValue = 123456789L; + float floatValue = 1.1f; + boolean boolValue = true; + + // Construct structured parcel + StatsDimensionsValueParcel sdvp = new StatsDimensionsValueParcel(); + sdvp.field = tupleField; + sdvp.valueType = StatsDimensionsValue.TUPLE_VALUE_TYPE; + sdvp.tupleValue = new StatsDimensionsValueParcel[5]; + + for (int i = 0; i < 5; i++) { + sdvp.tupleValue[i] = new StatsDimensionsValueParcel(); + sdvp.tupleValue[i].field = i + 1; + } + + sdvp.tupleValue[0].valueType = StatsDimensionsValue.STRING_VALUE_TYPE; + sdvp.tupleValue[1].valueType = StatsDimensionsValue.INT_VALUE_TYPE; + sdvp.tupleValue[2].valueType = StatsDimensionsValue.LONG_VALUE_TYPE; + sdvp.tupleValue[3].valueType = StatsDimensionsValue.FLOAT_VALUE_TYPE; + sdvp.tupleValue[4].valueType = StatsDimensionsValue.BOOLEAN_VALUE_TYPE; + + sdvp.tupleValue[0].stringValue = stringValue; + sdvp.tupleValue[1].intValue = intValue; + sdvp.tupleValue[2].longValue = longValue; + sdvp.tupleValue[3].floatValue = floatValue; + sdvp.tupleValue[4].boolValue = boolValue; + + // Convert to StatsDimensionsValue and check result + StatsDimensionsValue sdv = new StatsDimensionsValue(sdvp); + + assertThat(sdv.getField()).isEqualTo(tupleField); + assertThat(sdv.getValueType()).isEqualTo(StatsDimensionsValue.TUPLE_VALUE_TYPE); + List<StatsDimensionsValue> sdvChildren = sdv.getTupleValueList(); + assertThat(sdvChildren.size()).isEqualTo(5); + + for (int i = 0; i < 5; i++) { + assertThat(sdvChildren.get(i).getField()).isEqualTo(i + 1); + } + + assertThat(sdvChildren.get(0).getValueType()) + .isEqualTo(StatsDimensionsValue.STRING_VALUE_TYPE); + assertThat(sdvChildren.get(1).getValueType()) + .isEqualTo(StatsDimensionsValue.INT_VALUE_TYPE); + assertThat(sdvChildren.get(2).getValueType()) + .isEqualTo(StatsDimensionsValue.LONG_VALUE_TYPE); + assertThat(sdvChildren.get(3).getValueType()) + .isEqualTo(StatsDimensionsValue.FLOAT_VALUE_TYPE); + assertThat(sdvChildren.get(4).getValueType()) + .isEqualTo(StatsDimensionsValue.BOOLEAN_VALUE_TYPE); + + assertThat(sdvChildren.get(0).getStringValue()).isEqualTo(stringValue); + assertThat(sdvChildren.get(1).getIntValue()).isEqualTo(intValue); + assertThat(sdvChildren.get(2).getLongValue()).isEqualTo(longValue); + assertThat(sdvChildren.get(3).getFloatValue()).isEqualTo(floatValue); + assertThat(sdvChildren.get(4).getBooleanValue()).isEqualTo(boolValue); + + // Ensure that StatsDimensionsValue and StatsDimensionsValueParcel are + // parceled equivalently + Parcel sdvpParcel = Parcel.obtain(); + Parcel sdvParcel = Parcel.obtain(); + sdvp.writeToParcel(sdvpParcel, 0); + sdv.writeToParcel(sdvParcel, 0); + assertThat(sdvpParcel.dataSize()).isEqualTo(sdvParcel.dataSize()); + } + + @Test + public void testNullTupleArray() { + int tupleField = 100; // atom id + + StatsDimensionsValueParcel parcel = new StatsDimensionsValueParcel(); + parcel.field = tupleField; + parcel.valueType = StatsDimensionsValue.TUPLE_VALUE_TYPE; + parcel.tupleValue = null; + + StatsDimensionsValue sdv = new StatsDimensionsValue(parcel); + assertThat(sdv.getField()).isEqualTo(tupleField); + assertThat(sdv.getValueType()).isEqualTo(StatsDimensionsValue.TUPLE_VALUE_TYPE); + List<StatsDimensionsValue> sdvChildren = sdv.getTupleValueList(); + assertThat(sdvChildren.size()).isEqualTo(0); + } +} diff --git a/apex/statsd/framework/test/src/android/util/StatsEventTest.java b/apex/statsd/framework/test/src/android/util/StatsEventTest.java new file mode 100644 index 000000000000..8d263699d9c8 --- /dev/null +++ b/apex/statsd/framework/test/src/android/util/StatsEventTest.java @@ -0,0 +1,818 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.os.SystemClock; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.google.common.collect.Range; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Random; + +/** + * Internal tests for {@link StatsEvent}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class StatsEventTest { + + @Test + public void testNoFields() { + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = StatsEvent.newBuilder().usePooledBuffer().build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + final int expectedAtomId = 0; + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()).isEqualTo(3); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp)); + + assertWithMessage("Second element is not atom id") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect atom id") + .that(buffer.getInt()).isEqualTo(expectedAtomId); + + assertWithMessage("Third element is not errors type") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_ERRORS); + + final int errorMask = buffer.getInt(); + + assertWithMessage("ERROR_NO_ATOM_ID should be the only error in the error mask") + .that(errorMask).isEqualTo(StatsEvent.ERROR_NO_ATOM_ID); + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + @Test + public void testOnlyAtomId() { + final int expectedAtomId = 109; + + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = StatsEvent.newBuilder() + .setAtomId(expectedAtomId) + .usePooledBuffer() + .build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()).isEqualTo(2); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp)); + + assertWithMessage("Second element is not atom id") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect atom id") + .that(buffer.getInt()).isEqualTo(expectedAtomId); + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + @Test + public void testIntBooleanIntInt() { + final int expectedAtomId = 109; + final int field1 = 1; + final boolean field2 = true; + final int field3 = 3; + final int field4 = 4; + + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = StatsEvent.newBuilder() + .setAtomId(expectedAtomId) + .writeInt(field1) + .writeBoolean(field2) + .writeInt(field3) + .writeInt(field4) + .usePooledBuffer() + .build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()).isEqualTo(6); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp)); + + assertWithMessage("Second element is not atom id") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect atom id") + .that(buffer.getInt()).isEqualTo(expectedAtomId); + + assertWithMessage("First field is not Int") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect field 1") + .that(buffer.getInt()).isEqualTo(field1); + + assertWithMessage("Second field is not Boolean") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BOOLEAN); + + assertWithMessage("Incorrect field 2") + .that(buffer.get()).isEqualTo(1); + + assertWithMessage("Third field is not Int") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect field 3") + .that(buffer.getInt()).isEqualTo(field3); + + assertWithMessage("Fourth field is not Int") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect field 4") + .that(buffer.getInt()).isEqualTo(field4); + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + @Test + public void testStringFloatByteArray() { + final int expectedAtomId = 109; + final String field1 = "Str 1"; + final float field2 = 9.334f; + final byte[] field3 = new byte[] { 56, 23, 89, -120 }; + + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = StatsEvent.newBuilder() + .setAtomId(expectedAtomId) + .writeString(field1) + .writeFloat(field2) + .writeByteArray(field3) + .usePooledBuffer() + .build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()).isEqualTo(5); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp)); + + assertWithMessage("Second element is not atom id") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect atom id") + .that(buffer.getInt()).isEqualTo(expectedAtomId); + + assertWithMessage("First field is not String") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_STRING); + + final String field1Actual = getStringFromByteBuffer(buffer); + assertWithMessage("Incorrect field 1") + .that(field1Actual).isEqualTo(field1); + + assertWithMessage("Second field is not Float") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_FLOAT); + + assertWithMessage("Incorrect field 2") + .that(buffer.getFloat()).isEqualTo(field2); + + assertWithMessage("Third field is not byte array") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BYTE_ARRAY); + + final byte[] field3Actual = getByteArrayFromByteBuffer(buffer); + assertWithMessage("Incorrect field 3") + .that(field3Actual).isEqualTo(field3); + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + @Test + public void testAttributionChainLong() { + final int expectedAtomId = 109; + final int[] uids = new int[] { 1, 2, 3, 4, 5 }; + final String[] tags = new String[] { "1", "2", "3", "4", "5" }; + final long field2 = -230909823L; + + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = StatsEvent.newBuilder() + .setAtomId(expectedAtomId) + .writeAttributionChain(uids, tags) + .writeLong(field2) + .usePooledBuffer() + .build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()).isEqualTo(4); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp)); + + assertWithMessage("Second element is not atom id") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect atom id") + .that(buffer.getInt()).isEqualTo(expectedAtomId); + + assertWithMessage("First field is not Attribution Chain") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_ATTRIBUTION_CHAIN); + + assertWithMessage("Incorrect number of attribution nodes") + .that(buffer.get()).isEqualTo((byte) uids.length); + + for (int i = 0; i < tags.length; i++) { + assertWithMessage("Incorrect uid in Attribution Chain") + .that(buffer.getInt()).isEqualTo(uids[i]); + + final String tag = getStringFromByteBuffer(buffer); + assertWithMessage("Incorrect tag in Attribution Chain") + .that(tag).isEqualTo(tags[i]); + } + + assertWithMessage("Second field is not Long") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect field 2") + .that(buffer.getLong()).isEqualTo(field2); + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + @Test + public void testKeyValuePairs() { + final int expectedAtomId = 109; + final SparseIntArray intMap = new SparseIntArray(); + final SparseLongArray longMap = new SparseLongArray(); + final SparseArray<String> stringMap = new SparseArray<>(); + final SparseArray<Float> floatMap = new SparseArray<>(); + intMap.put(1, -1); + intMap.put(2, -2); + stringMap.put(3, "abc"); + stringMap.put(4, "2h"); + floatMap.put(9, -234.344f); + + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = StatsEvent.newBuilder() + .setAtomId(expectedAtomId) + .writeKeyValuePairs(intMap, longMap, stringMap, floatMap) + .usePooledBuffer() + .build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()).isEqualTo(3); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp)); + + assertWithMessage("Second element is not atom id") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect atom id") + .that(buffer.getInt()).isEqualTo(expectedAtomId); + + assertWithMessage("First field is not KeyValuePairs") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_KEY_VALUE_PAIRS); + + assertWithMessage("Incorrect number of key value pairs") + .that(buffer.get()).isEqualTo( + (byte) (intMap.size() + longMap.size() + stringMap.size() + + floatMap.size())); + + for (int i = 0; i < intMap.size(); i++) { + assertWithMessage("Incorrect key in intMap") + .that(buffer.getInt()).isEqualTo(intMap.keyAt(i)); + assertWithMessage("The type id of the value should be TYPE_INT in intMap") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + assertWithMessage("Incorrect value in intMap") + .that(buffer.getInt()).isEqualTo(intMap.valueAt(i)); + } + + for (int i = 0; i < longMap.size(); i++) { + assertWithMessage("Incorrect key in longMap") + .that(buffer.getInt()).isEqualTo(longMap.keyAt(i)); + assertWithMessage("The type id of the value should be TYPE_LONG in longMap") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG); + assertWithMessage("Incorrect value in longMap") + .that(buffer.getLong()).isEqualTo(longMap.valueAt(i)); + } + + for (int i = 0; i < stringMap.size(); i++) { + assertWithMessage("Incorrect key in stringMap") + .that(buffer.getInt()).isEqualTo(stringMap.keyAt(i)); + assertWithMessage("The type id of the value should be TYPE_STRING in stringMap") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_STRING); + final String value = getStringFromByteBuffer(buffer); + assertWithMessage("Incorrect value in stringMap") + .that(value).isEqualTo(stringMap.valueAt(i)); + } + + for (int i = 0; i < floatMap.size(); i++) { + assertWithMessage("Incorrect key in floatMap") + .that(buffer.getInt()).isEqualTo(floatMap.keyAt(i)); + assertWithMessage("The type id of the value should be TYPE_FLOAT in floatMap") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_FLOAT); + assertWithMessage("Incorrect value in floatMap") + .that(buffer.getFloat()).isEqualTo(floatMap.valueAt(i)); + } + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + @Test + public void testSingleAnnotations() { + final int expectedAtomId = 109; + final int field1 = 1; + final byte field1AnnotationId = 45; + final boolean field1AnnotationValue = false; + final boolean field2 = true; + final byte field2AnnotationId = 1; + final int field2AnnotationValue = 23; + + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = StatsEvent.newBuilder() + .setAtomId(expectedAtomId) + .writeInt(field1) + .addBooleanAnnotation(field1AnnotationId, field1AnnotationValue) + .writeBoolean(field2) + .addIntAnnotation(field2AnnotationId, field2AnnotationValue) + .usePooledBuffer() + .build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()).isEqualTo(4); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp)); + + assertWithMessage("Second element is not atom id") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect atom id") + .that(buffer.getInt()).isEqualTo(expectedAtomId); + + final byte field1Header = buffer.get(); + final int field1AnnotationValueCount = field1Header >> 4; + final byte field1Type = (byte) (field1Header & 0x0F); + assertWithMessage("First field is not Int") + .that(field1Type).isEqualTo(StatsEvent.TYPE_INT); + assertWithMessage("First field annotation count is wrong") + .that(field1AnnotationValueCount).isEqualTo(1); + assertWithMessage("Incorrect field 1") + .that(buffer.getInt()).isEqualTo(field1); + assertWithMessage("First field's annotation id is wrong") + .that(buffer.get()).isEqualTo(field1AnnotationId); + assertWithMessage("First field's annotation type is wrong") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BOOLEAN); + assertWithMessage("First field's annotation value is wrong") + .that(buffer.get()).isEqualTo(field1AnnotationValue ? 1 : 0); + + final byte field2Header = buffer.get(); + final int field2AnnotationValueCount = field2Header >> 4; + final byte field2Type = (byte) (field2Header & 0x0F); + assertWithMessage("Second field is not boolean") + .that(field2Type).isEqualTo(StatsEvent.TYPE_BOOLEAN); + assertWithMessage("Second field annotation count is wrong") + .that(field2AnnotationValueCount).isEqualTo(1); + assertWithMessage("Incorrect field 2") + .that(buffer.get()).isEqualTo(field2 ? 1 : 0); + assertWithMessage("Second field's annotation id is wrong") + .that(buffer.get()).isEqualTo(field2AnnotationId); + assertWithMessage("Second field's annotation type is wrong") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + assertWithMessage("Second field's annotation value is wrong") + .that(buffer.getInt()).isEqualTo(field2AnnotationValue); + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + @Test + public void testAtomIdAnnotations() { + final int expectedAtomId = 109; + final byte atomAnnotationId = 84; + final int atomAnnotationValue = 9; + final int field1 = 1; + final byte field1AnnotationId = 45; + final boolean field1AnnotationValue = false; + final boolean field2 = true; + final byte field2AnnotationId = 1; + final int field2AnnotationValue = 23; + + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = StatsEvent.newBuilder() + .setAtomId(expectedAtomId) + .addIntAnnotation(atomAnnotationId, atomAnnotationValue) + .writeInt(field1) + .addBooleanAnnotation(field1AnnotationId, field1AnnotationValue) + .writeBoolean(field2) + .addIntAnnotation(field2AnnotationId, field2AnnotationValue) + .usePooledBuffer() + .build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()).isEqualTo(4); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp)); + + final byte atomIdHeader = buffer.get(); + final int atomIdAnnotationValueCount = atomIdHeader >> 4; + final byte atomIdValueType = (byte) (atomIdHeader & 0x0F); + assertWithMessage("Second element is not atom id") + .that(atomIdValueType).isEqualTo(StatsEvent.TYPE_INT); + assertWithMessage("Atom id annotation count is wrong") + .that(atomIdAnnotationValueCount).isEqualTo(1); + assertWithMessage("Incorrect atom id") + .that(buffer.getInt()).isEqualTo(expectedAtomId); + assertWithMessage("Atom id's annotation id is wrong") + .that(buffer.get()).isEqualTo(atomAnnotationId); + assertWithMessage("Atom id's annotation type is wrong") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + assertWithMessage("Atom id's annotation value is wrong") + .that(buffer.getInt()).isEqualTo(atomAnnotationValue); + + final byte field1Header = buffer.get(); + final int field1AnnotationValueCount = field1Header >> 4; + final byte field1Type = (byte) (field1Header & 0x0F); + assertWithMessage("First field is not Int") + .that(field1Type).isEqualTo(StatsEvent.TYPE_INT); + assertWithMessage("First field annotation count is wrong") + .that(field1AnnotationValueCount).isEqualTo(1); + assertWithMessage("Incorrect field 1") + .that(buffer.getInt()).isEqualTo(field1); + assertWithMessage("First field's annotation id is wrong") + .that(buffer.get()).isEqualTo(field1AnnotationId); + assertWithMessage("First field's annotation type is wrong") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BOOLEAN); + assertWithMessage("First field's annotation value is wrong") + .that(buffer.get()).isEqualTo(field1AnnotationValue ? 1 : 0); + + final byte field2Header = buffer.get(); + final int field2AnnotationValueCount = field2Header >> 4; + final byte field2Type = (byte) (field2Header & 0x0F); + assertWithMessage("Second field is not boolean") + .that(field2Type).isEqualTo(StatsEvent.TYPE_BOOLEAN); + assertWithMessage("Second field annotation count is wrong") + .that(field2AnnotationValueCount).isEqualTo(1); + assertWithMessage("Incorrect field 2") + .that(buffer.get()).isEqualTo(field2 ? 1 : 0); + assertWithMessage("Second field's annotation id is wrong") + .that(buffer.get()).isEqualTo(field2AnnotationId); + assertWithMessage("Second field's annotation type is wrong") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + assertWithMessage("Second field's annotation value is wrong") + .that(buffer.getInt()).isEqualTo(field2AnnotationValue); + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + @Test + public void testSetAtomIdNotCalledImmediately() { + final int expectedAtomId = 109; + final int field1 = 25; + final boolean field2 = true; + + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = StatsEvent.newBuilder() + .writeInt(field1) + .setAtomId(expectedAtomId) + .writeBoolean(field2) + .usePooledBuffer() + .build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()).isEqualTo(3); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp)); + + assertWithMessage("Second element is not atom id") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect atom id") + .that(buffer.getInt()).isEqualTo(expectedAtomId); + + assertWithMessage("Third element is not errors type") + .that(buffer.get()).isEqualTo(StatsEvent.TYPE_ERRORS); + + final int errorMask = buffer.getInt(); + + assertWithMessage("ERROR_ATOM_ID_INVALID_POSITION should be the only error in the mask") + .that(errorMask).isEqualTo(StatsEvent.ERROR_ATOM_ID_INVALID_POSITION); + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + @Test + public void testLargePulledEvent() { + final int expectedAtomId = 10_020; + byte[] field1 = new byte[10 * 1024]; + new Random().nextBytes(field1); + + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = + StatsEvent.newBuilder().setAtomId(expectedAtomId).writeByteArray(field1).build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()) + .isEqualTo(3); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()) + .isIn(Range.closed(minTimestamp, maxTimestamp)); + + assertWithMessage("Second element is not atom id") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId); + + assertWithMessage("Third element is not byte array") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_BYTE_ARRAY); + + final byte[] field1Actual = getByteArrayFromByteBuffer(buffer); + assertWithMessage("Incorrect field 1").that(field1Actual).isEqualTo(field1); + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + @Test + public void testPulledEventOverflow() { + final int expectedAtomId = 10_020; + byte[] field1 = new byte[50 * 1024]; + new Random().nextBytes(field1); + + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = + StatsEvent.newBuilder().setAtomId(expectedAtomId).writeByteArray(field1).build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()) + .isEqualTo(3); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()) + .isIn(Range.closed(minTimestamp, maxTimestamp)); + + assertWithMessage("Second element is not atom id") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId); + + assertWithMessage("Third element is not errors type") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_ERRORS); + + final int errorMask = buffer.getInt(); + + assertWithMessage("ERROR_OVERFLOW should be the only error in the error mask") + .that(errorMask) + .isEqualTo(StatsEvent.ERROR_OVERFLOW); + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + @Test + public void testPushedEventOverflow() { + final int expectedAtomId = 10_020; + byte[] field1 = new byte[10 * 1024]; + new Random().nextBytes(field1); + + final long minTimestamp = SystemClock.elapsedRealtimeNanos(); + final StatsEvent statsEvent = StatsEvent.newBuilder() + .setAtomId(expectedAtomId) + .writeByteArray(field1) + .usePooledBuffer() + .build(); + final long maxTimestamp = SystemClock.elapsedRealtimeNanos(); + + assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId); + + final ByteBuffer buffer = + ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + + assertWithMessage("Root element in buffer is not TYPE_OBJECT") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_OBJECT); + + assertWithMessage("Incorrect number of elements in root object") + .that(buffer.get()) + .isEqualTo(3); + + assertWithMessage("First element is not timestamp") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_LONG); + + assertWithMessage("Incorrect timestamp") + .that(buffer.getLong()) + .isIn(Range.closed(minTimestamp, maxTimestamp)); + + assertWithMessage("Second element is not atom id") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_INT); + + assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId); + + assertWithMessage("Third element is not errors type") + .that(buffer.get()) + .isEqualTo(StatsEvent.TYPE_ERRORS); + + final int errorMask = buffer.getInt(); + + assertWithMessage("ERROR_OVERFLOW should be the only error in the error mask") + .that(errorMask) + .isEqualTo(StatsEvent.ERROR_OVERFLOW); + + assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position()); + + statsEvent.release(); + } + + private static byte[] getByteArrayFromByteBuffer(final ByteBuffer buffer) { + final int numBytes = buffer.getInt(); + byte[] bytes = new byte[numBytes]; + buffer.get(bytes); + return bytes; + } + + private static String getStringFromByteBuffer(final ByteBuffer buffer) { + final byte[] bytes = getByteArrayFromByteBuffer(buffer); + return new String(bytes, UTF_8); + } +} diff --git a/apex/statsd/jni/android_util_StatsLog.cpp b/apex/statsd/jni/android_util_StatsLog.cpp new file mode 100644 index 000000000000..71ce94923c8d --- /dev/null +++ b/apex/statsd/jni/android_util_StatsLog.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NAMESPACE "StatsLog.tag." +#define LOG_TAG "StatsLog_println" + +#include <jni.h> +#include <log/log.h> +#include <nativehelper/scoped_local_ref.h> +#include "stats_buffer_writer.h" + +namespace android { + +static void android_util_StatsLog_write(JNIEnv* env, jobject clazz, jbyteArray buf, jint size, + jint atomId) { + if (buf == NULL) { + return; + } + jint actualSize = env->GetArrayLength(buf); + if (actualSize < size) { + return; + } + + jbyte* bufferArray = env->GetByteArrayElements(buf, NULL); + if (bufferArray == NULL) { + return; + } + + write_buffer_to_statsd((void*) bufferArray, size, atomId); + + env->ReleaseByteArrayElements(buf, bufferArray, 0); +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + { "writeImpl", "([BII)V", (void*) android_util_StatsLog_write }, +}; + +int register_android_util_StatsLog(JNIEnv* env) +{ + static const char* kStatsLogClass = "android/util/StatsLog"; + + ScopedLocalRef<jclass> cls(env, env->FindClass(kStatsLogClass)); + if (cls.get() == nullptr) { + ALOGE("jni statsd registration failure, class not found '%s'", kStatsLogClass); + return JNI_ERR; + } + + const jint count = sizeof(gMethods) / sizeof(gMethods[0]); + int status = env->RegisterNatives(cls.get(), gMethods, count); + if (status < 0) { + ALOGE("jni statsd registration failure, status: %d", status); + return JNI_ERR; + } + return JNI_VERSION_1_4; +} + +}; // namespace android + +/* + * JNI Initialization + */ +jint JNI_OnLoad(JavaVM* jvm, void* reserved) { + JNIEnv* e; + + ALOGV("statsd : loading JNI\n"); + // Check JNI version + if (jvm->GetEnv((void**)&e, JNI_VERSION_1_4)) { + ALOGE("JNI version mismatch error"); + return JNI_ERR; + } + + return android::register_android_util_StatsLog(e); +} diff --git a/apex/statsd/service/Android.bp b/apex/statsd/service/Android.bp new file mode 100644 index 000000000000..df0ccfc54d64 --- /dev/null +++ b/apex/statsd/service/Android.bp @@ -0,0 +1,35 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +filegroup { + name: "service-statsd-sources", + srcs: [ + "java/**/*.java", + ], +} + +java_library { + name: "service-statsd", + srcs: [ ":service-statsd-sources" ], + sdk_version: "system_server_current", + libs: [ + "framework-annotations-lib", + "framework-statsd", + ], + plugins: ["java_api_finder"], + apex_available: [ + "com.android.os.statsd", + "test_com.android.os.statsd", + ], +} diff --git a/apex/statsd/service/java/com/android/server/stats/StatsCompanion.java b/apex/statsd/service/java/com/android/server/stats/StatsCompanion.java new file mode 100644 index 000000000000..dc477a5590ea --- /dev/null +++ b/apex/statsd/service/java/com/android/server/stats/StatsCompanion.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.stats; + +import android.app.PendingIntent; +import android.app.StatsManager; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.IPendingIntentRef; +import android.os.Process; +import android.os.StatsDimensionsValue; +import android.os.StatsDimensionsValueParcel; +import android.util.Log; + +import com.android.server.SystemService; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * @hide + */ +public class StatsCompanion { + private static final String TAG = "StatsCompanion"; + private static final boolean DEBUG = false; + + private static final int AID_STATSD = 1066; + + private static final String STATS_COMPANION_SERVICE = "statscompanion"; + private static final String STATS_MANAGER_SERVICE = "statsmanager"; + + static void enforceStatsdCallingUid() { + if (Binder.getCallingPid() == Process.myPid()) { + return; + } + if (Binder.getCallingUid() != AID_STATSD) { + throw new SecurityException("Not allowed to access StatsCompanion"); + } + } + + /** + * Lifecycle class for both {@link StatsCompanionService} and {@link StatsManagerService}. + */ + public static final class Lifecycle extends SystemService { + private StatsCompanionService mStatsCompanionService; + private StatsManagerService mStatsManagerService; + + public Lifecycle(Context context) { + super(context); + } + + @Override + public void onStart() { + mStatsCompanionService = new StatsCompanionService(getContext()); + mStatsManagerService = new StatsManagerService(getContext()); + mStatsCompanionService.setStatsManagerService(mStatsManagerService); + mStatsManagerService.setStatsCompanionService(mStatsCompanionService); + + try { + publishBinderService(STATS_COMPANION_SERVICE, mStatsCompanionService); + if (DEBUG) Log.d(TAG, "Published " + STATS_COMPANION_SERVICE); + publishBinderService(STATS_MANAGER_SERVICE, mStatsManagerService); + if (DEBUG) Log.d(TAG, "Published " + STATS_MANAGER_SERVICE); + } catch (Exception e) { + Log.e(TAG, "Failed to publishBinderService", e); + } + } + + @Override + public void onBootPhase(int phase) { + super.onBootPhase(phase); + if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + mStatsCompanionService.systemReady(); + } + if (phase == PHASE_BOOT_COMPLETED) { + mStatsCompanionService.bootCompleted(); + } + } + } + + /** + * Wrapper for {@link PendingIntent}. Allows Statsd to send PendingIntents. + */ + public static class PendingIntentRef extends IPendingIntentRef.Stub { + + private static final String TAG = "PendingIntentRef"; + + /** + * The last report time is provided with each intent registered to + * StatsManager#setFetchReportsOperation. This allows easy de-duping in the receiver if + * statsd is requesting the client to retrieve the same statsd data. The last report time + * corresponds to the last_report_elapsed_nanos that will provided in the current + * ConfigMetricsReport, and this timestamp also corresponds to the + * current_report_elapsed_nanos of the most recently obtained ConfigMetricsReport. + */ + private static final String EXTRA_LAST_REPORT_TIME = "android.app.extra.LAST_REPORT_TIME"; + private static final int CODE_DATA_BROADCAST = 1; + private static final int CODE_ACTIVE_CONFIGS_BROADCAST = 1; + private static final int CODE_SUBSCRIBER_BROADCAST = 1; + + private final PendingIntent mPendingIntent; + private final Context mContext; + + public PendingIntentRef(PendingIntent pendingIntent, Context context) { + mPendingIntent = pendingIntent; + mContext = context; + } + + @Override + public void sendDataBroadcast(long lastReportTimeNs) { + enforceStatsdCallingUid(); + Intent intent = new Intent(); + intent.putExtra(EXTRA_LAST_REPORT_TIME, lastReportTimeNs); + try { + mPendingIntent.send(mContext, CODE_DATA_BROADCAST, intent, null, null); + } catch (PendingIntent.CanceledException e) { + Log.w(TAG, "Unable to send PendingIntent"); + } + } + + @Override + public void sendActiveConfigsChangedBroadcast(long[] configIds) { + enforceStatsdCallingUid(); + Intent intent = new Intent(); + intent.putExtra(StatsManager.EXTRA_STATS_ACTIVE_CONFIG_KEYS, configIds); + try { + mPendingIntent.send(mContext, CODE_ACTIVE_CONFIGS_BROADCAST, intent, null, null); + if (DEBUG) { + Log.d(TAG, "Sent broadcast with config ids " + Arrays.toString(configIds)); + } + } catch (PendingIntent.CanceledException e) { + Log.w(TAG, "Unable to send active configs changed broadcast using PendingIntent"); + } + } + + @Override + public void sendSubscriberBroadcast(long configUid, long configId, long subscriptionId, + long subscriptionRuleId, String[] cookies, + StatsDimensionsValueParcel dimensionsValueParcel) { + enforceStatsdCallingUid(); + StatsDimensionsValue dimensionsValue = new StatsDimensionsValue(dimensionsValueParcel); + Intent intent = + new Intent() + .putExtra(StatsManager.EXTRA_STATS_CONFIG_UID, configUid) + .putExtra(StatsManager.EXTRA_STATS_CONFIG_KEY, configId) + .putExtra(StatsManager.EXTRA_STATS_SUBSCRIPTION_ID, subscriptionId) + .putExtra(StatsManager.EXTRA_STATS_SUBSCRIPTION_RULE_ID, + subscriptionRuleId) + .putExtra(StatsManager.EXTRA_STATS_DIMENSIONS_VALUE, dimensionsValue); + + ArrayList<String> cookieList = new ArrayList<>(cookies.length); + cookieList.addAll(Arrays.asList(cookies)); + intent.putStringArrayListExtra( + StatsManager.EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES, cookieList); + + if (DEBUG) { + Log.d(TAG, + String.format( + "Statsd sendSubscriberBroadcast with params {%d %d %d %d %s %s}", + configUid, configId, subscriptionId, subscriptionRuleId, + Arrays.toString(cookies), + dimensionsValue)); + } + try { + mPendingIntent.send(mContext, CODE_SUBSCRIBER_BROADCAST, intent, null, null); + } catch (PendingIntent.CanceledException e) { + Log.w(TAG, + "Unable to send using PendingIntent from uid " + configUid + + "; presumably it had been cancelled."); + } + } + } +} diff --git a/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java b/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java new file mode 100644 index 000000000000..cbc8ed636ff2 --- /dev/null +++ b/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java @@ -0,0 +1,817 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.stats; + +import static android.os.Process.THREAD_PRIORITY_BACKGROUND; + +import android.app.AlarmManager; +import android.app.AlarmManager.OnAlarmListener; +import android.app.StatsManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Binder; +import android.os.Bundle; +import android.os.FileUtils; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.IStatsCompanionService; +import android.os.IStatsd; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.StatsFrameworkInitializer; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Helper service for statsd (the native stats management service in cmds/statsd/). + * Used for registering and receiving alarms on behalf of statsd. + * + * @hide + */ +public class StatsCompanionService extends IStatsCompanionService.Stub { + + private static final long MILLIS_IN_A_DAY = TimeUnit.DAYS.toMillis(1); + + public static final String RESULT_RECEIVER_CONTROLLER_KEY = "controller_activity"; + public static final String CONFIG_DIR = "/data/misc/stats-service"; + + static final String TAG = "StatsCompanionService"; + static final boolean DEBUG = false; + /** + * Hard coded field ids of frameworks/base/cmds/statsd/src/uid_data.proto + * to be used in ProtoOutputStream. + */ + private static final int APPLICATION_INFO_FIELD_ID = 1; + private static final int UID_FIELD_ID = 1; + private static final int VERSION_FIELD_ID = 2; + private static final int VERSION_STRING_FIELD_ID = 3; + private static final int PACKAGE_NAME_FIELD_ID = 4; + private static final int INSTALLER_FIELD_ID = 5; + + public static final int DEATH_THRESHOLD = 10; + + static final class CompanionHandler extends Handler { + CompanionHandler(Looper looper) { + super(looper); + } + } + + private final Context mContext; + private final AlarmManager mAlarmManager; + @GuardedBy("sStatsdLock") + private static IStatsd sStatsd; + private static final Object sStatsdLock = new Object(); + + private final OnAlarmListener mAnomalyAlarmListener; + private final OnAlarmListener mPullingAlarmListener; + private final OnAlarmListener mPeriodicAlarmListener; + + private StatsManagerService mStatsManagerService; + + @GuardedBy("sStatsdLock") + private final HashSet<Long> mDeathTimeMillis = new HashSet<>(); + @GuardedBy("sStatsdLock") + private final HashMap<Long, String> mDeletedFiles = new HashMap<>(); + private final CompanionHandler mHandler; + + // Flag that is set when PHASE_BOOT_COMPLETED is triggered in the StatsCompanion lifecycle. + private AtomicBoolean mBootCompleted = new AtomicBoolean(false); + + public StatsCompanionService(Context context) { + super(); + mContext = context; + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + if (DEBUG) Log.d(TAG, "Registered receiver for ACTION_PACKAGE_REPLACED and ADDED."); + HandlerThread handlerThread = new HandlerThread(TAG); + handlerThread.start(); + mHandler = new CompanionHandler(handlerThread.getLooper()); + + mAnomalyAlarmListener = new AnomalyAlarmListener(context); + mPullingAlarmListener = new PullingAlarmListener(context); + mPeriodicAlarmListener = new PeriodicAlarmListener(context); + } + + private final static int[] toIntArray(List<Integer> list) { + int[] ret = new int[list.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = list.get(i); + } + return ret; + } + + private final static long[] toLongArray(List<Long> list) { + long[] ret = new long[list.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = list.get(i); + } + return ret; + } + + /** + * Non-blocking call to retrieve a reference to statsd + * + * @return IStatsd object if statsd is ready, null otherwise. + */ + private static IStatsd getStatsdNonblocking() { + synchronized (sStatsdLock) { + return sStatsd; + } + } + + private static void informAllUids(Context context) { + ParcelFileDescriptor[] fds; + try { + fds = ParcelFileDescriptor.createPipe(); + } catch (IOException e) { + Log.e(TAG, "Failed to create a pipe to send uid map data.", e); + return; + } + HandlerThread backgroundThread = new HandlerThread( + "statsCompanionService.bg", THREAD_PRIORITY_BACKGROUND); + backgroundThread.start(); + Handler handler = new Handler(backgroundThread.getLooper()); + handler.post(() -> { + UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); + PackageManager pm = context.getPackageManager(); + final List<UserHandle> users = um.getUserHandles(true); + if (DEBUG) { + Log.d(TAG, "Iterating over " + users.size() + " userHandles."); + } + IStatsd statsd = getStatsdNonblocking(); + if (statsd == null) { + return; + } + try { + statsd.informAllUidData(fds[0]); + } catch (RemoteException e) { + Log.e(TAG, "Failed to send uid map to statsd"); + } + try { + fds[0].close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close the read side of the pipe.", e); + } + final ParcelFileDescriptor writeFd = fds[1]; + FileOutputStream fout = new ParcelFileDescriptor.AutoCloseOutputStream(writeFd); + try { + ProtoOutputStream output = new ProtoOutputStream(fout); + int numRecords = 0; + // Add in all the apps for every user/profile. + for (UserHandle userHandle : users) { + List<PackageInfo> pi = + pm.getInstalledPackagesAsUser(PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.MATCH_ANY_USER + | PackageManager.MATCH_APEX, + userHandle.getIdentifier()); + for (int j = 0; j < pi.size(); j++) { + if (pi.get(j).applicationInfo != null) { + String installer; + try { + installer = pm.getInstallerPackageName(pi.get(j).packageName); + } catch (IllegalArgumentException e) { + installer = ""; + } + long applicationInfoToken = + output.start(ProtoOutputStream.FIELD_TYPE_MESSAGE + | ProtoOutputStream.FIELD_COUNT_REPEATED + | APPLICATION_INFO_FIELD_ID); + output.write(ProtoOutputStream.FIELD_TYPE_INT32 + | ProtoOutputStream.FIELD_COUNT_SINGLE | UID_FIELD_ID, + pi.get(j).applicationInfo.uid); + output.write(ProtoOutputStream.FIELD_TYPE_INT64 + | ProtoOutputStream.FIELD_COUNT_SINGLE + | VERSION_FIELD_ID, pi.get(j).getLongVersionCode()); + output.write(ProtoOutputStream.FIELD_TYPE_STRING + | ProtoOutputStream.FIELD_COUNT_SINGLE + | VERSION_STRING_FIELD_ID, + pi.get(j).versionName); + output.write(ProtoOutputStream.FIELD_TYPE_STRING + | ProtoOutputStream.FIELD_COUNT_SINGLE + | PACKAGE_NAME_FIELD_ID, pi.get(j).packageName); + output.write(ProtoOutputStream.FIELD_TYPE_STRING + | ProtoOutputStream.FIELD_COUNT_SINGLE + | INSTALLER_FIELD_ID, + installer == null ? "" : installer); + numRecords++; + output.end(applicationInfoToken); + } + } + } + output.flush(); + if (DEBUG) { + Log.d(TAG, "Sent data for " + numRecords + " apps"); + } + } finally { + FileUtils.closeQuietly(fout); + backgroundThread.quit(); + backgroundThread.interrupt(); + } + }); + } + + private static class WakelockThread extends Thread { + private final PowerManager.WakeLock mWl; + private final Runnable mRunnable; + + WakelockThread(Context context, String wakelockName, Runnable runnable) { + PowerManager powerManager = (PowerManager) + context.getSystemService(Context.POWER_SERVICE); + mWl = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, wakelockName); + mRunnable = runnable; + } + @Override + public void run() { + try { + mRunnable.run(); + } finally { + mWl.release(); + } + } + @Override + public void start() { + mWl.acquire(); + super.start(); + } + } + + private final static class AppUpdateReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + /** + * App updates actually consist of REMOVE, ADD, and then REPLACE broadcasts. To avoid + * waste, we ignore the REMOVE and ADD broadcasts that contain the replacing flag. + * If we can't find the value for EXTRA_REPLACING, we default to false. + */ + if (!intent.getAction().equals(Intent.ACTION_PACKAGE_REPLACED) + && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + return; // Keep only replacing or normal add and remove. + } + if (DEBUG) Log.d(TAG, "StatsCompanionService noticed an app was updated."); + synchronized (sStatsdLock) { + if (sStatsd == null) { + Log.w(TAG, "Could not access statsd to inform it of an app update"); + return; + } + try { + if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) { + Bundle b = intent.getExtras(); + int uid = b.getInt(Intent.EXTRA_UID); + boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); + if (!replacing) { + // Don't bother sending an update if we're right about to get another + // intent for the new version that's added. + String app = intent.getData().getSchemeSpecificPart(); + sStatsd.informOnePackageRemoved(app, uid); + } + } else { + PackageManager pm = context.getPackageManager(); + Bundle b = intent.getExtras(); + int uid = b.getInt(Intent.EXTRA_UID); + String app = intent.getData().getSchemeSpecificPart(); + PackageInfo pi = pm.getPackageInfo(app, PackageManager.MATCH_ANY_USER); + String installer; + try { + installer = pm.getInstallerPackageName(app); + } catch (IllegalArgumentException e) { + installer = ""; + } + sStatsd.informOnePackage( + app, + uid, + pi.getLongVersionCode(), + pi.versionName == null ? "" : pi.versionName, + installer == null ? "" : installer); + } + } catch (Exception e) { + Log.w(TAG, "Failed to inform statsd of an app update", e); + } + } + } + } + + private static final class UserUpdateReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + // Pull the latest state of UID->app name, version mapping. + // Needed since the new user basically has a version of every app. + informAllUids(context); + } + } + + public static final class AnomalyAlarmListener implements OnAlarmListener { + private final Context mContext; + + AnomalyAlarmListener(Context context) { + mContext = context; + } + + @Override + public void onAlarm() { + if (DEBUG) { + Log.i(TAG, "StatsCompanionService believes an anomaly has occurred at time " + + System.currentTimeMillis() + "ms."); + } + IStatsd statsd = getStatsdNonblocking(); + if (statsd == null) { + Log.w(TAG, "Could not access statsd to inform it of anomaly alarm firing"); + return; + } + + // Wakelock needs to be retained while calling statsd. + Thread thread = new WakelockThread(mContext, + AnomalyAlarmListener.class.getCanonicalName(), new Runnable() { + @Override + public void run() { + try { + statsd.informAnomalyAlarmFired(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to inform statsd of anomaly alarm firing", e); + } + } + }); + thread.start(); + } + } + + public final static class PullingAlarmListener implements OnAlarmListener { + private final Context mContext; + + PullingAlarmListener(Context context) { + mContext = context; + } + + @Override + public void onAlarm() { + if (DEBUG) { + Log.d(TAG, "Time to poll something."); + } + IStatsd statsd = getStatsdNonblocking(); + if (statsd == null) { + Log.w(TAG, "Could not access statsd to inform it of pulling alarm firing."); + return; + } + + // Wakelock needs to be retained while calling statsd. + Thread thread = new WakelockThread(mContext, + PullingAlarmListener.class.getCanonicalName(), new Runnable() { + @Override + public void run() { + try { + statsd.informPollAlarmFired(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to inform statsd of pulling alarm firing.", e); + } + } + }); + thread.start(); + } + } + + public final static class PeriodicAlarmListener implements OnAlarmListener { + private final Context mContext; + + PeriodicAlarmListener(Context context) { + mContext = context; + } + + @Override + public void onAlarm() { + if (DEBUG) { + Log.d(TAG, "Time to trigger periodic alarm."); + } + IStatsd statsd = getStatsdNonblocking(); + if (statsd == null) { + Log.w(TAG, "Could not access statsd to inform it of periodic alarm firing."); + return; + } + + // Wakelock needs to be retained while calling statsd. + Thread thread = new WakelockThread(mContext, + PeriodicAlarmListener.class.getCanonicalName(), new Runnable() { + @Override + public void run() { + try { + statsd.informAlarmForSubscriberTriggeringFired(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to inform statsd of periodic alarm firing.", e); + } + } + }); + thread.start(); + } + } + + public final static class ShutdownEventReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + /** + * Skip immediately if intent is not relevant to device shutdown. + */ + if (!intent.getAction().equals(Intent.ACTION_REBOOT) + && !(intent.getAction().equals(Intent.ACTION_SHUTDOWN) + && (intent.getFlags() & Intent.FLAG_RECEIVER_FOREGROUND) != 0)) { + return; + } + + if (DEBUG) { + Log.i(TAG, "StatsCompanionService noticed a shutdown."); + } + IStatsd statsd = getStatsdNonblocking(); + if (statsd == null) { + Log.w(TAG, "Could not access statsd to inform it of a shutdown event."); + return; + } + try { + // two way binder call + statsd.informDeviceShutdown(); + } catch (Exception e) { + Log.w(TAG, "Failed to inform statsd of a shutdown event.", e); + } + } + } + + @Override // Binder call + public void setAnomalyAlarm(long timestampMs) { + StatsCompanion.enforceStatsdCallingUid(); + if (DEBUG) Log.d(TAG, "Setting anomaly alarm for " + timestampMs); + final long callingToken = Binder.clearCallingIdentity(); + try { + // using ELAPSED_REALTIME, not ELAPSED_REALTIME_WAKEUP, so if device is asleep, will + // only fire when it awakens. + // AlarmManager will automatically cancel any previous mAnomalyAlarmListener alarm. + mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, timestampMs, TAG + ".anomaly", + mAnomalyAlarmListener, mHandler); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + @Override // Binder call + public void cancelAnomalyAlarm() { + StatsCompanion.enforceStatsdCallingUid(); + if (DEBUG) Log.d(TAG, "Cancelling anomaly alarm"); + final long callingToken = Binder.clearCallingIdentity(); + try { + mAlarmManager.cancel(mAnomalyAlarmListener); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + @Override // Binder call + public void setAlarmForSubscriberTriggering(long timestampMs) { + StatsCompanion.enforceStatsdCallingUid(); + if (DEBUG) { + Log.d(TAG, + "Setting periodic alarm in about " + (timestampMs + - SystemClock.elapsedRealtime())); + } + final long callingToken = Binder.clearCallingIdentity(); + try { + // using ELAPSED_REALTIME, not ELAPSED_REALTIME_WAKEUP, so if device is asleep, will + // only fire when it awakens. + mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, timestampMs, TAG + ".periodic", + mPeriodicAlarmListener, mHandler); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + @Override // Binder call + public void cancelAlarmForSubscriberTriggering() { + StatsCompanion.enforceStatsdCallingUid(); + if (DEBUG) { + Log.d(TAG, "Cancelling periodic alarm"); + } + final long callingToken = Binder.clearCallingIdentity(); + try { + mAlarmManager.cancel(mPeriodicAlarmListener); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + @Override // Binder call + public void setPullingAlarm(long nextPullTimeMs) { + StatsCompanion.enforceStatsdCallingUid(); + if (DEBUG) { + Log.d(TAG, "Setting pulling alarm in about " + + (nextPullTimeMs - SystemClock.elapsedRealtime())); + } + final long callingToken = Binder.clearCallingIdentity(); + try { + // using ELAPSED_REALTIME, not ELAPSED_REALTIME_WAKEUP, so if device is asleep, will + // only fire when it awakens. + mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextPullTimeMs, TAG + ".pull", + mPullingAlarmListener, mHandler); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + @Override // Binder call + public void cancelPullingAlarm() { + StatsCompanion.enforceStatsdCallingUid(); + if (DEBUG) { + Log.d(TAG, "Cancelling pulling alarm"); + } + final long callingToken = Binder.clearCallingIdentity(); + try { + mAlarmManager.cancel(mPullingAlarmListener); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + @Override // Binder call + public void statsdReady() { + StatsCompanion.enforceStatsdCallingUid(); + if (DEBUG) { + Log.d(TAG, "learned that statsdReady"); + } + sayHiToStatsd(); // tell statsd that we're ready too and link to it + + final Intent intent = new Intent(StatsManager.ACTION_STATSD_STARTED); + // Retrieve list of broadcast receivers for this broadcast & send them directed broadcasts + // to wake them up (if they're in background). + List<ResolveInfo> resolveInfos = + mContext.getPackageManager().queryBroadcastReceiversAsUser( + intent, 0, UserHandle.SYSTEM); + if (resolveInfos == null || resolveInfos.isEmpty()) { + return; // No need to send broadcast. + } + + for (ResolveInfo resolveInfo : resolveInfos) { + Intent intentToSend = new Intent(intent); + intentToSend.setComponent(new ComponentName( + resolveInfo.activityInfo.applicationInfo.packageName, + resolveInfo.activityInfo.name)); + mContext.sendBroadcastAsUser(intentToSend, UserHandle.SYSTEM, + android.Manifest.permission.DUMP); + } + } + + @Override // Binder call + public boolean checkPermission(String permission, int pid, int uid) { + StatsCompanion.enforceStatsdCallingUid(); + return mContext.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_GRANTED; + } + + // Statsd related code + + /** + * Fetches the statsd IBinder service. This is a blocking call that always refetches statsd + * instead of returning the cached sStatsd. + * Note: This should only be called from {@link #sayHiToStatsd()}. All other clients should use + * the cached sStatsd via {@link #getStatsdNonblocking()}. + */ + private IStatsd fetchStatsdServiceLocked() { + sStatsd = IStatsd.Stub.asInterface(StatsFrameworkInitializer + .getStatsServiceManager() + .getStatsdServiceRegisterer() + .get()); + return sStatsd; + } + + private void registerStatsdDeathRecipient(IStatsd statsd, List<BroadcastReceiver> receivers) { + StatsdDeathRecipient deathRecipient = new StatsdDeathRecipient(statsd, receivers); + + try { + statsd.asBinder().linkToDeath(deathRecipient, /*flags=*/0); + } catch (RemoteException e) { + Log.e(TAG, "linkToDeath (StatsdDeathRecipient) failed"); + // Statsd has already died. Unregister receivers ourselves. + for (BroadcastReceiver receiver : receivers) { + mContext.unregisterReceiver(receiver); + } + synchronized (sStatsdLock) { + if (statsd == sStatsd) { + statsdNotReadyLocked(); + } + } + } + } + + /** + * Now that the android system is ready, StatsCompanion is ready too, so inform statsd. + */ + void systemReady() { + if (DEBUG) Log.d(TAG, "Learned that systemReady"); + sayHiToStatsd(); + } + + void setStatsManagerService(StatsManagerService statsManagerService) { + mStatsManagerService = statsManagerService; + } + + /** + * Tells statsd that statscompanion is ready. If the binder call returns, link to + * statsd. + */ + private void sayHiToStatsd() { + IStatsd statsd; + synchronized (sStatsdLock) { + if (sStatsd != null && sStatsd.asBinder().isBinderAlive()) { + Log.e(TAG, "statsd has already been fetched before", + new IllegalStateException("IStatsd object should be null or dead")); + return; + } + statsd = fetchStatsdServiceLocked(); + } + + if (statsd == null) { + Log.i(TAG, "Could not yet find statsd to tell it that StatsCompanion is alive."); + return; + } + + // Cleann up from previous statsd - cancel any alarms that had been set. Do this here + // instead of in binder death because statsd can come back and set different alarms, or not + // want to set an alarm when it had been set. This guarantees that when we get a new statsd, + // we cancel any alarms before it is able to set them. + cancelAnomalyAlarm(); + cancelPullingAlarm(); + cancelAlarmForSubscriberTriggering(); + + if (DEBUG) Log.d(TAG, "Saying hi to statsd"); + mStatsManagerService.statsdReady(statsd); + try { + statsd.statsCompanionReady(); + + BroadcastReceiver appUpdateReceiver = new AppUpdateReceiver(); + BroadcastReceiver userUpdateReceiver = new UserUpdateReceiver(); + BroadcastReceiver shutdownEventReceiver = new ShutdownEventReceiver(); + + // Setup broadcast receiver for updates. + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REPLACED); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + mContext.registerReceiverForAllUsers(appUpdateReceiver, filter, null, null); + + // Setup receiver for user initialize (which happens once for a new user) + // and if a user is removed. + filter = new IntentFilter(Intent.ACTION_USER_INITIALIZE); + filter.addAction(Intent.ACTION_USER_REMOVED); + mContext.registerReceiverForAllUsers(userUpdateReceiver, filter, null, null); + + // Setup receiver for device reboots or shutdowns. + filter = new IntentFilter(Intent.ACTION_REBOOT); + filter.addAction(Intent.ACTION_SHUTDOWN); + mContext.registerReceiverForAllUsers(shutdownEventReceiver, filter, null, null); + + // Register death recipient. + List<BroadcastReceiver> broadcastReceivers = + List.of(appUpdateReceiver, userUpdateReceiver, shutdownEventReceiver); + registerStatsdDeathRecipient(statsd, broadcastReceivers); + + // Tell statsd that boot has completed. The signal may have already been sent, but since + // the signal-receiving function is idempotent, that's ok. + if (mBootCompleted.get()) { + statsd.bootCompleted(); + } + + // Pull the latest state of UID->app name, version mapping when statsd starts. + informAllUids(mContext); + + Log.i(TAG, "Told statsd that StatsCompanionService is alive."); + } catch (RemoteException e) { + Log.e(TAG, "Failed to inform statsd that statscompanion is ready", e); + } + } + + private class StatsdDeathRecipient implements IBinder.DeathRecipient { + + private final IStatsd mStatsd; + private final List<BroadcastReceiver> mReceiversToUnregister; + + StatsdDeathRecipient(IStatsd statsd, List<BroadcastReceiver> receivers) { + mStatsd = statsd; + mReceiversToUnregister = receivers; + } + + // It is possible for binderDied to be called after a restarted statsd calls statsdReady, + // but that's alright because the code does not assume an ordering of the two calls. + @Override + public void binderDied() { + Log.i(TAG, "Statsd is dead - erase all my knowledge, except pullers"); + synchronized (sStatsdLock) { + long now = SystemClock.elapsedRealtime(); + for (Long timeMillis : mDeathTimeMillis) { + long ageMillis = now - timeMillis; + if (ageMillis > MILLIS_IN_A_DAY) { + mDeathTimeMillis.remove(timeMillis); + } + } + for (Long timeMillis : mDeletedFiles.keySet()) { + long ageMillis = now - timeMillis; + if (ageMillis > MILLIS_IN_A_DAY * 7) { + mDeletedFiles.remove(timeMillis); + } + } + mDeathTimeMillis.add(now); + if (mDeathTimeMillis.size() >= DEATH_THRESHOLD) { + mDeathTimeMillis.clear(); + File[] configs = new File(CONFIG_DIR).listFiles(); + if (configs != null && configs.length > 0) { + String fileName = configs[0].getName(); + if (configs[0].delete()) { + mDeletedFiles.put(now, fileName); + } + } + } + + // Unregister receivers on death because receivers can only be unregistered once. + // Otherwise, an IllegalArgumentException is thrown. + for (BroadcastReceiver receiver: mReceiversToUnregister) { + mContext.unregisterReceiver(receiver); + } + + // It's possible for statsd to have restarted and called statsdReady, causing a new + // sStatsd binder object to be fetched, before the binderDied callback runs. Only + // call #statsdNotReadyLocked if that hasn't happened yet. + if (mStatsd == sStatsd) { + statsdNotReadyLocked(); + } + } + } + } + + private void statsdNotReadyLocked() { + sStatsd = null; + mStatsManagerService.statsdNotReady(); + } + + void bootCompleted() { + mBootCompleted.set(true); + IStatsd statsd = getStatsdNonblocking(); + if (statsd == null) { + // Statsd is not yet ready. + // Delay the boot completed ping to {@link #sayHiToStatsd()} + return; + } + try { + statsd.bootCompleted(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to notify statsd that boot completed"); + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + return; + } + + synchronized (sStatsdLock) { + writer.println("Number of configuration files deleted: " + mDeletedFiles.size()); + if (mDeletedFiles.size() > 0) { + writer.println(" timestamp, deleted file name"); + } + long lastBootMillis = + SystemClock.currentThreadTimeMillis() - SystemClock.elapsedRealtime(); + for (Long elapsedMillis : mDeletedFiles.keySet()) { + long deletionMillis = lastBootMillis + elapsedMillis; + writer.println(" " + deletionMillis + ", " + mDeletedFiles.get(elapsedMillis)); + } + } + } +} diff --git a/apex/statsd/service/java/com/android/server/stats/StatsManagerService.java b/apex/statsd/service/java/com/android/server/stats/StatsManagerService.java new file mode 100644 index 000000000000..97846f2397a5 --- /dev/null +++ b/apex/statsd/service/java/com/android/server/stats/StatsManagerService.java @@ -0,0 +1,661 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.stats; + +import static com.android.server.stats.StatsCompanion.PendingIntentRef; + +import android.Manifest; +import android.annotation.Nullable; +import android.app.AppOpsManager; +import android.app.PendingIntent; +import android.content.Context; +import android.os.Binder; +import android.os.IPullAtomCallback; +import android.os.IStatsManagerService; +import android.os.IStatsd; +import android.os.Process; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +import java.util.Map; +import java.util.Objects; + +/** + * Service for {@link android.app.StatsManager}. + * + * @hide + */ +public class StatsManagerService extends IStatsManagerService.Stub { + + private static final String TAG = "StatsManagerService"; + private static final boolean DEBUG = false; + + private static final int STATSD_TIMEOUT_MILLIS = 5000; + + private static final String USAGE_STATS_PERMISSION_OPS = "android:get_usage_stats"; + + @GuardedBy("mLock") + private IStatsd mStatsd; + private final Object mLock = new Object(); + + private StatsCompanionService mStatsCompanionService; + private Context mContext; + + @GuardedBy("mLock") + private ArrayMap<ConfigKey, PendingIntentRef> mDataFetchPirMap = new ArrayMap<>(); + @GuardedBy("mLock") + private ArrayMap<Integer, PendingIntentRef> mActiveConfigsPirMap = new ArrayMap<>(); + @GuardedBy("mLock") + private ArrayMap<ConfigKey, ArrayMap<Long, PendingIntentRef>> mBroadcastSubscriberPirMap = + new ArrayMap<>(); + + public StatsManagerService(Context context) { + super(); + mContext = context; + } + + private static class ConfigKey { + private final int mUid; + private final long mConfigId; + + ConfigKey(int uid, long configId) { + mUid = uid; + mConfigId = configId; + } + + public int getUid() { + return mUid; + } + + public long getConfigId() { + return mConfigId; + } + + @Override + public int hashCode() { + return Objects.hash(mUid, mConfigId); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ConfigKey) { + ConfigKey other = (ConfigKey) obj; + return this.mUid == other.getUid() && this.mConfigId == other.getConfigId(); + } + return false; + } + } + + private static class PullerKey { + private final int mUid; + private final int mAtomTag; + + PullerKey(int uid, int atom) { + mUid = uid; + mAtomTag = atom; + } + + public int getUid() { + return mUid; + } + + public int getAtom() { + return mAtomTag; + } + + @Override + public int hashCode() { + return Objects.hash(mUid, mAtomTag); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PullerKey) { + PullerKey other = (PullerKey) obj; + return this.mUid == other.getUid() && this.mAtomTag == other.getAtom(); + } + return false; + } + } + + private static class PullerValue { + private final long mCoolDownMillis; + private final long mTimeoutMillis; + private final int[] mAdditiveFields; + private final IPullAtomCallback mCallback; + + PullerValue(long coolDownMillis, long timeoutMillis, int[] additiveFields, + IPullAtomCallback callback) { + mCoolDownMillis = coolDownMillis; + mTimeoutMillis = timeoutMillis; + mAdditiveFields = additiveFields; + mCallback = callback; + } + + public long getCoolDownMillis() { + return mCoolDownMillis; + } + + public long getTimeoutMillis() { + return mTimeoutMillis; + } + + public int[] getAdditiveFields() { + return mAdditiveFields; + } + + public IPullAtomCallback getCallback() { + return mCallback; + } + } + + private final ArrayMap<PullerKey, PullerValue> mPullers = new ArrayMap<>(); + + @Override + public void registerPullAtomCallback(int atomTag, long coolDownMillis, long timeoutMillis, + int[] additiveFields, IPullAtomCallback pullerCallback) { + enforceRegisterStatsPullAtomPermission(); + if (pullerCallback == null) { + Log.w(TAG, "Puller callback is null for atom " + atomTag); + return; + } + int callingUid = Binder.getCallingUid(); + PullerKey key = new PullerKey(callingUid, atomTag); + PullerValue val = + new PullerValue(coolDownMillis, timeoutMillis, additiveFields, pullerCallback); + + // Always cache the puller in StatsManagerService. If statsd is down, we will register the + // puller when statsd comes back up. + synchronized (mLock) { + mPullers.put(key, val); + } + + IStatsd statsd = getStatsdNonblocking(); + if (statsd == null) { + return; + } + + final long token = Binder.clearCallingIdentity(); + try { + statsd.registerPullAtomCallback(callingUid, atomTag, coolDownMillis, timeoutMillis, + additiveFields, pullerCallback); + } catch (RemoteException e) { + Log.e(TAG, "Failed to access statsd to register puller for atom " + atomTag); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void unregisterPullAtomCallback(int atomTag) { + enforceRegisterStatsPullAtomPermission(); + int callingUid = Binder.getCallingUid(); + PullerKey key = new PullerKey(callingUid, atomTag); + + // Always remove the puller from StatsManagerService even if statsd is down. When statsd + // comes back up, we will not re-register the removed puller. + synchronized (mLock) { + mPullers.remove(key); + } + + IStatsd statsd = getStatsdNonblocking(); + if (statsd == null) { + return; + } + + final long token = Binder.clearCallingIdentity(); + try { + statsd.unregisterPullAtomCallback(callingUid, atomTag); + } catch (RemoteException e) { + Log.e(TAG, "Failed to access statsd to unregister puller for atom " + atomTag); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void setDataFetchOperation(long configId, PendingIntent pendingIntent, + String packageName) { + enforceDumpAndUsageStatsPermission(packageName); + int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + PendingIntentRef pir = new PendingIntentRef(pendingIntent, mContext); + ConfigKey key = new ConfigKey(callingUid, configId); + // We add the PIR to a map so we can reregister if statsd is unavailable. + synchronized (mLock) { + mDataFetchPirMap.put(key, pir); + } + try { + IStatsd statsd = getStatsdNonblocking(); + if (statsd != null) { + statsd.setDataFetchOperation(configId, pir, callingUid); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to setDataFetchOperation with statsd"); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void removeDataFetchOperation(long configId, String packageName) { + enforceDumpAndUsageStatsPermission(packageName); + int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + ConfigKey key = new ConfigKey(callingUid, configId); + synchronized (mLock) { + mDataFetchPirMap.remove(key); + } + try { + IStatsd statsd = getStatsdNonblocking(); + if (statsd != null) { + statsd.removeDataFetchOperation(configId, callingUid); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to removeDataFetchOperation with statsd"); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public long[] setActiveConfigsChangedOperation(PendingIntent pendingIntent, + String packageName) { + enforceDumpAndUsageStatsPermission(packageName); + int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + PendingIntentRef pir = new PendingIntentRef(pendingIntent, mContext); + // We add the PIR to a map so we can reregister if statsd is unavailable. + synchronized (mLock) { + mActiveConfigsPirMap.put(callingUid, pir); + } + try { + IStatsd statsd = getStatsdNonblocking(); + if (statsd != null) { + return statsd.setActiveConfigsChangedOperation(pir, callingUid); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to setActiveConfigsChangedOperation with statsd"); + } finally { + Binder.restoreCallingIdentity(token); + } + return new long[] {}; + } + + @Override + public void removeActiveConfigsChangedOperation(String packageName) { + enforceDumpAndUsageStatsPermission(packageName); + int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + synchronized (mLock) { + mActiveConfigsPirMap.remove(callingUid); + } + try { + IStatsd statsd = getStatsdNonblocking(); + if (statsd != null) { + statsd.removeActiveConfigsChangedOperation(callingUid); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to removeActiveConfigsChangedOperation with statsd"); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void setBroadcastSubscriber(long configId, long subscriberId, + PendingIntent pendingIntent, String packageName) { + enforceDumpAndUsageStatsPermission(packageName); + int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + PendingIntentRef pir = new PendingIntentRef(pendingIntent, mContext); + ConfigKey key = new ConfigKey(callingUid, configId); + // We add the PIR to a map so we can reregister if statsd is unavailable. + synchronized (mLock) { + ArrayMap<Long, PendingIntentRef> innerMap = mBroadcastSubscriberPirMap + .getOrDefault(key, new ArrayMap<>()); + innerMap.put(subscriberId, pir); + mBroadcastSubscriberPirMap.put(key, innerMap); + } + try { + IStatsd statsd = getStatsdNonblocking(); + if (statsd != null) { + statsd.setBroadcastSubscriber( + configId, subscriberId, pir, callingUid); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to setBroadcastSubscriber with statsd"); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void unsetBroadcastSubscriber(long configId, long subscriberId, String packageName) { + enforceDumpAndUsageStatsPermission(packageName); + int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + ConfigKey key = new ConfigKey(callingUid, configId); + synchronized (mLock) { + ArrayMap<Long, PendingIntentRef> innerMap = mBroadcastSubscriberPirMap + .getOrDefault(key, new ArrayMap<>()); + innerMap.remove(subscriberId); + if (innerMap.isEmpty()) { + mBroadcastSubscriberPirMap.remove(key); + } + } + try { + IStatsd statsd = getStatsdNonblocking(); + if (statsd != null) { + statsd.unsetBroadcastSubscriber(configId, subscriberId, callingUid); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to unsetBroadcastSubscriber with statsd"); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public long[] getRegisteredExperimentIds() throws IllegalStateException { + enforceDumpAndUsageStatsPermission(null); + final long token = Binder.clearCallingIdentity(); + try { + IStatsd statsd = waitForStatsd(); + if (statsd != null) { + return statsd.getRegisteredExperimentIds(); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to getRegisteredExperimentIds with statsd"); + throw new IllegalStateException(e.getMessage(), e); + } finally { + Binder.restoreCallingIdentity(token); + } + throw new IllegalStateException("Failed to connect to statsd to registerExperimentIds"); + } + + @Override + public byte[] getMetadata(String packageName) throws IllegalStateException { + enforceDumpAndUsageStatsPermission(packageName); + final long token = Binder.clearCallingIdentity(); + try { + IStatsd statsd = waitForStatsd(); + if (statsd != null) { + return statsd.getMetadata(); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to getMetadata with statsd"); + throw new IllegalStateException(e.getMessage(), e); + } finally { + Binder.restoreCallingIdentity(token); + } + throw new IllegalStateException("Failed to connect to statsd to getMetadata"); + } + + @Override + public byte[] getData(long key, String packageName) throws IllegalStateException { + enforceDumpAndUsageStatsPermission(packageName); + int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + IStatsd statsd = waitForStatsd(); + if (statsd != null) { + return statsd.getData(key, callingUid); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to getData with statsd"); + throw new IllegalStateException(e.getMessage(), e); + } finally { + Binder.restoreCallingIdentity(token); + } + throw new IllegalStateException("Failed to connect to statsd to getData"); + } + + @Override + public void addConfiguration(long configId, byte[] config, String packageName) + throws IllegalStateException { + enforceDumpAndUsageStatsPermission(packageName); + int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + IStatsd statsd = waitForStatsd(); + if (statsd != null) { + statsd.addConfiguration(configId, config, callingUid); + return; + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to addConfiguration with statsd"); + throw new IllegalStateException(e.getMessage(), e); + } finally { + Binder.restoreCallingIdentity(token); + } + throw new IllegalStateException("Failed to connect to statsd to addConfig"); + } + + @Override + public void removeConfiguration(long configId, String packageName) + throws IllegalStateException { + enforceDumpAndUsageStatsPermission(packageName); + int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + IStatsd statsd = waitForStatsd(); + if (statsd != null) { + statsd.removeConfiguration(configId, callingUid); + return; + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to removeConfiguration with statsd"); + throw new IllegalStateException(e.getMessage(), e); + } finally { + Binder.restoreCallingIdentity(token); + } + throw new IllegalStateException("Failed to connect to statsd to removeConfig"); + } + + void setStatsCompanionService(StatsCompanionService statsCompanionService) { + mStatsCompanionService = statsCompanionService; + } + + /** + * Checks that the caller has both DUMP and PACKAGE_USAGE_STATS permissions. Also checks that + * the caller has USAGE_STATS_PERMISSION_OPS for the specified packageName if it is not null. + * + * @param packageName The packageName to check USAGE_STATS_PERMISSION_OPS. + */ + private void enforceDumpAndUsageStatsPermission(@Nullable String packageName) { + int callingUid = Binder.getCallingUid(); + int callingPid = Binder.getCallingPid(); + + if (callingPid == Process.myPid()) { + return; + } + + mContext.enforceCallingPermission(Manifest.permission.DUMP, null); + mContext.enforceCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS, null); + + if (packageName == null) { + return; + } + AppOpsManager appOpsManager = (AppOpsManager) mContext + .getSystemService(Context.APP_OPS_SERVICE); + switch (appOpsManager.noteOp(USAGE_STATS_PERMISSION_OPS, + Binder.getCallingUid(), packageName, null, null)) { + case AppOpsManager.MODE_ALLOWED: + case AppOpsManager.MODE_DEFAULT: + break; + default: + throw new SecurityException( + String.format("UID %d / PID %d lacks app-op %s", + callingUid, callingPid, USAGE_STATS_PERMISSION_OPS) + ); + } + } + + private void enforceRegisterStatsPullAtomPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.REGISTER_STATS_PULL_ATOM, + "Need REGISTER_STATS_PULL_ATOM permission."); + } + + + /** + * Clients should call this if blocking until statsd to be ready is desired + * + * @return IStatsd object if statsd becomes ready within the timeout, null otherwise. + */ + private IStatsd waitForStatsd() { + synchronized (mLock) { + if (mStatsd == null) { + try { + mLock.wait(STATSD_TIMEOUT_MILLIS); + } catch (InterruptedException e) { + Log.e(TAG, "wait for statsd interrupted"); + } + } + return mStatsd; + } + } + + /** + * Clients should call this to receive a reference to statsd. + * + * @return IStatsd object if statsd is ready, null otherwise. + */ + private IStatsd getStatsdNonblocking() { + synchronized (mLock) { + return mStatsd; + } + } + + /** + * Called from {@link StatsCompanionService}. + * + * Tells StatsManagerService that Statsd is ready and updates + * Statsd with the contents of our local cache. + */ + void statsdReady(IStatsd statsd) { + synchronized (mLock) { + mStatsd = statsd; + mLock.notify(); + } + sayHiToStatsd(statsd); + } + + /** + * Called from {@link StatsCompanionService}. + * + * Tells StatsManagerService that Statsd is no longer ready + * and we should no longer make binder calls with statsd. + */ + void statsdNotReady() { + synchronized (mLock) { + mStatsd = null; + } + } + + private void sayHiToStatsd(IStatsd statsd) { + if (statsd == null) { + return; + } + + final long token = Binder.clearCallingIdentity(); + try { + registerAllPullers(statsd); + registerAllDataFetchOperations(statsd); + registerAllActiveConfigsChangedOperations(statsd); + registerAllBroadcastSubscribers(statsd); + } catch (RemoteException e) { + Log.e(TAG, "StatsManager failed to (re-)register data with statsd"); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // Pre-condition: the Binder calling identity has already been cleared + private void registerAllPullers(IStatsd statsd) throws RemoteException { + // Since we do not want to make an IPC with the lock held, we first create a copy of the + // data with the lock held before iterating through the map. + ArrayMap<PullerKey, PullerValue> pullersCopy; + synchronized (mLock) { + pullersCopy = new ArrayMap<>(mPullers); + } + + for (Map.Entry<PullerKey, PullerValue> entry : pullersCopy.entrySet()) { + PullerKey key = entry.getKey(); + PullerValue value = entry.getValue(); + statsd.registerPullAtomCallback(key.getUid(), key.getAtom(), value.getCoolDownMillis(), + value.getTimeoutMillis(), value.getAdditiveFields(), value.getCallback()); + } + statsd.allPullersFromBootRegistered(); + } + + // Pre-condition: the Binder calling identity has already been cleared + private void registerAllDataFetchOperations(IStatsd statsd) throws RemoteException { + // Since we do not want to make an IPC with the lock held, we first create a copy of the + // data with the lock held before iterating through the map. + ArrayMap<ConfigKey, PendingIntentRef> dataFetchCopy; + synchronized (mLock) { + dataFetchCopy = new ArrayMap<>(mDataFetchPirMap); + } + + for (Map.Entry<ConfigKey, PendingIntentRef> entry : dataFetchCopy.entrySet()) { + ConfigKey key = entry.getKey(); + statsd.setDataFetchOperation(key.getConfigId(), entry.getValue(), key.getUid()); + } + } + + // Pre-condition: the Binder calling identity has already been cleared + private void registerAllActiveConfigsChangedOperations(IStatsd statsd) throws RemoteException { + // Since we do not want to make an IPC with the lock held, we first create a copy of the + // data with the lock held before iterating through the map. + ArrayMap<Integer, PendingIntentRef> activeConfigsChangedCopy; + synchronized (mLock) { + activeConfigsChangedCopy = new ArrayMap<>(mActiveConfigsPirMap); + } + + for (Map.Entry<Integer, PendingIntentRef> entry : activeConfigsChangedCopy.entrySet()) { + statsd.setActiveConfigsChangedOperation(entry.getValue(), entry.getKey()); + } + } + + // Pre-condition: the Binder calling identity has already been cleared + private void registerAllBroadcastSubscribers(IStatsd statsd) throws RemoteException { + // Since we do not want to make an IPC with the lock held, we first create a deep copy of + // the data with the lock held before iterating through the map. + ArrayMap<ConfigKey, ArrayMap<Long, PendingIntentRef>> broadcastSubscriberCopy = + new ArrayMap<>(); + synchronized (mLock) { + for (Map.Entry<ConfigKey, ArrayMap<Long, PendingIntentRef>> entry : + mBroadcastSubscriberPirMap.entrySet()) { + broadcastSubscriberCopy.put(entry.getKey(), new ArrayMap(entry.getValue())); + } + } + + for (Map.Entry<ConfigKey, ArrayMap<Long, PendingIntentRef>> entry : + mBroadcastSubscriberPirMap.entrySet()) { + ConfigKey configKey = entry.getKey(); + for (Map.Entry<Long, PendingIntentRef> subscriberEntry : entry.getValue().entrySet()) { + statsd.setBroadcastSubscriber(configKey.getConfigId(), subscriberEntry.getKey(), + subscriberEntry.getValue(), configKey.getUid()); + } + } + } +} diff --git a/apex/statsd/statsd.rc b/apex/statsd/statsd.rc new file mode 100644 index 000000000000..605da2af0c19 --- /dev/null +++ b/apex/statsd/statsd.rc @@ -0,0 +1,20 @@ +# Copyright (C) 2017 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +service statsd /apex/com.android.os.statsd/bin/statsd + class main + socket statsdw dgram+passcred 0222 statsd statsd + user statsd + group statsd log + writepid /dev/cpuset/system-background/tasks diff --git a/apex/statsd/testing/Android.bp b/apex/statsd/testing/Android.bp new file mode 100644 index 000000000000..a9cd0ccb53e8 --- /dev/null +++ b/apex/statsd/testing/Android.bp @@ -0,0 +1,25 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +apex_test { + name: "test_com.android.os.statsd", + visibility: [ + "//system/apex/tests", + ], + defaults: ["com.android.os.statsd-defaults"], + manifest: "test_manifest.json", + file_contexts: ":com.android.os.statsd-file_contexts", + // Test APEX, should never be installed + installable: false, +} diff --git a/apex/statsd/testing/test_manifest.json b/apex/statsd/testing/test_manifest.json new file mode 100644 index 000000000000..57343d3e6ae5 --- /dev/null +++ b/apex/statsd/testing/test_manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.os.statsd", + "version": 2147483647 +} diff --git a/apex/statsd/tests/libstatspull/Android.bp b/apex/statsd/tests/libstatspull/Android.bp new file mode 100644 index 000000000000..05b3e049ac39 --- /dev/null +++ b/apex/statsd/tests/libstatspull/Android.bp @@ -0,0 +1,61 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +android_test { + name: "LibStatsPullTests", + static_libs: [ + "androidx.test.rules", + "platformprotoslite", + "statsdprotolite", + "truth-prebuilt", + ], + libs: [ + "android.test.runner.stubs", + "android.test.base.stubs", + ], + jni_libs: [ + "libstatspull_testhelper", + ], + srcs: [ + "src/**/*.java", + "protos/**/*.proto", + ], + test_suites: [ + "device-tests", + "mts", + ], + platform_apis: true, + privileged: true, + certificate: "platform", + compile_multilib: "both", +} + +cc_library_shared { + name: "libstatspull_testhelper", + srcs: ["jni/stats_pull_helper.cpp"], + cflags: [ + "-Wall", + "-Werror", + ], + shared_libs: [ + "libbinder_ndk", + "statsd-aidl-ndk_platform", + ], + static_libs: [ + "libstatspull_private", + "libstatssocket_private", + "libutils", + "libcutils", + ], +} diff --git a/apex/statsd/tests/libstatspull/AndroidManifest.xml b/apex/statsd/tests/libstatspull/AndroidManifest.xml new file mode 100644 index 000000000000..0c669b051c86 --- /dev/null +++ b/apex/statsd/tests/libstatspull/AndroidManifest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.internal.os.statsd.libstats" > + + + <uses-permission android:name="android.permission.DUMP" /> + <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /> + <uses-permission android:name="android.permission.REGISTER_STATS_PULL_ATOM" /> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.internal.os.statsd.libstats" + android:label="Tests for libstatspull"> + </instrumentation> +</manifest> + diff --git a/apex/statsd/tests/libstatspull/jni/stats_pull_helper.cpp b/apex/statsd/tests/libstatspull/jni/stats_pull_helper.cpp new file mode 100644 index 000000000000..166592d35151 --- /dev/null +++ b/apex/statsd/tests/libstatspull/jni/stats_pull_helper.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <jni.h> +#include <log/log.h> +#include <stats_event.h> +#include <stats_pull_atom_callback.h> + +#include <chrono> +#include <thread> + +using std::this_thread::sleep_for; + +namespace { +static int32_t sAtomTag; +static int32_t sPullReturnVal; +static int64_t sLatencyMillis; +static int32_t sAtomsPerPull; +static int32_t sNumPulls = 0; + +static AStatsManager_PullAtomCallbackReturn pullAtomCallback(int32_t atomTag, AStatsEventList* data, + void* /*cookie*/) { + sNumPulls++; + sleep_for(std::chrono::milliseconds(sLatencyMillis)); + for (int i = 0; i < sAtomsPerPull; i++) { + AStatsEvent* event = AStatsEventList_addStatsEvent(data); + AStatsEvent_setAtomId(event, atomTag); + AStatsEvent_writeInt64(event, (int64_t) sNumPulls); + AStatsEvent_build(event); + } + return sPullReturnVal; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_android_internal_os_statsd_libstats_LibStatsPullTests_setStatsPuller( + JNIEnv* /*env*/, jobject /* this */, jint atomTag, jlong timeoutMillis, + jlong coolDownMillis, jint pullRetVal, jlong latencyMillis, int atomsPerPull) { + sAtomTag = atomTag; + sPullReturnVal = pullRetVal; + sLatencyMillis = latencyMillis; + sAtomsPerPull = atomsPerPull; + sNumPulls = 0; + AStatsManager_PullAtomMetadata* metadata = AStatsManager_PullAtomMetadata_obtain(); + AStatsManager_PullAtomMetadata_setCoolDownMillis(metadata, coolDownMillis); + AStatsManager_PullAtomMetadata_setTimeoutMillis(metadata, timeoutMillis); + + AStatsManager_setPullAtomCallback(sAtomTag, metadata, &pullAtomCallback, nullptr); + AStatsManager_PullAtomMetadata_release(metadata); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_android_internal_os_statsd_libstats_LibStatsPullTests_clearStatsPuller(JNIEnv* /*env*/, + jobject /* this */, + jint /*atomTag*/) { + AStatsManager_clearPullAtomCallback(sAtomTag); +} +} // namespace diff --git a/apex/statsd/tests/libstatspull/protos/test_atoms.proto b/apex/statsd/tests/libstatspull/protos/test_atoms.proto new file mode 100644 index 000000000000..56c1b534a7ce --- /dev/null +++ b/apex/statsd/tests/libstatspull/protos/test_atoms.proto @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto2"; + +package com.android.internal.os.statsd.protos; + +option java_package = "com.android.internal.os.statsd.protos"; +option java_outer_classname = "TestAtoms"; + +message PullCallbackAtomWrapper { + optional PullCallbackAtom pull_callback_atom = 150030; +} + +message PullCallbackAtom { + optional int64 long_val = 1; +} + + + diff --git a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java new file mode 100644 index 000000000000..6108a324e15e --- /dev/null +++ b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/LibStatsPullTests.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.os.statsd.libstats; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.StatsManager; +import android.content.Context; +import android.util.Log; +import android.util.StatsLog; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.os.StatsdConfigProto.AtomMatcher; +import com.android.internal.os.StatsdConfigProto.FieldFilter; +import com.android.internal.os.StatsdConfigProto.GaugeMetric; +import com.android.internal.os.StatsdConfigProto.PullAtomPackages; +import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher; +import com.android.internal.os.StatsdConfigProto.StatsdConfig; +import com.android.internal.os.StatsdConfigProto.TimeUnit; +import com.android.internal.os.statsd.protos.TestAtoms; +import com.android.os.AtomsProto.Atom; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +/** + * Test puller registration. + */ +@MediumTest +@RunWith(AndroidJUnit4.class) +public class LibStatsPullTests { + private static final String LOG_TAG = LibStatsPullTests.class.getSimpleName(); + private static final int SHORT_SLEEP_MILLIS = 250; + private static final int LONG_SLEEP_MILLIS = 1_000; + private Context mContext; + private static final int PULL_ATOM_TAG = 150030; + private static final int APP_BREADCRUMB_LABEL = 3; + private static int sPullReturnValue; + private static long sConfigId; + private static long sPullLatencyMillis; + private static long sPullTimeoutMillis; + private static long sCoolDownMillis; + private static int sAtomsPerPull; + + static { + System.loadLibrary("statspull_testhelper"); + } + + /** + * Setup the tests. Initialize shared data. + */ + @Before + public void setup() { + mContext = InstrumentationRegistry.getTargetContext(); + assertThat(InstrumentationRegistry.getInstrumentation()).isNotNull(); + sPullReturnValue = StatsManager.PULL_SUCCESS; + sPullLatencyMillis = 0; + sPullTimeoutMillis = 10_000L; + sCoolDownMillis = 1_000L; + sAtomsPerPull = 1; + } + + /** + * Teardown the tests. + */ + @After + public void tearDown() throws Exception { + clearStatsPuller(PULL_ATOM_TAG); + StatsManager statsManager = (StatsManager) mContext.getSystemService( + Context.STATS_MANAGER); + statsManager.removeConfig(sConfigId); + } + + /** + * Tests adding a puller callback and that pulls complete successfully. + */ + @Test + public void testPullAtomCallbackRegistration() throws Exception { + StatsManager statsManager = (StatsManager) mContext.getSystemService( + Context.STATS_MANAGER); + // Upload a config that captures that pulled atom. + createAndAddConfigToStatsd(statsManager); + + // Add the puller. + setStatsPuller(PULL_ATOM_TAG, sPullTimeoutMillis, sCoolDownMillis, sPullReturnValue, + sPullLatencyMillis, sAtomsPerPull); + Thread.sleep(SHORT_SLEEP_MILLIS); + StatsLog.logStart(APP_BREADCRUMB_LABEL); + // Let the current bucket finish. + Thread.sleep(LONG_SLEEP_MILLIS); + List<Atom> data = StatsConfigUtils.getGaugeMetricDataList(statsManager, sConfigId); + clearStatsPuller(PULL_ATOM_TAG); + assertThat(data.size()).isEqualTo(1); + TestAtoms.PullCallbackAtomWrapper atomWrapper = null; + try { + atomWrapper = TestAtoms.PullCallbackAtomWrapper.parser() + .parseFrom(data.get(0).toByteArray()); + } catch (Exception e) { + Log.e(LOG_TAG, "Failed to parse primitive atoms"); + } + assertThat(atomWrapper).isNotNull(); + assertThat(atomWrapper.hasPullCallbackAtom()).isTrue(); + TestAtoms.PullCallbackAtom atom = + atomWrapper.getPullCallbackAtom(); + assertThat(atom.getLongVal()).isEqualTo(1); + } + + /** + * Tests that a failed pull is skipped. + */ + @Test + public void testPullAtomCallbackFailure() throws Exception { + StatsManager statsManager = (StatsManager) mContext.getSystemService( + Context.STATS_MANAGER); + createAndAddConfigToStatsd(statsManager); + sPullReturnValue = StatsManager.PULL_SKIP; + // Add the puller. + setStatsPuller(PULL_ATOM_TAG, sPullTimeoutMillis, sCoolDownMillis, sPullReturnValue, + sPullLatencyMillis, sAtomsPerPull); + Thread.sleep(SHORT_SLEEP_MILLIS); + StatsLog.logStart(APP_BREADCRUMB_LABEL); + // Let the current bucket finish. + Thread.sleep(LONG_SLEEP_MILLIS); + List<Atom> data = StatsConfigUtils.getGaugeMetricDataList(statsManager, sConfigId); + clearStatsPuller(PULL_ATOM_TAG); + assertThat(data.size()).isEqualTo(0); + } + + /** + * Tests that a pull that times out is skipped. + */ + @Test + public void testPullAtomCallbackTimeout() throws Exception { + StatsManager statsManager = (StatsManager) mContext.getSystemService( + Context.STATS_MANAGER); + createAndAddConfigToStatsd(statsManager); + // The puller will sleep for 1.5 sec. + sPullLatencyMillis = 1_500; + // 1 second timeout + sPullTimeoutMillis = 1_000; + + // Add the puller. + setStatsPuller(PULL_ATOM_TAG, sPullTimeoutMillis, sCoolDownMillis, sPullReturnValue, + sPullLatencyMillis, sAtomsPerPull); + Thread.sleep(SHORT_SLEEP_MILLIS); + StatsLog.logStart(APP_BREADCRUMB_LABEL); + // Let the current bucket finish and the pull timeout. + Thread.sleep(sPullLatencyMillis * 2); + List<Atom> data = StatsConfigUtils.getGaugeMetricDataList(statsManager, sConfigId); + clearStatsPuller(PULL_ATOM_TAG); + assertThat(data.size()).isEqualTo(0); + } + + /** + * Tests that 2 pulls in quick succession use the cache instead of pulling again. + */ + @Test + public void testPullAtomCallbackCache() throws Exception { + StatsManager statsManager = (StatsManager) mContext.getSystemService( + Context.STATS_MANAGER); + createAndAddConfigToStatsd(statsManager); + + // Set the cooldown to 10 seconds + sCoolDownMillis = 10_000L; + // Add the puller. + setStatsPuller(PULL_ATOM_TAG, sPullTimeoutMillis, sCoolDownMillis, sPullReturnValue, + sPullLatencyMillis, sAtomsPerPull); + + Thread.sleep(SHORT_SLEEP_MILLIS); + StatsLog.logStart(APP_BREADCRUMB_LABEL); + // Pull from cache. + StatsLog.logStart(APP_BREADCRUMB_LABEL); + Thread.sleep(LONG_SLEEP_MILLIS); + List<Atom> data = StatsConfigUtils.getGaugeMetricDataList(statsManager, sConfigId); + clearStatsPuller(PULL_ATOM_TAG); + assertThat(data.size()).isEqualTo(2); + for (int i = 0; i < data.size(); i++) { + TestAtoms.PullCallbackAtomWrapper atomWrapper = null; + try { + atomWrapper = TestAtoms.PullCallbackAtomWrapper.parser() + .parseFrom(data.get(i).toByteArray()); + } catch (Exception e) { + Log.e(LOG_TAG, "Failed to parse primitive atoms"); + } + assertThat(atomWrapper).isNotNull(); + assertThat(atomWrapper.hasPullCallbackAtom()).isTrue(); + TestAtoms.PullCallbackAtom atom = + atomWrapper.getPullCallbackAtom(); + assertThat(atom.getLongVal()).isEqualTo(1); + } + } + + /** + * Tests that a pull that returns 1000 stats events works properly. + */ + @Test + public void testPullAtomCallbackStress() throws Exception { + StatsManager statsManager = (StatsManager) mContext.getSystemService( + Context.STATS_MANAGER); + // Upload a config that captures that pulled atom. + createAndAddConfigToStatsd(statsManager); + sAtomsPerPull = 1000; + // Add the puller. + setStatsPuller(PULL_ATOM_TAG, sPullTimeoutMillis, sCoolDownMillis, sPullReturnValue, + sPullLatencyMillis, sAtomsPerPull); + + Thread.sleep(SHORT_SLEEP_MILLIS); + StatsLog.logStart(APP_BREADCRUMB_LABEL); + // Let the current bucket finish. + Thread.sleep(LONG_SLEEP_MILLIS); + List<Atom> data = StatsConfigUtils.getGaugeMetricDataList(statsManager, sConfigId); + clearStatsPuller(PULL_ATOM_TAG); + assertThat(data.size()).isEqualTo(sAtomsPerPull); + + for (int i = 0; i < data.size(); i++) { + TestAtoms.PullCallbackAtomWrapper atomWrapper = null; + try { + atomWrapper = TestAtoms.PullCallbackAtomWrapper.parser() + .parseFrom(data.get(i).toByteArray()); + } catch (Exception e) { + Log.e(LOG_TAG, "Failed to parse primitive atoms"); + } + assertThat(atomWrapper).isNotNull(); + assertThat(atomWrapper.hasPullCallbackAtom()).isTrue(); + TestAtoms.PullCallbackAtom atom = + atomWrapper.getPullCallbackAtom(); + assertThat(atom.getLongVal()).isEqualTo(1); + } + } + + private void createAndAddConfigToStatsd(StatsManager statsManager) throws Exception { + sConfigId = System.currentTimeMillis(); + long triggerMatcherId = sConfigId + 10; + long pullerMatcherId = sConfigId + 11; + long metricId = sConfigId + 100; + StatsdConfig config = StatsConfigUtils.getSimpleTestConfig(sConfigId) + .addAtomMatcher( + StatsConfigUtils.getAppBreadcrumbMatcher(triggerMatcherId, + APP_BREADCRUMB_LABEL)) + .addAtomMatcher(AtomMatcher.newBuilder() + .setId(pullerMatcherId) + .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder() + .setAtomId(PULL_ATOM_TAG)) + ) + .addGaugeMetric(GaugeMetric.newBuilder() + .setId(metricId) + .setWhat(pullerMatcherId) + .setTriggerEvent(triggerMatcherId) + .setGaugeFieldsFilter(FieldFilter.newBuilder().setIncludeAll(true)) + .setBucket(TimeUnit.CTS) + .setSamplingType(GaugeMetric.SamplingType.FIRST_N_SAMPLES) + .setMaxNumGaugeAtomsPerBucket(1000) + ) + .addPullAtomPackages(PullAtomPackages.newBuilder() + .setAtomId(PULL_ATOM_TAG) + .addPackages(LibStatsPullTests.class.getPackage().getName())) + .build(); + statsManager.addConfig(sConfigId, config.toByteArray()); + assertThat(StatsConfigUtils.verifyValidConfigExists(statsManager, sConfigId)).isTrue(); + } + + private native void setStatsPuller(int atomTag, long timeoutMillis, long coolDownMillis, + int pullReturnVal, long latencyMillis, int atomPerPull); + + private native void clearStatsPuller(int atomTag); +} + diff --git a/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java new file mode 100644 index 000000000000..b5afb94886de --- /dev/null +++ b/apex/statsd/tests/libstatspull/src/com/android/internal/os/statsd/libstats/StatsConfigUtils.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.os.statsd.libstats; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.StatsManager; +import android.util.Log; + +import com.android.internal.os.StatsdConfigProto.AtomMatcher; +import com.android.internal.os.StatsdConfigProto.FieldValueMatcher; +import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher; +import com.android.internal.os.StatsdConfigProto.StatsdConfig; +import com.android.os.AtomsProto.AppBreadcrumbReported; +import com.android.os.AtomsProto.Atom; +import com.android.os.StatsLog.ConfigMetricsReport; +import com.android.os.StatsLog.ConfigMetricsReportList; +import com.android.os.StatsLog.GaugeBucketInfo; +import com.android.os.StatsLog.GaugeMetricData; +import com.android.os.StatsLog.StatsLogReport; +import com.android.os.StatsLog.StatsdStatsReport; +import com.android.os.StatsLog.StatsdStatsReport.ConfigStats; + +import java.util.ArrayList; +import java.util.List; + +/** + * Util class for constructing statsd configs. + */ +public class StatsConfigUtils { + public static final String TAG = "statsd.StatsConfigUtils"; + public static final int SHORT_WAIT = 2_000; // 2 seconds. + + /** + * @return An empty StatsdConfig in serialized proto format. + */ + public static StatsdConfig.Builder getSimpleTestConfig(long configId) { + return StatsdConfig.newBuilder().setId(configId) + .addAllowedLogSource(StatsConfigUtils.class.getPackage().getName()); + } + + + public static boolean verifyValidConfigExists(StatsManager statsManager, long configId) { + StatsdStatsReport report = null; + try { + report = StatsdStatsReport.parser().parseFrom(statsManager.getStatsMetadata()); + } catch (Exception e) { + Log.e(TAG, "getMetadata failed", e); + } + assertThat(report).isNotNull(); + boolean foundConfig = false; + for (ConfigStats configStats : report.getConfigStatsList()) { + if (configStats.getId() == configId && configStats.getIsValid() + && configStats.getDeletionTimeSec() == 0) { + foundConfig = true; + } + } + return foundConfig; + } + + public static AtomMatcher getAppBreadcrumbMatcher(long id, int label) { + return AtomMatcher.newBuilder() + .setId(id) + .setSimpleAtomMatcher( + SimpleAtomMatcher.newBuilder() + .setAtomId(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER) + .addFieldValueMatcher(FieldValueMatcher.newBuilder() + .setField(AppBreadcrumbReported.LABEL_FIELD_NUMBER) + .setEqInt(label) + ) + ) + .build(); + } + + public static ConfigMetricsReport getConfigMetricsReport(StatsManager statsManager, + long configId) { + ConfigMetricsReportList reportList = null; + try { + reportList = ConfigMetricsReportList.parser() + .parseFrom(statsManager.getReports(configId)); + } catch (Exception e) { + Log.e(TAG, "getData failed", e); + } + assertThat(reportList).isNotNull(); + assertThat(reportList.getReportsCount()).isEqualTo(1); + ConfigMetricsReport report = reportList.getReports(0); + assertThat(report.getDumpReportReason()) + .isEqualTo(ConfigMetricsReport.DumpReportReason.GET_DATA_CALLED); + return report; + + } + public static List<Atom> getGaugeMetricDataList(ConfigMetricsReport report) { + List<Atom> data = new ArrayList<>(); + for (StatsLogReport metric : report.getMetricsList()) { + for (GaugeMetricData gaugeMetricData : metric.getGaugeMetrics().getDataList()) { + for (GaugeBucketInfo bucketInfo : gaugeMetricData.getBucketInfoList()) { + for (Atom atom : bucketInfo.getAtomList()) { + data.add(atom); + } + } + } + } + return data; + } + + public static List<Atom> getGaugeMetricDataList(StatsManager statsManager, long configId) { + ConfigMetricsReport report = getConfigMetricsReport(statsManager, configId); + return getGaugeMetricDataList(report); + } +} + |