diff options
15 files changed, 1115 insertions, 0 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index bc34f5bfe13f..49d08aa53d76 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -91,6 +91,7 @@ package android { field public static final String BIND_TRANSLATION_SERVICE = "android.permission.BIND_TRANSLATION_SERVICE"; field public static final String BIND_TRUST_AGENT = "android.permission.BIND_TRUST_AGENT"; field public static final String BIND_TV_REMOTE_SERVICE = "android.permission.BIND_TV_REMOTE_SERVICE"; + field @FlaggedApi("android.content.pm.verification_service") public static final String BIND_VERIFICATION_AGENT = "android.permission.BIND_VERIFICATION_AGENT"; field public static final String BIND_VISUAL_QUERY_DETECTION_SERVICE = "android.permission.BIND_VISUAL_QUERY_DETECTION_SERVICE"; field public static final String BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE = "android.permission.BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE"; field public static final String BIND_WEARABLE_SENSING_SERVICE = "android.permission.BIND_WEARABLE_SENSING_SERVICE"; @@ -412,6 +413,7 @@ package android { field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String USE_ON_DEVICE_INTELLIGENCE = "android.permission.USE_ON_DEVICE_INTELLIGENCE"; field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK"; field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED"; + field @FlaggedApi("android.content.pm.verification_service") public static final String VERIFICATION_AGENT = "android.permission.VERIFICATION_AGENT"; field @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final String VIBRATE_VENDOR_EFFECTS = "android.permission.VIBRATE_VENDOR_EFFECTS"; field public static final String WHITELIST_AUTO_REVOKE_PERMISSIONS = "android.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS"; field public static final String WHITELIST_RESTRICTED_PERMISSIONS = "android.permission.WHITELIST_RESTRICTED_PERMISSIONS"; @@ -4302,6 +4304,7 @@ package android.content.pm { method @Deprecated @RequiresPermission(android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT) public abstract void verifyIntentFilter(int, int, @NonNull java.util.List<java.lang.String>); field public static final String ACTION_REQUEST_PERMISSIONS = "android.content.pm.action.REQUEST_PERMISSIONS"; field public static final String ACTION_REQUEST_PERMISSIONS_FOR_OTHER = "android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER"; + field @FlaggedApi("android.content.pm.verification_service") public static final String ACTION_VERIFY_PACKAGE = "android.content.pm.action.VERIFY_PACKAGE"; field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_APK = 1; // 0x1 field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_INSTALLER = 2; // 0x2 field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_SYSTEM_IMAGE = 3; // 0x3 @@ -4615,6 +4618,61 @@ package android.content.pm.verify.domain { } +package android.content.pm.verify.pkg { + + @FlaggedApi("android.content.pm.verification_service") public final class VerificationSession implements android.os.Parcelable { + method public int describeContents(); + method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long extendTimeRemaining(long); + method @NonNull public java.util.List<android.content.pm.SharedLibraryInfo> getDeclaredLibraries(); + method @NonNull public android.os.PersistableBundle getExtensionParams(); + method public int getId(); + method public int getInstallSessionId(); + method @NonNull public String getPackageName(); + method @NonNull public android.content.pm.SigningInfo getSigningInfo(); + method @NonNull public android.net.Uri getStagedPackageUri(); + method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long getTimeoutTime(); + method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus); + method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus, @NonNull android.os.PersistableBundle); + method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationIncomplete(int); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationSession> CREATOR; + field public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2; // 0x2 + field public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1; // 0x1 + field public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0; // 0x0 + } + + @FlaggedApi("android.content.pm.verification_service") public final class VerificationStatus implements android.os.Parcelable { + method public int describeContents(); + method public int getAslStatus(); + method @NonNull public String getFailureMessage(); + method public boolean isVerified(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationStatus> CREATOR; + field public static final int VERIFIER_STATUS_ASL_BAD = 2; // 0x2 + field public static final int VERIFIER_STATUS_ASL_GOOD = 1; // 0x1 + field public static final int VERIFIER_STATUS_ASL_UNDEFINED = 0; // 0x0 + } + + public static final class VerificationStatus.Builder { + ctor public VerificationStatus.Builder(); + method @NonNull public android.content.pm.verify.pkg.VerificationStatus build(); + method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setAslStatus(int); + method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setFailureMessage(@NonNull String); + method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setVerified(boolean); + } + + @FlaggedApi("android.content.pm.verification_service") public abstract class VerifierService extends android.app.Service { + ctor public VerifierService(); + method @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent); + method public abstract void onPackageNameAvailable(@NonNull String); + method public abstract void onVerificationCancelled(@NonNull String); + method public abstract void onVerificationRequired(@NonNull android.content.pm.verify.pkg.VerificationSession); + method public abstract void onVerificationRetry(@NonNull android.content.pm.verify.pkg.VerificationSession); + method public abstract void onVerificationTimeout(int); + } + +} + package android.content.rollback { public final class PackageRollbackInfo implements android.os.Parcelable { diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index fb2655c771c4..e985f88f38fc 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -5039,6 +5039,25 @@ public abstract class PackageManager { "android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER"; /** + * Used by the system to query a {@link android.content.pm.verify.pkg.VerifierService} provider, + * which registers itself via an intent-filter handling this action. + * + * <p class="note">Only the system can bind to such a verifier service. This is protected by the + * {@link android.Manifest.permission#BIND_VERIFICATION_AGENT} permission. The verifier service + * app should protect the service by adding this permission in the service declaration in its + * manifest. + * <p> + * A verifier service must be a privileged app and hold the + * {@link android.Manifest.permission#VERIFICATION_AGENT} permission. + * + * @hide + */ + @SystemApi + @FlaggedApi(android.content.pm.Flags.FLAG_VERIFICATION_SERVICE) + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String ACTION_VERIFY_PACKAGE = "android.content.pm.action.VERIFY_PACKAGE"; + + /** * The names of the requested permissions. * <p> * <strong>Type:</strong> String[] diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 160cbdffe5bb..300740e84c60 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -309,3 +309,11 @@ flag { description: "Feature flag to enable the holder of SYSTEM_APP_PROTECTION_SERVICE role to silently delete packages. To be deprecated by delete_packages_silently." bug: "361776825" } + +flag { + name: "verification_service" + namespace: "package_manager_service" + description: "Feature flag to enable the new verification service." + bug: "360129103" + is_fixed_read_only: true +} diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl new file mode 100644 index 000000000000..38a7956603ae --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.pm.verify.pkg; + +import android.content.pm.verify.pkg.VerificationStatus; +import android.os.PersistableBundle; + +/** + * Oneway interface that allows the verifier to send response or verification results back to + * the system. + * @hide + */ +oneway interface IVerificationSessionCallback { + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + void reportVerificationIncomplete(int verificationId, int reason); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + void reportVerificationComplete(int verificationId, in VerificationStatus status); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + void reportVerificationCompleteWithExtensionResponse(int verificationId, in VerificationStatus status, in PersistableBundle response); +} diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl new file mode 100644 index 000000000000..7a9484abd1b1 --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.pm.verify.pkg; + +/** + * Non-oneway interface that allows the verifier to retrieve information from the system. + * @hide + */ +interface IVerificationSessionInterface { + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + long getTimeoutTime(int verificationId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + long extendTimeRemaining(int verificationId, long additionalMs); +}
\ No newline at end of file diff --git a/core/java/android/content/pm/verify/pkg/IVerifierService.aidl b/core/java/android/content/pm/verify/pkg/IVerifierService.aidl new file mode 100644 index 000000000000..d3071fd67b9d --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/IVerifierService.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.pm.verify.pkg; + +import android.content.pm.verify.pkg.VerificationSession; + +/** + * Oneway interface that allows the system to communicate to the verifier service agent. + * @hide + */ +oneway interface IVerifierService { + void onPackageNameAvailable(in String packageName); + void onVerificationCancelled(in String packageName); + void onVerificationRequired(in VerificationSession session); + void onVerificationRetry(in VerificationSession session); + void onVerificationTimeout(int verificationId); +}
\ No newline at end of file diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.aidl b/core/java/android/content/pm/verify/pkg/VerificationSession.aidl new file mode 100644 index 000000000000..ac855850a86c --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/VerificationSession.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.pm.verify.pkg; + +/** @hide */ +parcelable VerificationSession; diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.java b/core/java/android/content/pm/verify/pkg/VerificationSession.java new file mode 100644 index 000000000000..70b4a022f521 --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/VerificationSession.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.pm.verify.pkg; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.content.pm.Flags; +import android.content.pm.SharedLibraryInfo; +import android.content.pm.SigningInfo; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.os.RemoteException; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.List; + +/** + * This class is used by the system to describe the details about a verification request sent to the + * verification agent, aka the verifier. It includes the interfaces for the verifier to communicate + * back to the system. + * @hide + */ +@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) +@SystemApi +public final class VerificationSession implements Parcelable { + /** + * The verification cannot be completed because of unknown reasons. + */ + public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0; + /** + * The verification cannot be completed because the network is unavailable. + */ + public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1; + /** + * The verification cannot be completed because the network is limited. + */ + public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2; + + /** + * @hide + */ + @IntDef(prefix = {"VERIFICATION_INCOMPLETE_"}, value = { + VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE, + VERIFICATION_INCOMPLETE_NETWORK_LIMITED, + VERIFICATION_INCOMPLETE_UNKNOWN, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface VerificationIncompleteReason { + } + + private final int mId; + private final int mInstallSessionId; + @NonNull + private final String mPackageName; + @NonNull + private final Uri mStagedPackageUri; + @NonNull + private final SigningInfo mSigningInfo; + @NonNull + private final List<SharedLibraryInfo> mDeclaredLibraries; + @NonNull + private final PersistableBundle mExtensionParams; + @NonNull + private final IVerificationSessionInterface mSession; + @NonNull + private final IVerificationSessionCallback mCallback; + + /** + * Constructor used by the system to describe the details of a verification session. + * @hide + */ + public VerificationSession(int id, int installSessionId, @NonNull String packageName, + @NonNull Uri stagedPackageUri, @NonNull SigningInfo signingInfo, + @NonNull List<SharedLibraryInfo> declaredLibraries, + @NonNull PersistableBundle extensionParams, + @NonNull IVerificationSessionInterface session, + @NonNull IVerificationSessionCallback callback) { + mId = id; + mInstallSessionId = installSessionId; + mPackageName = packageName; + mStagedPackageUri = stagedPackageUri; + mSigningInfo = signingInfo; + mDeclaredLibraries = declaredLibraries; + mExtensionParams = extensionParams; + mSession = session; + mCallback = callback; + } + + /** + * A unique identifier tied to this specific verification session. + */ + public int getId() { + return mId; + } + + /** + * The package name of the app that is to be verified. + */ + public @NonNull String getPackageName() { + return mPackageName; + } + + /** + * The id of the installation session associated with the verification. + */ + public int getInstallSessionId() { + return mInstallSessionId; + } + + /** + * The Uri of the path where the package's code files are located. + */ + public @NonNull Uri getStagedPackageUri() { + return mStagedPackageUri; + } + + /** + * Signing info of the package to be verified. + */ + public @NonNull SigningInfo getSigningInfo() { + return mSigningInfo; + } + + /** + * Returns a mapping of any shared libraries declared in the manifest + * to the {@link SharedLibraryInfo#Type} that is declared. This will be an empty + * map if no shared libraries are declared by the package. + */ + @NonNull + public List<SharedLibraryInfo> getDeclaredLibraries() { + return Collections.unmodifiableList(mDeclaredLibraries); + } + + /** + * Returns any extension params associated with the verification request. + */ + @NonNull + public PersistableBundle getExtensionParams() { + return mExtensionParams; + } + + /** + * Get the value of Clock.elapsedRealtime() at which time this verification + * will timeout as incomplete if no other verification response is provided. + */ + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public long getTimeoutTime() { + try { + return mSession.getTimeoutTime(mId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Extend the timeout for this session by the provided additionalMs to + * fetch relevant information over the network or wait for the network. + * This may be called multiple times. If the request would bypass any max + * duration by the system, the method will return a lower value than the + * requested amount that indicates how much the time was extended. + */ + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public long extendTimeRemaining(long additionalMs) { + try { + return mSession.extendTimeRemaining(mId, additionalMs); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Report to the system that verification could not be completed along + * with an approximate reason to pass on to the installer. + */ + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public void reportVerificationIncomplete(@VerificationIncompleteReason int reason) { + try { + mCallback.reportVerificationIncomplete(mId, reason); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Report to the system that the verification has completed and the + * install process may act on that status to either block in the case + * of failure or continue to process the install in the case of success. + */ + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public void reportVerificationComplete(@NonNull VerificationStatus status) { + try { + mCallback.reportVerificationComplete(mId, status); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Same as {@link #reportVerificationComplete(VerificationStatus)}, but also provide + * a result to the extension params provided in the request, which will be passed to the + * installer in the installation result. + */ + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public void reportVerificationComplete(@NonNull VerificationStatus status, + @NonNull PersistableBundle response) { + try { + mCallback.reportVerificationCompleteWithExtensionResponse(mId, status, response); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private VerificationSession(@NonNull Parcel in) { + mId = in.readInt(); + mInstallSessionId = in.readInt(); + mPackageName = in.readString8(); + mStagedPackageUri = Uri.CREATOR.createFromParcel(in); + mSigningInfo = SigningInfo.CREATOR.createFromParcel(in); + mDeclaredLibraries = in.createTypedArrayList(SharedLibraryInfo.CREATOR); + mExtensionParams = in.readPersistableBundle(getClass().getClassLoader()); + mSession = IVerificationSessionInterface.Stub.asInterface(in.readStrongBinder()); + mCallback = IVerificationSessionCallback.Stub.asInterface(in.readStrongBinder()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mId); + dest.writeInt(mInstallSessionId); + dest.writeString8(mPackageName); + Uri.writeToParcel(dest, mStagedPackageUri); + mSigningInfo.writeToParcel(dest, flags); + dest.writeTypedList(mDeclaredLibraries); + dest.writePersistableBundle(mExtensionParams); + dest.writeStrongBinder(mSession.asBinder()); + dest.writeStrongBinder(mCallback.asBinder()); + } + + @NonNull + public static final Creator<VerificationSession> CREATOR = new Creator<>() { + @Override + public VerificationSession createFromParcel(@NonNull Parcel in) { + return new VerificationSession(in); + } + + @Override + public VerificationSession[] newArray(int size) { + return new VerificationSession[size]; + } + }; +} diff --git a/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl b/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl new file mode 100644 index 000000000000..6a1cb4f2d1d2 --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.pm.verify.pkg; + +/** @hide */ +parcelable VerificationStatus; diff --git a/core/java/android/content/pm/verify/pkg/VerificationStatus.java b/core/java/android/content/pm/verify/pkg/VerificationStatus.java new file mode 100644 index 000000000000..4d0379d79773 --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/VerificationStatus.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.pm.verify.pkg; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.pm.Flags; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This class is used by the verifier to describe the status of the verification request, whether + * it's successful or it has failed along with any relevant details. + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) +public final class VerificationStatus implements Parcelable { + /** + * The ASL status has not been determined. This happens in situations where the verification + * service is not monitoring ASLs, and means the ASL data in the app is not necessarily bad but + * can't be trusted. + */ + public static final int VERIFIER_STATUS_ASL_UNDEFINED = 0; + + /** + * The app's ASL data is considered to be in a good state. + */ + public static final int VERIFIER_STATUS_ASL_GOOD = 1; + + /** + * There is something bad in the app's ASL data; the user should be warned about this when shown + * the ASL data and/or appropriate decisions made about the use of this data by the platform. + */ + public static final int VERIFIER_STATUS_ASL_BAD = 2; + + /** @hide */ + @IntDef(prefix = {"VERIFIER_STATUS_ASL_"}, value = { + VERIFIER_STATUS_ASL_UNDEFINED, + VERIFIER_STATUS_ASL_GOOD, + VERIFIER_STATUS_ASL_BAD, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface VerifierStatusAsl {} + + private boolean mIsVerified; + private @VerifierStatusAsl int mAslStatus; + @NonNull + private String mFailuresMessage = ""; + + private VerificationStatus() {} + + /** + * @return whether the status is set to verified or not. + */ + public boolean isVerified() { + return mIsVerified; + } + + /** + * @return the failure message associated with the failure status. + */ + @NonNull + public String getFailureMessage() { + return mFailuresMessage; + } + + /** + * @return the asl status. + */ + public @VerifierStatusAsl int getAslStatus() { + return mAslStatus; + } + + /** + * Builder to construct a {@link VerificationStatus} object. + */ + public static final class Builder { + final VerificationStatus mStatus = new VerificationStatus(); + + /** + * Set in the status whether the verification has succeeded or failed. + */ + @NonNull + public Builder setVerified(boolean verified) { + mStatus.mIsVerified = verified; + return this; + } + + /** + * Set a developer-facing failure message to include in the verification failure status. + */ + @NonNull + public Builder setFailureMessage(@NonNull String failureMessage) { + mStatus.mFailuresMessage = failureMessage; + return this; + } + + /** + * Set the ASL status, as defined in {@link VerifierStatusAsl}. + */ + @NonNull + public Builder setAslStatus(@VerifierStatusAsl int aslStatus) { + mStatus.mAslStatus = aslStatus; + return this; + } + + /** + * Build the status object. + */ + @NonNull + public VerificationStatus build() { + return mStatus; + } + } + + private VerificationStatus(Parcel in) { + mIsVerified = in.readBoolean(); + mAslStatus = in.readInt(); + mFailuresMessage = in.readString8(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeBoolean(mIsVerified); + dest.writeInt(mAslStatus); + dest.writeString8(mFailuresMessage); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Creator<VerificationStatus> CREATOR = new Creator<>() { + @Override + public VerificationStatus createFromParcel(@NonNull Parcel in) { + return new VerificationStatus(in); + } + + @Override + public VerificationStatus[] newArray(int size) { + return new VerificationStatus[size]; + } + }; +} diff --git a/core/java/android/content/pm/verify/pkg/VerifierService.java b/core/java/android/content/pm/verify/pkg/VerifierService.java new file mode 100644 index 000000000000..ccf211915326 --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/VerifierService.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.pm.verify.pkg; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.Service; +import android.content.Intent; +import android.content.pm.Flags; +import android.content.pm.PackageManager; +import android.os.IBinder; + +/** + * A base service implementation for the verifier agent to implement. + * + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) +public abstract class VerifierService extends Service { + /** + * Called when a package name is available for a pending verification, + * giving the verifier opportunity to pre-fetch any relevant information + * that may be needed should a verification for the package be required. + */ + public abstract void onPackageNameAvailable(@NonNull String packageName); + + /** + * Called when a package recently provided via {@link #onPackageNameAvailable} + * is no longer expected to be installed. This is a hint that any pre-fetch or + * cache created as a result of the previous call may be be cleared. + * <p>This method will never be called after {@link #onVerificationRequired} is called for the + * same package. Once a verification is officially requested by + * {@link #onVerificationRequired}, it cannot be cancelled. + * </p> + */ + public abstract void onVerificationCancelled(@NonNull String packageName); + + /** + * Called when an application needs to be verified. Details about the + * verification and actions that can be taken on it will be encapsulated in + * the provided {@link VerificationSession} parameter. + */ + public abstract void onVerificationRequired(@NonNull VerificationSession session); + + /** + * Called when a verification needs to be retried. This can be encountered + * when a prior verification was marked incomplete and the user has indicated + * that they've resolved the issue, or when a timeout is reached, but the + * the system is attempting to retry. Details about the + * verification and actions that can be taken on it will be encapsulated in + * the provided {@link VerificationSession} parameter. + */ + public abstract void onVerificationRetry(@NonNull VerificationSession session); + + /** + * Called in the case that an active verification has failed. Any APIs called + * on the {@link VerificationSession} instance associated with this {@code verificationId} will + * throw an {@link IllegalStateException}. + */ + public abstract void onVerificationTimeout(int verificationId); + + /** + * Called when the verifier service is bound to the system. + */ + public @Nullable IBinder onBind(@Nullable Intent intent) { + if (intent == null || !PackageManager.ACTION_VERIFY_PACKAGE.equals(intent.getAction())) { + return null; + } + return new IVerifierService.Stub() { + @Override + public void onPackageNameAvailable(@NonNull String packageName) { + VerifierService.this.onPackageNameAvailable(packageName); + } + + @Override + public void onVerificationCancelled(@NonNull String packageName) { + VerifierService.this.onVerificationCancelled(packageName); + } + + @Override + public void onVerificationRequired(@NonNull VerificationSession session) { + VerifierService.this.onVerificationRequired(session); + } + + @Override + public void onVerificationRetry(@NonNull VerificationSession session) { + VerifierService.this.onVerificationRetry(session); + } + + @Override + public void onVerificationTimeout(int verificationId) { + VerifierService.this.onVerificationTimeout(verificationId); + } + }; + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index d35c66ed719e..ed33edecb333 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8449,6 +8449,29 @@ <permission android:name="android.permission.RESERVED_FOR_TESTING_SIGNATURE" android:protectionLevel="signature"/> + <!-- @SystemApi + @FlaggedApi("android.content.pm.verification_service") + Allows app to be the verification agent to verify packages. + <p>Protection level: signature|privileged + @hide + --> + <permission android:name="android.permission.VERIFICATION_AGENT" + android:protectionLevel="signature|privileged" + android:featureFlag="android.content.pm.verification_service" /> + + <!-- @SystemApi + @FlaggedApi("android.content.pm.verification_service") + Must be required by a privileged {@link android.content.pm.verify.pkg.VerifierService} + to ensure that only the system can bind to it. + This permission should not be held by anything other than the system. + <p>Not for use by third-party applications. </p> + <p>Protection level: signature + @hide + --> + <permission android:name="android.permission.BIND_VERIFICATION_AGENT" + android:protectionLevel="internal" + android:featureFlag="android.content.pm.verification_service" /> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java new file mode 100644 index 000000000000..987f68d4f9e1 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.pm.verify; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.pm.SharedLibraryInfo; +import android.content.pm.SigningInfo; +import android.content.pm.VersionedPackage; +import android.content.pm.verify.pkg.IVerificationSessionCallback; +import android.content.pm.verify.pkg.IVerificationSessionInterface; +import android.content.pm.verify.pkg.VerificationSession; +import android.content.pm.verify.pkg.VerificationStatus; +import android.net.Uri; +import android.os.Parcel; +import android.os.PersistableBundle; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Presubmit +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VerificationSessionTest { + private static final int TEST_ID = 100; + private static final int TEST_INSTALL_SESSION_ID = 33; + private static final String TEST_PACKAGE_NAME = "com.foo"; + private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test"); + private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo(); + private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO1 = + new SharedLibraryInfo("sharedLibPath1", TEST_PACKAGE_NAME, + Collections.singletonList("path1"), "sharedLib1", 101, + SharedLibraryInfo.TYPE_DYNAMIC, new VersionedPackage(TEST_PACKAGE_NAME, 1), + null, null, false); + private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO2 = + new SharedLibraryInfo("sharedLibPath2", TEST_PACKAGE_NAME, + Collections.singletonList("path2"), "sharedLib2", 102, + SharedLibraryInfo.TYPE_DYNAMIC, + new VersionedPackage(TEST_PACKAGE_NAME, 2), null, null, false); + private static final long TEST_TIMEOUT_TIME = System.currentTimeMillis(); + private static final long TEST_EXTEND_TIME = 2000L; + private static final String TEST_KEY = "test key"; + private static final String TEST_VALUE = "test value"; + + private final ArrayList<SharedLibraryInfo> mTestDeclaredLibraries = new ArrayList<>(); + private final PersistableBundle mTestExtensionParams = new PersistableBundle(); + @Mock + private IVerificationSessionInterface mTestSessionInterface; + @Mock + private IVerificationSessionCallback mTestCallback; + private VerificationSession mTestSession; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO1); + mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO2); + mTestExtensionParams.putString(TEST_KEY, TEST_VALUE); + mTestSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID, + TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO, mTestDeclaredLibraries, + mTestExtensionParams, mTestSessionInterface, mTestCallback); + } + + @Test + public void testParcel() { + Parcel parcel = Parcel.obtain(); + mTestSession.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + VerificationSession sessionFromParcel = + VerificationSession.CREATOR.createFromParcel(parcel); + assertThat(sessionFromParcel.getId()).isEqualTo(TEST_ID); + assertThat(sessionFromParcel.getInstallSessionId()).isEqualTo(TEST_INSTALL_SESSION_ID); + assertThat(sessionFromParcel.getPackageName()).isEqualTo(TEST_PACKAGE_NAME); + assertThat(sessionFromParcel.getStagedPackageUri()).isEqualTo(TEST_PACKAGE_URI); + assertThat(sessionFromParcel.getSigningInfo().getSigningDetails()) + .isEqualTo(TEST_SIGNING_INFO.getSigningDetails()); + List<SharedLibraryInfo> declaredLibrariesFromParcel = + sessionFromParcel.getDeclaredLibraries(); + assertThat(declaredLibrariesFromParcel).hasSize(2); + // SharedLibraryInfo doesn't have a "equals" method, so we have to check it indirectly + assertThat(declaredLibrariesFromParcel.getFirst().toString()) + .isEqualTo(TEST_SHARED_LIBRARY_INFO1.toString()); + assertThat(declaredLibrariesFromParcel.get(1).toString()) + .isEqualTo(TEST_SHARED_LIBRARY_INFO2.toString()); + // We can't directly test with PersistableBundle.equals() because the parceled bundle's + // structure is different, but all the key/value pairs should be preserved as before. + assertThat(sessionFromParcel.getExtensionParams().getString(TEST_KEY)) + .isEqualTo(mTestExtensionParams.getString(TEST_KEY)); + } + + @Test + public void testInterface() throws Exception { + when(mTestSessionInterface.getTimeoutTime(anyInt())).thenAnswer(i -> TEST_TIMEOUT_TIME); + when(mTestSessionInterface.extendTimeRemaining(anyInt(), anyLong())).thenAnswer( + i -> i.getArguments()[1]); + + assertThat(mTestSession.getTimeoutTime()).isEqualTo(TEST_TIMEOUT_TIME); + verify(mTestSessionInterface, times(1)).getTimeoutTime(eq(TEST_ID)); + assertThat(mTestSession.extendTimeRemaining(TEST_EXTEND_TIME)).isEqualTo(TEST_EXTEND_TIME); + verify(mTestSessionInterface, times(1)).extendTimeRemaining( + eq(TEST_ID), eq(TEST_EXTEND_TIME)); + } + + @Test + public void testCallback() throws Exception { + PersistableBundle response = new PersistableBundle(); + response.putString("test key", "test value"); + final VerificationStatus status = + new VerificationStatus.Builder().setVerified(true).build(); + mTestSession.reportVerificationComplete(status); + verify(mTestCallback, times(1)).reportVerificationComplete( + eq(TEST_ID), eq(status)); + mTestSession.reportVerificationComplete(status, response); + verify(mTestCallback, times(1)) + .reportVerificationCompleteWithExtensionResponse( + eq(TEST_ID), eq(status), eq(response)); + + final int reason = VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN; + mTestSession.reportVerificationIncomplete(reason); + verify(mTestCallback, times(1)).reportVerificationIncomplete( + eq(TEST_ID), eq(reason)); + } +} diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java new file mode 100644 index 000000000000..67d407a72925 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.pm.verify; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.pm.verify.pkg.VerificationStatus; +import android.os.Parcel; +import android.os.PersistableBundle; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@Presubmit +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VerificationStatusTest { + private static final boolean TEST_VERIFIED = true; + private static final int TEST_ASL_STATUS = VerificationStatus.VERIFIER_STATUS_ASL_GOOD; + private static final String TEST_FAILURE_MESSAGE = "test test"; + private static final String TEST_KEY = "test key"; + private static final String TEST_VALUE = "test value"; + private final PersistableBundle mTestExtras = new PersistableBundle(); + private VerificationStatus mStatus; + + @Before + public void setUpWithBuilder() { + mTestExtras.putString(TEST_KEY, TEST_VALUE); + mStatus = new VerificationStatus.Builder() + .setAslStatus(TEST_ASL_STATUS) + .setFailureMessage(TEST_FAILURE_MESSAGE) + .setVerified(TEST_VERIFIED) + .build(); + } + + @Test + public void testGetters() { + assertThat(mStatus.isVerified()).isEqualTo(TEST_VERIFIED); + assertThat(mStatus.getAslStatus()).isEqualTo(TEST_ASL_STATUS); + assertThat(mStatus.getFailureMessage()).isEqualTo(TEST_FAILURE_MESSAGE); + } + + @Test + public void testParcel() { + Parcel parcel = Parcel.obtain(); + mStatus.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + VerificationStatus statusFromParcel = VerificationStatus.CREATOR.createFromParcel(parcel); + assertThat(statusFromParcel.isVerified()).isEqualTo(TEST_VERIFIED); + assertThat(statusFromParcel.getAslStatus()).isEqualTo(TEST_ASL_STATUS); + assertThat(statusFromParcel.getFailureMessage()).isEqualTo(TEST_FAILURE_MESSAGE); + } +} diff --git a/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java new file mode 100644 index 000000000000..7f73a1eb4b48 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.pm.verify; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.SigningInfo; +import android.content.pm.verify.pkg.IVerifierService; +import android.content.pm.verify.pkg.VerificationSession; +import android.content.pm.verify.pkg.VerifierService; +import android.net.Uri; +import android.os.PersistableBundle; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mockito; + +import java.util.ArrayList; + +@Presubmit +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VerifierServiceTest { + private static final int TEST_ID = 100; + private static final int TEST_INSTALL_SESSION_ID = 33; + private static final String TEST_PACKAGE_NAME = "com.foo"; + private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test"); + private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo(); + private VerifierService mService; + private VerificationSession mSession; + + @Before + public void setUp() { + mService = Mockito.mock(VerifierService.class, Answers.CALLS_REAL_METHODS); + mSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID, + TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO, + new ArrayList<>(), + new PersistableBundle(), null, null); + } + + @Test + public void testBind() throws Exception { + Intent intent = Mockito.mock(Intent.class); + when(intent.getAction()).thenReturn(PackageManager.ACTION_VERIFY_PACKAGE); + IVerifierService binder = + (IVerifierService) mService.onBind(intent); + assertThat(binder).isNotNull(); + binder.onPackageNameAvailable(TEST_PACKAGE_NAME); + verify(mService).onPackageNameAvailable(eq(TEST_PACKAGE_NAME)); + binder.onVerificationCancelled(TEST_PACKAGE_NAME); + verify(mService).onVerificationCancelled(eq(TEST_PACKAGE_NAME)); + binder.onVerificationRequired(mSession); + verify(mService).onVerificationRequired(eq(mSession)); + binder.onVerificationRetry(mSession); + verify(mService).onVerificationRetry(eq(mSession)); + binder.onVerificationTimeout(TEST_ID); + verify(mService).onVerificationTimeout(eq(TEST_ID)); + } + + @Test + public void testBindFailsWithWrongIntent() { + Intent intent = Mockito.mock(Intent.class); + when(intent.getAction()).thenReturn(Intent.ACTION_SEND); + assertThat(mService.onBind(intent)).isNull(); + } +} |