diff options
32 files changed, 1750 insertions, 673 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 0eb5553e11fa..761230f8188a 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2837,21 +2837,29 @@ package android.content.pm.verify.domain { method @NonNull public String getPackageName(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.domain.DomainVerificationInfo> CREATOR; + field public static final int STATE_FIRST_VERIFIER_DEFINED = 1024; // 0x400 + field public static final int STATE_MODIFIABLE_UNVERIFIED = 3; // 0x3 + field public static final int STATE_MODIFIABLE_VERIFIED = 4; // 0x4 + field public static final int STATE_NO_RESPONSE = 0; // 0x0 + field public static final int STATE_SUCCESS = 1; // 0x1 + field public static final int STATE_UNMODIFIABLE = 2; // 0x2 } public final class DomainVerificationManager { method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.DOMAIN_VERIFICATION_AGENT, android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION}) public android.content.pm.verify.domain.DomainVerificationInfo getDomainVerificationInfo(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException; method @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public java.util.List<android.content.pm.verify.domain.DomainOwner> getOwnersForDomain(@NonNull String); - method public static boolean isStateModifiable(int); - method public static boolean isStateVerified(int); method @NonNull @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public java.util.List<java.lang.String> queryValidVerificationPackageNames(); method @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public void setDomainVerificationLinkHandlingAllowed(@NonNull String, boolean) throws android.content.pm.PackageManager.NameNotFoundException; - method @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public void setDomainVerificationStatus(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, int) throws android.content.pm.PackageManager.NameNotFoundException; - method @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public void setDomainVerificationUserSelection(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, boolean) throws android.content.pm.PackageManager.NameNotFoundException; + method @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public int setDomainVerificationStatus(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, int) throws android.content.pm.PackageManager.NameNotFoundException; + method @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public int setDomainVerificationUserSelection(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, boolean) throws android.content.pm.PackageManager.NameNotFoundException; + field public static final int ERROR_DOMAIN_SET_ID_INVALID = 1; // 0x1 + field public static final int ERROR_DOMAIN_SET_ID_NULL = 2; // 0x2 + field public static final int ERROR_DOMAIN_SET_NULL_OR_EMPTY = 3; // 0x3 + field public static final int ERROR_INVALID_STATE_CODE = 6; // 0x6 + field public static final int ERROR_UNABLE_TO_APPROVE = 5; // 0x5 + field public static final int ERROR_UNKNOWN_DOMAIN = 4; // 0x4 field public static final String EXTRA_VERIFICATION_REQUEST = "android.content.pm.verify.domain.extra.VERIFICATION_REQUEST"; - field public static final int STATE_FIRST_VERIFIER_DEFINED = 1024; // 0x400 - field public static final int STATE_NO_RESPONSE = 0; // 0x0 - field public static final int STATE_SUCCESS = 1; // 0x1 + field public static final int STATUS_OK = 0; // 0x0 } public final class DomainVerificationRequest implements android.os.Parcelable { diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java index fe8e4d7b1bb2..6f07dd7a24e8 100644 --- a/core/java/android/content/pm/ResolveInfo.java +++ b/core/java/android/content/pm/ResolveInfo.java @@ -19,6 +19,7 @@ package android.content.pm; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; +import android.content.Intent; import android.content.IntentFilter; import android.graphics.drawable.Drawable; import android.os.Build; @@ -183,6 +184,17 @@ public class ResolveInfo implements Parcelable { @SystemApi public boolean handleAllWebDataURI; + /** + * Whether the resolved {@link IntentFilter} declares {@link Intent#CATEGORY_BROWSABLE} and is + * thus allowed to automatically resolve an {@link Intent} as it's assumed the action is safe + * for the user. + * + * Note that the above doesn't apply when this is the only result is returned in the candidate + * set, as the system will not prompt before opening the result. It only applies when there are + * multiple candidates. + */ + private final boolean mAutoResolutionAllowed; + /** {@hide} */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public ComponentInfo getComponentInfo() { @@ -364,8 +376,26 @@ public class ResolveInfo implements Parcelable { && INTENT_FORWARDER_ACTIVITY.equals(activityInfo.targetActivity); } + /** + * @see #mAutoResolutionAllowed + * @hide + */ + public boolean isAutoResolutionAllowed() { + return mAutoResolutionAllowed; + } + public ResolveInfo() { targetUserId = UserHandle.USER_CURRENT; + + // It's safer to assume that an unaware caller that constructs a ResolveInfo doesn't + // accidentally mark a result as auto resolveable. + mAutoResolutionAllowed = false; + } + + /** @hide */ + public ResolveInfo(boolean autoResolutionAllowed) { + targetUserId = UserHandle.USER_CURRENT; + mAutoResolutionAllowed = autoResolutionAllowed; } public ResolveInfo(ResolveInfo orig) { @@ -386,6 +416,7 @@ public class ResolveInfo implements Parcelable { system = orig.system; targetUserId = orig.targetUserId; handleAllWebDataURI = orig.handleAllWebDataURI; + mAutoResolutionAllowed = orig.mAutoResolutionAllowed; isInstantAppAvailable = orig.isInstantAppAvailable; } @@ -450,6 +481,7 @@ public class ResolveInfo implements Parcelable { dest.writeInt(noResourceId ? 1 : 0); dest.writeInt(iconResourceId); dest.writeInt(handleAllWebDataURI ? 1 : 0); + dest.writeInt(mAutoResolutionAllowed ? 1 : 0); dest.writeInt(isInstantAppAvailable ? 1 : 0); } @@ -498,6 +530,7 @@ public class ResolveInfo implements Parcelable { noResourceId = source.readInt() != 0; iconResourceId = source.readInt(); handleAllWebDataURI = source.readInt() != 0; + mAutoResolutionAllowed = source.readInt() != 0; isInstantAppAvailable = source.readInt() != 0; } diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationInfo.java b/core/java/android/content/pm/verify/domain/DomainVerificationInfo.java index 7c335b1d26dd..62277ef671e3 100644 --- a/core/java/android/content/pm/verify/domain/DomainVerificationInfo.java +++ b/core/java/android/content/pm/verify/domain/DomainVerificationInfo.java @@ -43,9 +43,50 @@ import java.util.UUID; */ @SystemApi @DataClass(genAidl = true, genHiddenConstructor = true, genParcelable = true, genToString = true, - genEqualsHashCode = true) + genEqualsHashCode = true, genHiddenConstDefs = true) public final class DomainVerificationInfo implements Parcelable { + // Implementation note: the following states are OUTPUT only. Any value that is synonymous with + // a value in DomainVerificationState must be the EXACT same integer, so that state + // transformation does not have to occur when sending input into the system, assuming that the + // system only accepts those synonymous values. The public API values declared here are only + // used when exiting the system server to prepare this data object for consumption by the + // verification agent. These constants should only be referenced inside public API classes. + // The server must use DomainVerificationState. + + /** + * No response has been recorded by either the system or any verification agent. + */ + public static final int STATE_NO_RESPONSE = DomainVerificationState.STATE_NO_RESPONSE; + + /** + * The domain has been explicitly verified. + */ + public static final int STATE_SUCCESS = DomainVerificationState.STATE_SUCCESS; + + /** + * Indicates the host cannot be modified by the verification agent. + */ + public static final int STATE_UNMODIFIABLE = 2; + + /** + * Indicates the host can be modified by the verification agent and is not considered verified. + */ + public static final int STATE_MODIFIABLE_UNVERIFIED = 3; + + /** + * Indicates the host can be modified by the verification agent and is considered verified. + */ + public static final int STATE_MODIFIABLE_VERIFIED = 4; + + /** + * The first available custom response code. This and any greater integer, along with {@link + * #STATE_SUCCESS} are the only values settable by the verification agent. All custom values + * will be treated as if the domain is unverified. + */ + public static final int STATE_FIRST_VERIFIER_DEFINED = + DomainVerificationState.STATE_FIRST_VERIFIER_DEFINED; + /** * A domain verification ID for use in later API calls. This represents the snapshot of the * domains for a package on device, and will be invalidated whenever the package changes. @@ -74,16 +115,12 @@ public final class DomainVerificationInfo implements Parcelable { /** * Map of host names to their current state. State is an integer, which defaults to {@link - * DomainVerificationManager#STATE_NO_RESPONSE}. State can be modified by the domain - * verification agent (the intended consumer of this API), which can be equal to {@link - * DomainVerificationManager#STATE_SUCCESS} when verified, or equal to or greater than {@link - * DomainVerificationManager#STATE_FIRST_VERIFIER_DEFINED} for any unsuccessful response. + * #STATE_NO_RESPONSE}. State can be modified by the domain verification agent (the intended + * consumer of this API), which can be equal to {@link #STATE_SUCCESS} when verified, or equal + * to or greater than {@link #STATE_FIRST_VERIFIER_DEFINED} for any unsuccessful response. * <p> - * Any value non-inclusive between those 2 values are reserved for use by the system. The domain - * verification agent may be able to act on these reserved values, and this ability can be - * queried using {@link DomainVerificationManager#isStateModifiable(int)}. It is expected that - * the agent attempt to verify all domains that it can modify the state of, even if it does not - * understand the meaning of those values. + * Hosts which cannot be edited will be assigned {@link #STATE_UNMODIFIABLE}. It is expected + * that the agent attempt to verify all domains that it can modify the state of. */ @NonNull private final Map<String, Integer> mHostToStateMap; @@ -112,6 +149,39 @@ public final class DomainVerificationInfo implements Parcelable { //@formatter:off + /** @hide */ + @android.annotation.IntDef(prefix = "STATE_", value = { + STATE_NO_RESPONSE, + STATE_SUCCESS, + STATE_UNMODIFIABLE, + STATE_MODIFIABLE_UNVERIFIED, + STATE_MODIFIABLE_VERIFIED, + STATE_FIRST_VERIFIER_DEFINED + }) + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface State {} + + /** @hide */ + @DataClass.Generated.Member + public static String stateToString(@State int value) { + switch (value) { + case STATE_NO_RESPONSE: + return "STATE_NO_RESPONSE"; + case STATE_SUCCESS: + return "STATE_SUCCESS"; + case STATE_UNMODIFIABLE: + return "STATE_UNMODIFIABLE"; + case STATE_MODIFIABLE_UNVERIFIED: + return "STATE_MODIFIABLE_UNVERIFIED"; + case STATE_MODIFIABLE_VERIFIED: + return "STATE_MODIFIABLE_VERIFIED"; + case STATE_FIRST_VERIFIER_DEFINED: + return "STATE_FIRST_VERIFIER_DEFINED"; + default: return Integer.toHexString(value); + } + } + /** * Creates a new DomainVerificationInfo. * @@ -134,16 +204,12 @@ public final class DomainVerificationInfo implements Parcelable { * The package name that this data corresponds to. * @param hostToStateMap * Map of host names to their current state. State is an integer, which defaults to {@link - * DomainVerificationManager#STATE_NO_RESPONSE}. State can be modified by the domain - * verification agent (the intended consumer of this API), which can be equal to {@link - * DomainVerificationManager#STATE_SUCCESS} when verified, or equal to or greater than {@link - * DomainVerificationManager#STATE_FIRST_VERIFIER_DEFINED} for any unsuccessful response. + * #STATE_NO_RESPONSE}. State can be modified by the domain verification agent (the intended + * consumer of this API), which can be equal to {@link #STATE_SUCCESS} when verified, or equal + * to or greater than {@link #STATE_FIRST_VERIFIER_DEFINED} for any unsuccessful response. * <p> - * Any value non-inclusive between those 2 values are reserved for use by the system. The domain - * verification agent may be able to act on these reserved values, and this ability can be - * queried using {@link DomainVerificationManager#isStateModifiable(int)}. It is expected that - * the agent attempt to verify all domains that it can modify the state of, even if it does not - * understand the meaning of those values. + * Hosts which cannot be edited will be assigned {@link #STATE_UNMODIFIABLE}. It is expected + * that the agent attempt to verify all domains that it can modify the state of. * @hide */ @DataClass.Generated.Member @@ -195,16 +261,12 @@ public final class DomainVerificationInfo implements Parcelable { /** * Map of host names to their current state. State is an integer, which defaults to {@link - * DomainVerificationManager#STATE_NO_RESPONSE}. State can be modified by the domain - * verification agent (the intended consumer of this API), which can be equal to {@link - * DomainVerificationManager#STATE_SUCCESS} when verified, or equal to or greater than {@link - * DomainVerificationManager#STATE_FIRST_VERIFIER_DEFINED} for any unsuccessful response. + * #STATE_NO_RESPONSE}. State can be modified by the domain verification agent (the intended + * consumer of this API), which can be equal to {@link #STATE_SUCCESS} when verified, or equal + * to or greater than {@link #STATE_FIRST_VERIFIER_DEFINED} for any unsuccessful response. * <p> - * Any value non-inclusive between those 2 values are reserved for use by the system. The domain - * verification agent may be able to act on these reserved values, and this ability can be - * queried using {@link DomainVerificationManager#isStateModifiable(int)}. It is expected that - * the agent attempt to verify all domains that it can modify the state of, even if it does not - * understand the meaning of those values. + * Hosts which cannot be edited will be assigned {@link #STATE_UNMODIFIABLE}. It is expected + * that the agent attempt to verify all domains that it can modify the state of. */ @DataClass.Generated.Member public @NonNull Map<String,Integer> getHostToStateMap() { @@ -320,10 +382,10 @@ public final class DomainVerificationInfo implements Parcelable { }; @DataClass.Generated( - time = 1614721812023L, + time = 1615317187669L, codegenVersion = "1.0.22", sourceFile = "frameworks/base/core/java/android/content/pm/verify/domain/DomainVerificationInfo.java", - inputSignatures = "private final @android.annotation.NonNull @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForUUID.class) java.util.UUID mIdentifier\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.lang.Integer> mHostToStateMap\nprivate void parcelHostToStateMap(android.os.Parcel,int)\nprivate java.util.Map<java.lang.String,java.lang.Integer> unparcelHostToStateMap(android.os.Parcel)\nclass DomainVerificationInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genAidl=true, genHiddenConstructor=true, genParcelable=true, genToString=true, genEqualsHashCode=true)") + inputSignatures = "public static final int STATE_NO_RESPONSE\npublic static final int STATE_SUCCESS\npublic static final int STATE_UNMODIFIABLE\npublic static final int STATE_MODIFIABLE_UNVERIFIED\npublic static final int STATE_MODIFIABLE_VERIFIED\npublic static final int STATE_FIRST_VERIFIER_DEFINED\nprivate final @android.annotation.NonNull @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForUUID.class) java.util.UUID mIdentifier\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.lang.Integer> mHostToStateMap\nprivate void parcelHostToStateMap(android.os.Parcel,int)\nprivate java.util.Map<java.lang.String,java.lang.Integer> unparcelHostToStateMap(android.os.Parcel)\nclass DomainVerificationInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genAidl=true, genHiddenConstructor=true, genParcelable=true, genToString=true, genEqualsHashCode=true, genHiddenConstDefs=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java index f7c81bcffda3..55e19f2727bf 100644 --- a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java +++ b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java @@ -60,154 +60,97 @@ public final class DomainVerificationManager { "android.content.pm.verify.domain.extra.VERIFICATION_REQUEST"; /** - * No response has been recorded by either the system or any verification agent. + * Default return code for when a method has succeeded. * * @hide */ @SystemApi - public static final int STATE_NO_RESPONSE = DomainVerificationState.STATE_NO_RESPONSE; + public static final int STATUS_OK = 0; /** - * The verification agent has explicitly verified the domain at some point. + * The provided domain set ID was invalid, probably due to the package being updated between + * the initial request that provided the ID and the method call that used it. This usually + * means the work being processed by the verification agent is outdated and a new request + * should be scheduled, which should already be in progress as part of the + * {@link Intent#ACTION_DOMAINS_NEED_VERIFICATION} broadcast. * * @hide */ @SystemApi - public static final int STATE_SUCCESS = DomainVerificationState.STATE_SUCCESS; + public static final int ERROR_DOMAIN_SET_ID_INVALID = 1; /** - * The first available custom response code. This and any greater integer, along with {@link - * #STATE_SUCCESS} are the only values settable by the verification agent. All values will be - * treated as if the domain is unverified. + * The provided domain set ID was null. This is a developer error. * * @hide */ @SystemApi - public static final int STATE_FIRST_VERIFIER_DEFINED = - DomainVerificationState.STATE_FIRST_VERIFIER_DEFINED; + public static final int ERROR_DOMAIN_SET_ID_NULL = 2; /** + * The provided set of domains was null or empty. This is a developer error. + * * @hide */ - @NonNull - public static String stateToDebugString(@DomainVerificationState.State int state) { - switch (state) { - case DomainVerificationState.STATE_NO_RESPONSE: - return "none"; - case DomainVerificationState.STATE_SUCCESS: - return "verified"; - case DomainVerificationState.STATE_APPROVED: - return "approved"; - case DomainVerificationState.STATE_DENIED: - return "denied"; - case DomainVerificationState.STATE_MIGRATED: - return "migrated"; - case DomainVerificationState.STATE_RESTORED: - return "restored"; - case DomainVerificationState.STATE_LEGACY_FAILURE: - return "legacy_failure"; - case DomainVerificationState.STATE_SYS_CONFIG: - return "system_configured"; - default: - return String.valueOf(state); - } - } + @SystemApi + public static final int ERROR_DOMAIN_SET_NULL_OR_EMPTY = 3; /** - * Checks if a state considers the corresponding domain to be successfully verified. The domain - * verification agent may use this to determine whether or not to re-verify a domain. + * The provided set of domains contains a domain not declared by the target package. This + * usually means the work being processed by the verification agent is outdated and a new + * request should be scheduled, which should already be in progress as part of the + * {@link Intent#ACTION_DOMAINS_NEED_VERIFICATION} broadcast. * * @hide */ @SystemApi - public static boolean isStateVerified(@DomainVerificationState.State int state) { - switch (state) { - case DomainVerificationState.STATE_SUCCESS: - case DomainVerificationState.STATE_APPROVED: - case DomainVerificationState.STATE_MIGRATED: - case DomainVerificationState.STATE_RESTORED: - case DomainVerificationState.STATE_SYS_CONFIG: - return true; - case DomainVerificationState.STATE_NO_RESPONSE: - case DomainVerificationState.STATE_DENIED: - case DomainVerificationState.STATE_LEGACY_FAILURE: - default: - return false; - } - } + public static final int ERROR_UNKNOWN_DOMAIN = 4; /** - * Checks if a state is modifiable by the domain verification agent. This is useful as the - * platform may add new state codes in newer versions, and older verification agents can use - * this method to determine if a state can be changed without having to be aware of what the new - * state means. + * The system was unable to select the domain for approval. This indicates another application + * has been granted a higher approval, usually through domain verification, and the target + * package is unable to override it. * * @hide */ @SystemApi - public static boolean isStateModifiable(@DomainVerificationState.State int state) { - switch (state) { - case DomainVerificationState.STATE_NO_RESPONSE: - case DomainVerificationState.STATE_SUCCESS: - case DomainVerificationState.STATE_MIGRATED: - case DomainVerificationState.STATE_RESTORED: - case DomainVerificationState.STATE_LEGACY_FAILURE: - return true; - case DomainVerificationState.STATE_APPROVED: - case DomainVerificationState.STATE_DENIED: - case DomainVerificationState.STATE_SYS_CONFIG: - return false; - default: - return state >= DomainVerificationState.STATE_FIRST_VERIFIER_DEFINED; - } - } + public static final int ERROR_UNABLE_TO_APPROVE = 5; /** - * For determine re-verify policy. This is hidden from the domain verification agent so that no - * behavior is made based on the result. + * The provided state code is incorrect. The domain verification agent is only allowed to + * assign {@link DomainVerificationInfo#STATE_SUCCESS} or error codes equal to or greater than + * {@link DomainVerificationInfo#STATE_FIRST_VERIFIER_DEFINED}. * * @hide */ - public static boolean isStateDefault(@DomainVerificationState.State int state) { - switch (state) { - case DomainVerificationState.STATE_NO_RESPONSE: - case DomainVerificationState.STATE_MIGRATED: - case DomainVerificationState.STATE_RESTORED: - return true; - case DomainVerificationState.STATE_SUCCESS: - case DomainVerificationState.STATE_APPROVED: - case DomainVerificationState.STATE_DENIED: - case DomainVerificationState.STATE_LEGACY_FAILURE: - case DomainVerificationState.STATE_SYS_CONFIG: - default: - return false; - } - } + @SystemApi + public static final int ERROR_INVALID_STATE_CODE = 6; /** + * Used to communicate through {@link ServiceSpecificException}. Should not be exposed as API. + * * @hide */ - public static final int ERROR_INVALID_DOMAIN_SET = 1; - /** - * @hide - */ - public static final int ERROR_NAME_NOT_FOUND = 2; + public static final int INTERNAL_ERROR_NAME_NOT_FOUND = 1; /** * @hide */ @IntDef(prefix = {"ERROR_"}, value = { - ERROR_INVALID_DOMAIN_SET, - ERROR_NAME_NOT_FOUND, + ERROR_DOMAIN_SET_ID_INVALID, + ERROR_DOMAIN_SET_ID_NULL, + ERROR_DOMAIN_SET_NULL_OR_EMPTY, + ERROR_UNKNOWN_DOMAIN, + ERROR_UNABLE_TO_APPROVE, + ERROR_INVALID_STATE_CODE }) - private @interface Error { + public @interface Error { } private final Context mContext; private final IDomainVerificationManager mDomainVerificationManager; - /** * System service to access the domain verification APIs. * <p> @@ -289,27 +232,24 @@ public final class DomainVerificationManager { * @param domainSetId See {@link DomainVerificationInfo#getIdentifier()}. * @param domains List of host names to change the state of. * @param state See {@link DomainVerificationInfo#getHostToStateMap()}. - * @throws IllegalArgumentException If the ID is invalidated or the {@param domains} are - * invalid. This usually means the work being processed by the - * verification agent is outdated and a new request should be - * scheduled, if one has not already been done as part of the - * {@link Intent#ACTION_DOMAINS_NEED_VERIFICATION} broadcast. * @throws NameNotFoundException If the ID is known to be good, but the package is * unavailable. This may be because the package is installed on * a volume that is no longer mounted. This error is * unrecoverable until the package is available again, and * should not be re-tried except on a time scheduled basis. + * @return error code or {@link #STATUS_OK} if successful + * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) - public void setDomainVerificationStatus(@NonNull UUID domainSetId, @NonNull Set<String> domains, - @DomainVerificationState.State int state) throws NameNotFoundException { + public int setDomainVerificationStatus(@NonNull UUID domainSetId, @NonNull Set<String> domains, + int state) throws NameNotFoundException { try { - mDomainVerificationManager.setDomainVerificationStatus(domainSetId.toString(), + return mDomainVerificationManager.setDomainVerificationStatus(domainSetId.toString(), new DomainSet(domains), state); } catch (Exception e) { - Exception converted = rethrow(e, domainSetId); + Exception converted = rethrow(e, null); if (converted instanceof NameNotFoundException) { throw (NameNotFoundException) converted; } else if (converted instanceof RuntimeException) { @@ -338,7 +278,7 @@ public final class DomainVerificationManager { mDomainVerificationManager.setDomainVerificationLinkHandlingAllowed(packageName, allowed, mContext.getUserId()); } catch (Exception e) { - Exception converted = rethrow(e, packageName); + Exception converted = rethrow(e, null); if (converted instanceof NameNotFoundException) { throw (NameNotFoundException) converted; } else if (converted instanceof RuntimeException) { @@ -372,24 +312,24 @@ public final class DomainVerificationManager { * @param domainSetId See {@link DomainVerificationInfo#getIdentifier()}. * @param domains The domains to toggle the state of. * @param enabled Whether or not the app should automatically open the domains specified. - * @throws IllegalArgumentException If the ID is invalidated or the {@param domains} are - * invalid. * @throws NameNotFoundException If the ID is known to be good, but the package is * unavailable. This may be because the package is installed on * a volume that is no longer mounted. This error is * unrecoverable until the package is available again, and * should not be re-tried except on a time scheduled basis. + * @return error code or {@link #STATUS_OK} if successful + * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) - public void setDomainVerificationUserSelection(@NonNull UUID domainSetId, + public int setDomainVerificationUserSelection(@NonNull UUID domainSetId, @NonNull Set<String> domains, boolean enabled) throws NameNotFoundException { try { - mDomainVerificationManager.setDomainVerificationUserSelection(domainSetId.toString(), - new DomainSet(domains), enabled, mContext.getUserId()); + return mDomainVerificationManager.setDomainVerificationUserSelection( + domainSetId.toString(), new DomainSet(domains), enabled, mContext.getUserId()); } catch (Exception e) { - Exception converted = rethrow(e, domainSetId); + Exception converted = rethrow(e, null); if (converted instanceof NameNotFoundException) { throw (NameNotFoundException) converted; } else if (converted instanceof RuntimeException) { @@ -447,123 +387,22 @@ public final class DomainVerificationManager { } } - private Exception rethrow(Exception exception, @Nullable UUID domainSetId) { - return rethrow(exception, domainSetId, null); - } - private Exception rethrow(Exception exception, @Nullable String packageName) { - return rethrow(exception, null, packageName); - } - - private Exception rethrow(Exception exception, @Nullable UUID domainSetId, - @Nullable String packageName) { if (exception instanceof ServiceSpecificException) { - int packedErrorCode = ((ServiceSpecificException) exception).errorCode; + int serviceSpecificErrorCode = ((ServiceSpecificException) exception).errorCode; if (packageName == null) { packageName = exception.getMessage(); } - @Error int managerErrorCode = packedErrorCode & 0xFFFF; - switch (managerErrorCode) { - case ERROR_INVALID_DOMAIN_SET: - int errorSpecificCode = packedErrorCode >> 16; - return new IllegalArgumentException(InvalidDomainSetException.buildMessage( - domainSetId, packageName, errorSpecificCode)); - case ERROR_NAME_NOT_FOUND: - return new NameNotFoundException(packageName); - default: - return exception; + if (serviceSpecificErrorCode == INTERNAL_ERROR_NAME_NOT_FOUND) { + return new NameNotFoundException(packageName); } + + return exception; } else if (exception instanceof RemoteException) { return ((RemoteException) exception).rethrowFromSystemServer(); } else { return exception; } } - - /** - * Thrown if a {@link DomainVerificationInfo#getIdentifier()}} or an associated set of domains - * provided by the caller is no longer valid. This may be recoverable, and the caller should - * re-query the package name associated with the ID using - * {@link #getDomainVerificationInfo(String)} - * in order to check. If that also fails, then the package is no longer known to the device and - * thus all pending work for it should be dropped. - * - * @hide - */ - public static class InvalidDomainSetException extends IllegalArgumentException { - - public static final int REASON_ID_NULL = 1; - public static final int REASON_ID_INVALID = 2; - public static final int REASON_SET_NULL_OR_EMPTY = 3; - public static final int REASON_UNKNOWN_DOMAIN = 4; - public static final int REASON_UNABLE_TO_APPROVE = 5; - - /** - * @hide - */ - @IntDef({ - REASON_ID_NULL, - REASON_ID_INVALID, - REASON_SET_NULL_OR_EMPTY, - REASON_UNKNOWN_DOMAIN, - REASON_UNABLE_TO_APPROVE - }) - public @interface Reason { - } - - public static String buildMessage(@Nullable UUID domainSetId, @Nullable String packageName, - @Reason int reason) { - switch (reason) { - case REASON_ID_NULL: - return "Domain set ID cannot be null"; - case REASON_ID_INVALID: - return "Domain set ID " + domainSetId + " has been invalidated"; - case REASON_SET_NULL_OR_EMPTY: - return "Domain set cannot be null or empty"; - case REASON_UNKNOWN_DOMAIN: - return "Domain set contains value that was not declared by the target package " - + packageName; - case REASON_UNABLE_TO_APPROVE: - return "Domain set contains value that was owned by another package"; - default: - return "Unknown failure"; - } - } - - @Reason - private final int mReason; - - @Nullable - private final UUID mDomainSetId; - - @Nullable - private final String mPackageName; - - /** - * @hide - */ - public InvalidDomainSetException(@Nullable UUID domainSetId, @Nullable String packageName, - @Reason int reason) { - super(buildMessage(domainSetId, packageName, reason)); - mDomainSetId = domainSetId; - mPackageName = packageName; - mReason = reason; - } - - @Nullable - public UUID getDomainSetId() { - return mDomainSetId; - } - - @Nullable - public String getPackageName() { - return mPackageName; - } - - @Reason - public int getReason() { - return mReason; - } - } } diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationState.java b/core/java/android/content/pm/verify/domain/DomainVerificationState.java index 17593ef2aeb1..8e28042bf581 100644 --- a/core/java/android/content/pm/verify/domain/DomainVerificationState.java +++ b/core/java/android/content/pm/verify/domain/DomainVerificationState.java @@ -17,15 +17,13 @@ package android.content.pm.verify.domain; import android.annotation.IntDef; +import android.annotation.NonNull; /** * @hide */ public interface DomainVerificationState { - /** - * @hide - */ @IntDef({ STATE_NO_RESPONSE, STATE_SUCCESS, @@ -42,12 +40,12 @@ public interface DomainVerificationState { // TODO(b/159952358): Document all the places that states need to be updated when one is added /** - * @see DomainVerificationManager#STATE_NO_RESPONSE + * @see DomainVerificationInfo#STATE_NO_RESPONSE */ int STATE_NO_RESPONSE = 0; /** - * @see DomainVerificationManager#STATE_SUCCESS + * @see DomainVerificationInfo#STATE_SUCCESS */ int STATE_SUCCESS = 1; @@ -94,7 +92,132 @@ public interface DomainVerificationState { int STATE_SYS_CONFIG = 7; /** - * @see DomainVerificationManager#STATE_FIRST_VERIFIER_DEFINED + * @see DomainVerificationInfo#STATE_FIRST_VERIFIER_DEFINED */ int STATE_FIRST_VERIFIER_DEFINED = 0b10000000000; + + @NonNull + static String stateToDebugString(@DomainVerificationState.State int state) { + switch (state) { + case DomainVerificationState.STATE_NO_RESPONSE: + return "none"; + case DomainVerificationState.STATE_SUCCESS: + return "verified"; + case DomainVerificationState.STATE_APPROVED: + return "approved"; + case DomainVerificationState.STATE_DENIED: + return "denied"; + case DomainVerificationState.STATE_MIGRATED: + return "migrated"; + case DomainVerificationState.STATE_RESTORED: + return "restored"; + case DomainVerificationState.STATE_LEGACY_FAILURE: + return "legacy_failure"; + case DomainVerificationState.STATE_SYS_CONFIG: + return "system_configured"; + default: + return String.valueOf(state); + } + } + + /** + * For determining re-verify policy. This is hidden from the domain verification agent so that + * no behavior is made based on the result. + */ + static boolean isDefault(@State int state) { + switch (state) { + case STATE_NO_RESPONSE: + case STATE_MIGRATED: + case STATE_RESTORED: + return true; + case STATE_SUCCESS: + case STATE_APPROVED: + case STATE_DENIED: + case STATE_LEGACY_FAILURE: + case STATE_SYS_CONFIG: + default: + return false; + } + } + + /** + * Checks if a state considers the corresponding domain to be successfully verified. The domain + * verification agent may use this to determine whether or not to re-verify a domain. + */ + static boolean isVerified(@DomainVerificationState.State int state) { + switch (state) { + case DomainVerificationState.STATE_SUCCESS: + case DomainVerificationState.STATE_APPROVED: + case DomainVerificationState.STATE_MIGRATED: + case DomainVerificationState.STATE_RESTORED: + case DomainVerificationState.STATE_SYS_CONFIG: + return true; + case DomainVerificationState.STATE_NO_RESPONSE: + case DomainVerificationState.STATE_DENIED: + case DomainVerificationState.STATE_LEGACY_FAILURE: + default: + return false; + } + } + + /** + * Checks if a state is modifiable by the domain verification agent. This is useful as the + * platform may add new state codes in newer versions, and older verification agents can use + * this method to determine if a state can be changed without having to be aware of what the new + * state means. + */ + static boolean isModifiable(@DomainVerificationState.State int state) { + switch (state) { + case DomainVerificationState.STATE_NO_RESPONSE: + case DomainVerificationState.STATE_SUCCESS: + case DomainVerificationState.STATE_MIGRATED: + case DomainVerificationState.STATE_RESTORED: + case DomainVerificationState.STATE_LEGACY_FAILURE: + return true; + case DomainVerificationState.STATE_APPROVED: + case DomainVerificationState.STATE_DENIED: + case DomainVerificationState.STATE_SYS_CONFIG: + return false; + default: + return state >= DomainVerificationState.STATE_FIRST_VERIFIER_DEFINED; + } + } + + /** + * Whether the state is migrated when updating a package. Generally this is only for states + * that maintain verification state or were set by an explicit user or developer action. + */ + static boolean shouldMigrate(@State int state) { + switch (state) { + case STATE_SUCCESS: + case STATE_MIGRATED: + case STATE_RESTORED: + case STATE_APPROVED: + case STATE_DENIED: + return true; + case STATE_NO_RESPONSE: + case STATE_LEGACY_FAILURE: + case STATE_SYS_CONFIG: + case STATE_FIRST_VERIFIER_DEFINED: + default: + return false; + } + } + + @DomainVerificationInfo.State + static int convertToInfoState(@State int internalState) { + if (internalState >= STATE_FIRST_VERIFIER_DEFINED) { + return internalState; + } else if (internalState == STATE_NO_RESPONSE) { + return DomainVerificationInfo.STATE_NO_RESPONSE; + } else if (internalState == STATE_SUCCESS) { + return DomainVerificationInfo.STATE_SUCCESS; + } else if (!isModifiable(internalState)) { + return DomainVerificationInfo.STATE_UNMODIFIABLE; + } else if (isVerified(internalState)) { + return DomainVerificationInfo.STATE_MODIFIABLE_VERIFIED; + } else { + return DomainVerificationInfo.STATE_MODIFIABLE_UNVERIFIED; + } + } } diff --git a/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl b/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl index 332b92544581..53205f3ea470 100644 --- a/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl +++ b/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl @@ -40,10 +40,10 @@ interface IDomainVerificationManager { @nullable List<DomainOwner> getOwnersForDomain(String domain, int userId); - void setDomainVerificationStatus(String domainSetId, in DomainSet domains, int state); + int setDomainVerificationStatus(String domainSetId, in DomainSet domains, int state); void setDomainVerificationLinkHandlingAllowed(String packageName, boolean allowed, int userId); - void setDomainVerificationUserSelection(String domainSetId, in DomainSet domains, + int setDomainVerificationUserSelection(String domainSetId, in DomainSet domains, boolean enabled, int userId); } diff --git a/services/core/java/com/android/server/pm/ComponentResolver.java b/services/core/java/com/android/server/pm/ComponentResolver.java index 4be509b3f464..1d556fec31ea 100644 --- a/services/core/java/com/android/server/pm/ComponentResolver.java +++ b/services/core/java/com/android/server/pm/ComponentResolver.java @@ -1492,7 +1492,7 @@ public class ComponentResolver { } return null; } - final ResolveInfo res = new ResolveInfo(); + final ResolveInfo res = new ResolveInfo(info.hasCategory(Intent.CATEGORY_BROWSABLE)); res.activityInfo = ai; if ((mFlags & PackageManager.GET_RESOLVED_FILTER) != 0) { res.filter = info; diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index dfe72b26f72a..1530e41917c7 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -1768,6 +1768,11 @@ public class PackageManagerService extends IPackageManager.Stub public int[] getAllUserIds() { return mUserManager.getUserIds(); } + + @Override + public boolean doesUserExist(@UserIdInt int userId) { + return mUserManager.exists(userId); + } } /** @@ -2639,8 +2644,7 @@ public class PackageManagerService extends IPackageManager.Stub // We'll want to include browser possibilities in a few cases boolean includeBrowser = false; - if (!DomainVerificationUtils.isDomainVerificationIntent(intent, candidates, - matchFlags)) { + if (!DomainVerificationUtils.isDomainVerificationIntent(intent, matchFlags)) { result.addAll(undefinedList); // Maybe add one for the other profile. if (xpDomainInfo != null && xpDomainInfo.highestApprovalLevel @@ -25693,6 +25697,7 @@ public class PackageManagerService extends IPackageManager.Stub if (!convertedFromPreCreated || !readPermissionStateForUser(userId)) { mPermissionManager.onUserCreated(userId); mLegacyPermissionManager.grantDefaultPermissions(userId); + mDomainVerificationManager.clearUser(userId); } } diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationDebug.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationDebug.java index b61fd8d633f6..39ed4882c69c 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationDebug.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationDebug.java @@ -21,7 +21,6 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; -import android.content.pm.verify.domain.DomainVerificationManager; import android.content.pm.verify.domain.DomainVerificationState; import android.os.UserHandle; import android.util.ArrayMap; @@ -151,7 +150,7 @@ public class DomainVerificationDebug { Integer state = reusedMap.valueAt(stateIndex); writer.print(domain); writer.print(": "); - writer.println(DomainVerificationManager.stateToDebugString(state)); + writer.println(DomainVerificationState.stateToDebugString(state)); } writer.decreaseIndent(); writer.decreaseIndent(); diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationEnforcer.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationEnforcer.java index 1721a18f4f60..f4bcd3e65913 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationEnforcer.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationEnforcer.java @@ -141,6 +141,12 @@ public class DomainVerificationEnforcer { "Caller is not allowed to edit other users"); } + if (!mCallback.doesUserExist(callingUserId)) { + throw new SecurityException("User " + callingUserId + " does not exist"); + } else if (!mCallback.doesUserExist(targetUserId)) { + throw new SecurityException("User " + targetUserId + " does not exist"); + } + return !mCallback.filterAppAccess(packageName, callingUid, targetUserId); } @@ -161,6 +167,12 @@ public class DomainVerificationEnforcer { Binder.getCallingPid(), callingUid, "Caller is not allowed to edit user selections"); + if (!mCallback.doesUserExist(callingUserId)) { + throw new SecurityException("User " + callingUserId + " does not exist"); + } else if (!mCallback.doesUserExist(targetUserId)) { + throw new SecurityException("User " + targetUserId + " does not exist"); + } + if (packageName == null) { return true; } @@ -184,6 +196,12 @@ public class DomainVerificationEnforcer { } } + if (!mCallback.doesUserExist(callingUserId)) { + throw new SecurityException("User " + callingUserId + " does not exist"); + } else if (!mCallback.doesUserExist(targetUserId)) { + throw new SecurityException("User " + targetUserId + " does not exist"); + } + return !mCallback.filterAppAccess(packageName, callingUid, targetUserId); } @@ -197,6 +215,12 @@ public class DomainVerificationEnforcer { "Caller is not allowed to edit other users"); } + if (!mCallback.doesUserExist(callingUserId)) { + throw new SecurityException("User " + callingUserId + " does not exist"); + } else if (!mCallback.doesUserExist(targetUserId)) { + throw new SecurityException("User " + targetUserId + " does not exist"); + } + return !mCallback.filterAppAccess(packageName, callingUid, targetUserId); } @@ -221,6 +245,12 @@ public class DomainVerificationEnforcer { mContext.enforcePermission( android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION, callingPid, callingUid, "Caller is not allowed to query user selections"); + + if (!mCallback.doesUserExist(callingUserId)) { + throw new SecurityException("User " + callingUserId + " does not exist"); + } else if (!mCallback.doesUserExist(targetUserId)) { + throw new SecurityException("User " + targetUserId + " does not exist"); + } } public interface Callback { @@ -229,5 +259,7 @@ public class DomainVerificationEnforcer { * if the package was not installed */ boolean filterAppAccess(@NonNull String packageName, int callingUid, @UserIdInt int userId); + + boolean doesUserExist(@UserIdInt int userId); } } diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java index b4bcde726173..73182732cdaf 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java @@ -333,10 +333,10 @@ public interface DomainVerificationManagerInternal { @Nullable UUID getDomainVerificationInfoId(@NonNull String packageName); + @DomainVerificationManager.Error @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) - void setDomainVerificationStatusInternal(int callingUid, @NonNull UUID domainSetId, - @NonNull Set<String> domains, int state) - throws IllegalArgumentException, NameNotFoundException; + int setDomainVerificationStatusInternal(int callingUid, @NonNull UUID domainSetId, + @NonNull Set<String> domains, int state) throws NameNotFoundException; interface Connection extends DomainVerificationEnforcer.Callback { diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java index a7a52e0cd10c..2a17c6d4cec5 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java @@ -24,7 +24,6 @@ import android.content.pm.verify.domain.DomainOwner; import android.content.pm.verify.domain.DomainSet; import android.content.pm.verify.domain.DomainVerificationInfo; import android.content.pm.verify.domain.DomainVerificationManager; -import android.content.pm.verify.domain.DomainVerificationManager.InvalidDomainSetException; import android.content.pm.verify.domain.DomainVerificationUserState; import android.content.pm.verify.domain.IDomainVerificationManager; import android.os.ServiceSpecificException; @@ -61,11 +60,12 @@ public class DomainVerificationManagerStub extends IDomainVerificationManager.St } } + @DomainVerificationManager.Error @Override - public void setDomainVerificationStatus(String domainSetId, @NonNull DomainSet domainSet, + public int setDomainVerificationStatus(String domainSetId, @NonNull DomainSet domainSet, int state) { try { - mService.setDomainVerificationStatus(UUID.fromString(domainSetId), + return mService.setDomainVerificationStatus(UUID.fromString(domainSetId), domainSet.getDomains(), state); } catch (Exception e) { throw rethrow(e); @@ -82,11 +82,12 @@ public class DomainVerificationManagerStub extends IDomainVerificationManager.St } } + @DomainVerificationManager.Error @Override - public void setDomainVerificationUserSelection(String domainSetId, @NonNull DomainSet domainSet, + public int setDomainVerificationUserSelection(String domainSetId, @NonNull DomainSet domainSet, boolean enabled, @UserIdInt int userId) { try { - mService.setDomainVerificationUserSelection(UUID.fromString(domainSetId), + return mService.setDomainVerificationUserSelection(UUID.fromString(domainSetId), domainSet.getDomains(), enabled, userId); } catch (Exception e) { throw rethrow(e); @@ -116,14 +117,9 @@ public class DomainVerificationManagerStub extends IDomainVerificationManager.St } private RuntimeException rethrow(Exception exception) throws RuntimeException { - if (exception instanceof InvalidDomainSetException) { - int packedErrorCode = DomainVerificationManager.ERROR_INVALID_DOMAIN_SET; - packedErrorCode |= ((InvalidDomainSetException) exception).getReason() << 16; - return new ServiceSpecificException(packedErrorCode, - ((InvalidDomainSetException) exception).getPackageName()); - } else if (exception instanceof NameNotFoundException) { + if (exception instanceof NameNotFoundException) { return new ServiceSpecificException( - DomainVerificationManager.ERROR_NAME_NOT_FOUND); + DomainVerificationManager.INTERNAL_ERROR_NAME_NOT_FOUND); } else if (exception instanceof RuntimeException) { return (RuntimeException) exception; } else { diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java index e85bbe41f747..a0e252a8a28a 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java @@ -19,6 +19,7 @@ package com.android.server.pm.verify.domain; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; +import android.annotation.CheckResult; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -34,7 +35,6 @@ import android.content.pm.parsing.component.ParsedActivity; import android.content.pm.verify.domain.DomainOwner; import android.content.pm.verify.domain.DomainVerificationInfo; import android.content.pm.verify.domain.DomainVerificationManager; -import android.content.pm.verify.domain.DomainVerificationManager.InvalidDomainSetException; import android.content.pm.verify.domain.DomainVerificationState; import android.content.pm.verify.domain.DomainVerificationUserState; import android.content.pm.verify.domain.IDomainVerificationManager; @@ -243,17 +243,17 @@ public class DomainVerificationService extends SystemService throws NameNotFoundException { mEnforcer.assertApprovedQuerent(mConnection.getCallingUid(), mProxy); synchronized (mLock) { - DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName); - if (pkgState == null) { - return null; - } - AndroidPackage pkg = mConnection.getPackageLocked(packageName); if (pkg == null) { throw DomainVerificationUtils.throwPackageUnavailable(packageName); } - Map<String, Integer> hostToStateMap = new ArrayMap<>(pkgState.getStateMap()); + DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName); + if (pkgState == null) { + return null; + } + + ArrayMap<String, Integer> hostToStateMap = new ArrayMap<>(pkgState.getStateMap()); // TODO(b/159952358): Should the domain list be cached? ArraySet<String> domains = mCollector.collectValidAutoVerifyDomains(pkg); @@ -267,44 +267,56 @@ public class DomainVerificationService extends SystemService DomainVerificationState.STATE_NO_RESPONSE); } + final int mapSize = hostToStateMap.size(); + for (int index = 0; index < mapSize; index++) { + int internalValue = hostToStateMap.valueAt(index); + int publicValue = DomainVerificationState.convertToInfoState(internalValue); + hostToStateMap.setValueAt(index, publicValue); + } + // TODO(b/159952358): Do not return if no values are editable (all ignored states)? return new DomainVerificationInfo(pkgState.getId(), packageName, hostToStateMap); } } - public void setDomainVerificationStatus(@NonNull UUID domainSetId, @NonNull Set<String> domains, - int state) throws InvalidDomainSetException, NameNotFoundException { + @DomainVerificationManager.Error + public int setDomainVerificationStatus(@NonNull UUID domainSetId, @NonNull Set<String> domains, + int state) throws NameNotFoundException { if (state < DomainVerificationState.STATE_FIRST_VERIFIER_DEFINED) { if (state != DomainVerificationState.STATE_SUCCESS) { - throw new IllegalArgumentException( - "Verifier can only set STATE_SUCCESS or codes greater than or equal to " - + "STATE_FIRST_VERIFIER_DEFINED"); + return DomainVerificationManager.ERROR_INVALID_STATE_CODE; } } - setDomainVerificationStatusInternal(mConnection.getCallingUid(), domainSetId, domains, - state); + return setDomainVerificationStatusInternal(mConnection.getCallingUid(), domainSetId, + domains, state); } + @DomainVerificationManager.Error @Override - public void setDomainVerificationStatusInternal(int callingUid, @NonNull UUID domainSetId, + public int setDomainVerificationStatusInternal(int callingUid, @NonNull UUID domainSetId, @NonNull Set<String> domains, int state) - throws InvalidDomainSetException, NameNotFoundException { + throws NameNotFoundException { mEnforcer.assertApprovedVerifier(callingUid, mProxy); synchronized (mLock) { List<String> verifiedDomains = new ArrayList<>(); - DomainVerificationPkgState pkgState = getAndValidateAttachedLocked(domainSetId, domains, + GetAttachedResult result = getAndValidateAttachedLocked(domainSetId, domains, true /* forAutoVerify */, callingUid, null /* userId */); + if (result.isError()) { + return result.getErrorCode(); + } + + DomainVerificationPkgState pkgState = result.getPkgState(); ArrayMap<String, Integer> stateMap = pkgState.getStateMap(); for (String domain : domains) { Integer previousState = stateMap.get(domain); if (previousState != null - && !DomainVerificationManager.isStateModifiable(previousState)) { + && !DomainVerificationState.isModifiable(previousState)) { continue; } - if (DomainVerificationManager.isStateVerified(state)) { + if (DomainVerificationState.isVerified(state)) { verifiedDomains.add(domain); } @@ -318,6 +330,7 @@ public class DomainVerificationService extends SystemService } mConnection.scheduleWriteSettings(); + return DomainVerificationManager.STATUS_OK; } @Override @@ -460,17 +473,24 @@ public class DomainVerificationService extends SystemService throw DomainVerificationUtils.throwPackageUnavailable(packageName); } - pkgState.getOrCreateUserState(userId) - .setLinkHandlingAllowed(allowed); + if (userId == UserHandle.USER_ALL) { + for (int aUserId : mConnection.getAllUserIds()) { + pkgState.getOrCreateUserState(aUserId) + .setLinkHandlingAllowed(allowed); + } + } else { + pkgState.getOrCreateUserState(userId) + .setLinkHandlingAllowed(allowed); + } } } mConnection.scheduleWriteSettings(); } - public void setDomainVerificationUserSelection(@NonNull UUID domainSetId, + public int setDomainVerificationUserSelection(@NonNull UUID domainSetId, @NonNull Set<String> domains, boolean enabled, @UserIdInt int userId) - throws InvalidDomainSetException, NameNotFoundException { + throws NameNotFoundException { synchronized (mLock) { final int callingUid = mConnection.getCallingUid(); // Pass null for package name here and do the app visibility enforcement inside @@ -478,14 +498,17 @@ public class DomainVerificationService extends SystemService // ID reason if the target app is invisible if (!mEnforcer.assertApprovedUserSelector(callingUid, mConnection.getCallingUserId(), null /* packageName */, userId)) { - throw new InvalidDomainSetException(domainSetId, null, - InvalidDomainSetException.REASON_ID_INVALID); + return DomainVerificationManager.ERROR_DOMAIN_SET_ID_INVALID; } - DomainVerificationPkgState pkgState = getAndValidateAttachedLocked(domainSetId, domains, + GetAttachedResult result = getAndValidateAttachedLocked(domainSetId, domains, false /* forAutoVerify */, callingUid, userId); - DomainVerificationInternalUserState userState = - pkgState.getOrCreateUserState(userId); + if (result.isError()) { + return result.getErrorCode(); + } + + DomainVerificationPkgState pkgState = result.getPkgState(); + DomainVerificationInternalUserState userState = pkgState.getOrCreateUserState(userId); // Disable other packages if approving this one. Note that this check is only done for // enabling. This allows an escape hatch in case multiple packages somehow get selected. @@ -503,8 +526,7 @@ public class DomainVerificationService extends SystemService userId, APPROVAL_LEVEL_NONE + 1, mConnection::getPackageSettingLocked); int highestApproval = packagesToLevel.second; if (highestApproval > APPROVAL_LEVEL_SELECTION) { - throw new InvalidDomainSetException(domainSetId, null, - InvalidDomainSetException.REASON_UNABLE_TO_APPROVE); + return DomainVerificationManager.ERROR_UNABLE_TO_APPROVE; } domainToApprovedPackages.put(domain, packagesToLevel.first); @@ -544,6 +566,7 @@ public class DomainVerificationService extends SystemService } mConnection.scheduleWriteSettings(); + return DomainVerificationManager.STATUS_OK; } @Override @@ -636,16 +659,16 @@ public class DomainVerificationService extends SystemService throw DomainVerificationUtils.throwPackageUnavailable(packageName); } synchronized (mLock) { - DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName); - if (pkgState == null) { - return null; - } - AndroidPackage pkg = mConnection.getPackageLocked(packageName); if (pkg == null) { throw DomainVerificationUtils.throwPackageUnavailable(packageName); } + DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName); + if (pkgState == null) { + return null; + } + ArraySet<String> webDomains = mCollector.collectAllWebDomains(pkg); int webDomainsSize = webDomains.size(); @@ -659,7 +682,7 @@ public class DomainVerificationService extends SystemService Integer state = stateMap.get(host); int domainState; - if (state != null && DomainVerificationManager.isStateVerified(state)) { + if (state != null && DomainVerificationState.isVerified(state)) { domainState = DomainVerificationUserState.DOMAIN_STATE_VERIFIED; } else if (enabledHosts.contains(host)) { domainState = DomainVerificationUserState.DOMAIN_STATE_SELECTED; @@ -793,19 +816,12 @@ public class DomainVerificationService extends SystemService Integer oldStateInteger = oldStateMap.get(domain); if (oldStateInteger != null) { int oldState = oldStateInteger; - switch (oldState) { - case DomainVerificationState.STATE_SUCCESS: - case DomainVerificationState.STATE_RESTORED: - case DomainVerificationState.STATE_MIGRATED: - newStateMap.put(domain, oldState); - break; - default: - // In all other cases, the state code is left unset - // (STATE_NO_RESPONSE) to signal to the verification agent that any - // existing error has been cleared and the domain should be - // re-attempted. This makes update of a package a signal to - // re-verify. - break; + // If the following case fails, the state code is left unset + // (STATE_NO_RESPONSE) to signal to the verification agent that any existing + // error has been cleared and the domain should be re-attempted. This makes + // update of a package a signal to re-verify. + if (DomainVerificationState.shouldMigrate(oldState)) { + newStateMap.put(domain, oldState); } } } @@ -858,13 +874,13 @@ public class DomainVerificationService extends SystemService boolean sendBroadcast = true; DomainVerificationPkgState pkgState; - pkgState = mSettings.getPendingState(pkgName); + pkgState = mSettings.removePendingState(pkgName); if (pkgState != null) { // Don't send when attaching from pending read, which is usually boot scan. Re-send on // boot is handled in a separate method once all packages are added. sendBroadcast = false; } else { - pkgState = mSettings.getRestoredState(pkgName); + pkgState = mSettings.removeRestoredState(pkgName); } AndroidPackage pkg = newPkgSetting.getPkg(); @@ -872,7 +888,7 @@ public class DomainVerificationService extends SystemService boolean hasAutoVerifyDomains = !domains.isEmpty(); boolean isPendingOrRestored = pkgState != null; if (isPendingOrRestored) { - pkgState.setId(domainSetId); + pkgState = new DomainVerificationPkgState(pkgState, domainSetId, hasAutoVerifyDomains); } else { pkgState = new DomainVerificationPkgState(pkgName, domainSetId, hasAutoVerifyDomains); } @@ -1097,28 +1113,25 @@ public class DomainVerificationService extends SystemService * @param userIdForFilter which user to filter app access to, or null if the caller has already * validated package visibility */ + @CheckResult @GuardedBy("mLock") - private DomainVerificationPkgState getAndValidateAttachedLocked(@NonNull UUID domainSetId, + private GetAttachedResult getAndValidateAttachedLocked(@NonNull UUID domainSetId, @NonNull Set<String> domains, boolean forAutoVerify, int callingUid, - @Nullable Integer userIdForFilter) - throws InvalidDomainSetException, NameNotFoundException { + @Nullable Integer userIdForFilter) throws NameNotFoundException { if (domainSetId == null) { - throw new InvalidDomainSetException(null, null, - InvalidDomainSetException.REASON_ID_NULL); + return GetAttachedResult.error(DomainVerificationManager.ERROR_DOMAIN_SET_ID_NULL); } DomainVerificationPkgState pkgState = mAttachedPkgStates.get(domainSetId); if (pkgState == null) { - throw new InvalidDomainSetException(domainSetId, null, - InvalidDomainSetException.REASON_ID_INVALID); + return GetAttachedResult.error(DomainVerificationManager.ERROR_DOMAIN_SET_ID_INVALID); } String pkgName = pkgState.getPackageName(); if (userIdForFilter != null && mConnection.filterAppAccess(pkgName, callingUid, userIdForFilter)) { - throw new InvalidDomainSetException(domainSetId, null, - InvalidDomainSetException.REASON_ID_INVALID); + return GetAttachedResult.error(DomainVerificationManager.ERROR_DOMAIN_SET_ID_INVALID); } PackageSetting pkgSetting = mConnection.getPackageSettingLocked(pkgName); @@ -1127,8 +1140,8 @@ public class DomainVerificationService extends SystemService } if (CollectionUtils.isEmpty(domains)) { - throw new InvalidDomainSetException(domainSetId, pkgState.getPackageName(), - InvalidDomainSetException.REASON_SET_NULL_OR_EMPTY); + return GetAttachedResult.error( + DomainVerificationManager.ERROR_DOMAIN_SET_NULL_OR_EMPTY); } AndroidPackage pkg = pkgSetting.getPkg(); ArraySet<String> declaredDomains = forAutoVerify @@ -1136,11 +1149,10 @@ public class DomainVerificationService extends SystemService : mCollector.collectAllWebDomains(pkg); if (domains.retainAll(declaredDomains)) { - throw new InvalidDomainSetException(domainSetId, pkgState.getPackageName(), - InvalidDomainSetException.REASON_UNKNOWN_DOMAIN); + return GetAttachedResult.error(DomainVerificationManager.ERROR_UNKNOWN_DOMAIN); } - return pkgState; + return GetAttachedResult.success(pkgState); } @Override @@ -1185,7 +1197,7 @@ public class DomainVerificationService extends SystemService /** * Determine whether or not a broadcast should be sent at boot for the given {@param pkgState}. * Sends only if the only states recorded are default as decided by {@link - * DomainVerificationManager#isStateDefault(int)}. + * DomainVerificationState#isDefault(int)}. * * If any other state is set, it's assumed that the domain verification agent is aware of the * package and has already scheduled future verification requests. @@ -1199,7 +1211,7 @@ public class DomainVerificationService extends SystemService int statesSize = stateMap.size(); for (int stateIndex = 0; stateIndex < statesSize; stateIndex++) { Integer state = stateMap.valueAt(stateIndex); - if (!DomainVerificationManager.isStateDefault(state)) { + if (!DomainVerificationState.isDefault(state)) { return false; } } @@ -1318,83 +1330,50 @@ public class DomainVerificationService extends SystemService @NonNull Function<String, PackageSetting> pkgSettingFunction) { String domain = intent.getData().getHost(); - // Collect package names - ArrayMap<String, Integer> packageApprovals = new ArrayMap<>(); + // Collect valid infos + ArrayMap<ResolveInfo, Integer> infoApprovals = new ArrayMap<>(); int infosSize = infos.size(); for (int index = 0; index < infosSize; index++) { - packageApprovals.put(infos.get(index).getComponentInfo().packageName, - APPROVAL_LEVEL_NONE); + final ResolveInfo info = infos.get(index); + // Only collect for intent filters that can auto resolve + if (info.isAutoResolutionAllowed()) { + infoApprovals.put(info, null); + } } // Find all approval levels - int highestApproval = fillMapWithApprovalLevels(packageApprovals, domain, userId, + int highestApproval = fillMapWithApprovalLevels(infoApprovals, domain, userId, pkgSettingFunction); if (highestApproval == APPROVAL_LEVEL_NONE) { return Pair.create(emptyList(), highestApproval); } - // Filter to highest, non-zero packages - ArraySet<String> approvedPackages = new ArraySet<>(); - int approvalsSize = packageApprovals.size(); - for (int index = 0; index < approvalsSize; index++) { - if (packageApprovals.valueAt(index) == highestApproval) { - approvedPackages.add(packageApprovals.keyAt(index)); + // Filter to highest, non-zero infos + for (int index = infoApprovals.size() - 1; index >= 0; index--) { + if (infoApprovals.valueAt(index) != highestApproval) { + infoApprovals.removeAt(index); } } - ArraySet<String> filteredPackages = new ArraySet<>(); - if (highestApproval == APPROVAL_LEVEL_LEGACY_ASK) { + if (highestApproval != APPROVAL_LEVEL_LEGACY_ASK) { // To maintain legacy behavior while the Settings API is not implemented, // show the chooser if all approved apps are marked ask, skipping the // last app, last declaration filtering. - filteredPackages.addAll(approvedPackages); - } else { - // Filter to last installed package - long latestInstall = Long.MIN_VALUE; - int approvedSize = approvedPackages.size(); - for (int index = 0; index < approvedSize; index++) { - String packageName = approvedPackages.valueAt(index); - PackageSetting pkgSetting = pkgSettingFunction.apply(packageName); - if (pkgSetting == null) { - continue; - } - long installTime = pkgSetting.getFirstInstallTime(); - if (installTime > latestInstall) { - latestInstall = installTime; - filteredPackages.clear(); - filteredPackages.add(packageName); - } else if (installTime == latestInstall) { - filteredPackages.add(packageName); - } - } + filterToLastFirstInstalled(infoApprovals, pkgSettingFunction); } - // Filter to approved ResolveInfos - ArrayMap<String, List<ResolveInfo>> approvedInfos = new ArrayMap<>(); - for (int index = 0; index < infosSize; index++) { - ResolveInfo info = infos.get(index); - String packageName = info.getComponentInfo().packageName; - if (filteredPackages.contains(packageName)) { - List<ResolveInfo> infosPerPackage = approvedInfos.get(packageName); - if (infosPerPackage == null) { - infosPerPackage = new ArrayList<>(); - approvedInfos.put(packageName, infosPerPackage); - } - infosPerPackage.add(info); - } + // Easier to transform into list as the filterToLastDeclared method + // requires swapping indexes, which doesn't work with ArrayMap keys + final int size = infoApprovals.size(); + List<ResolveInfo> finalList = new ArrayList<>(size); + for (int index = 0; index < size; index++) { + finalList.add(infoApprovals.keyAt(index)); } - List<ResolveInfo> finalList; - if (highestApproval == APPROVAL_LEVEL_LEGACY_ASK) { - // If legacy ask, skip the last declaration filtering - finalList = new ArrayList<>(); - int size = approvedInfos.size(); - for (int index = 0; index < size; index++) { - finalList.addAll(approvedInfos.valueAt(index)); - } - } else { + // If legacy ask, skip the last declaration filtering + if (highestApproval != APPROVAL_LEVEL_LEGACY_ASK) { // Find the last declared ResolveInfo per package - finalList = filterToLastDeclared(approvedInfos, pkgSettingFunction); + filterToLastDeclared(finalList, pkgSettingFunction); } return Pair.create(finalList, highestApproval); @@ -1403,68 +1382,127 @@ public class DomainVerificationService extends SystemService /** * @return highest approval level found */ - private int fillMapWithApprovalLevels(@NonNull ArrayMap<String, Integer> inputMap, + @ApprovalLevel + private int fillMapWithApprovalLevels(@NonNull ArrayMap<ResolveInfo, Integer> inputMap, @NonNull String domain, @UserIdInt int userId, @NonNull Function<String, PackageSetting> pkgSettingFunction) { int highestApproval = APPROVAL_LEVEL_NONE; int size = inputMap.size(); for (int index = 0; index < size; index++) { - String packageName = inputMap.keyAt(index); + if (inputMap.valueAt(index) != null) { + // Already filled by previous iteration + continue; + } + + ResolveInfo info = inputMap.keyAt(index); + final String packageName = info.getComponentInfo().packageName; PackageSetting pkgSetting = pkgSettingFunction.apply(packageName); if (pkgSetting == null) { - inputMap.setValueAt(index, APPROVAL_LEVEL_NONE); + fillInfoMapForSamePackage(inputMap, packageName, APPROVAL_LEVEL_NONE); continue; } int approval = approvalLevelForDomain(pkgSetting, domain, userId, domain); highestApproval = Math.max(highestApproval, approval); - inputMap.setValueAt(index, approval); + fillInfoMapForSamePackage(inputMap, packageName, approval); } return highestApproval; } + private void fillInfoMapForSamePackage(@NonNull ArrayMap<ResolveInfo, Integer> inputMap, + @NonNull String targetPackageName, @ApprovalLevel int level) { + final int size = inputMap.size(); + for (int index = 0; index < size; index++) { + final String packageName = inputMap.keyAt(index).getComponentInfo().packageName; + if (Objects.equals(targetPackageName, packageName)) { + inputMap.setValueAt(index, level); + } + } + } + @NonNull - private List<ResolveInfo> filterToLastDeclared( - @NonNull ArrayMap<String, List<ResolveInfo>> inputMap, + private void filterToLastFirstInstalled(@NonNull ArrayMap<ResolveInfo, Integer> inputMap, @NonNull Function<String, PackageSetting> pkgSettingFunction) { - List<ResolveInfo> finalList = new ArrayList<>(inputMap.size()); - - int inputSize = inputMap.size(); - for (int inputIndex = 0; inputIndex < inputSize; inputIndex++) { - String packageName = inputMap.keyAt(inputIndex); - List<ResolveInfo> infos = inputMap.valueAt(inputIndex); + // First, find the package with the latest first install time + String targetPackageName = null; + long latestInstall = Long.MIN_VALUE; + final int size = inputMap.size(); + for (int index = 0; index < size; index++) { + ResolveInfo info = inputMap.keyAt(index); + String packageName = info.getComponentInfo().packageName; PackageSetting pkgSetting = pkgSettingFunction.apply(packageName); + if (pkgSetting == null) { + continue; + } + + long installTime = pkgSetting.getFirstInstallTime(); + if (installTime > latestInstall) { + latestInstall = installTime; + targetPackageName = packageName; + } + } + + // Then, remove all infos that don't match the package + for (int index = inputMap.size() - 1; index >= 0; index--) { + ResolveInfo info = inputMap.keyAt(index); + if (!Objects.equals(targetPackageName, info.getComponentInfo().packageName)) { + inputMap.removeAt(index); + } + } + } + + @NonNull + private void filterToLastDeclared(@NonNull List<ResolveInfo> inputList, + @NonNull Function<String, PackageSetting> pkgSettingFunction) { + // Must call size each time as the size of the list will decrease + for (int index = 0; index < inputList.size(); index++) { + ResolveInfo info = inputList.get(index); + String targetPackageName = info.getComponentInfo().packageName; + PackageSetting pkgSetting = pkgSettingFunction.apply(targetPackageName); AndroidPackage pkg = pkgSetting == null ? null : pkgSetting.getPkg(); if (pkg == null) { continue; } - ResolveInfo result = null; - int highestIndex = -1; - int infosSize = infos.size(); - for (int infoIndex = 0; infoIndex < infosSize; infoIndex++) { - ResolveInfo info = infos.get(infoIndex); - List<ParsedActivity> activities = pkg.getActivities(); - int activitiesSize = activities.size(); - for (int activityIndex = 0; activityIndex < activitiesSize; activityIndex++) { - if (Objects.equals(activities.get(activityIndex).getComponentName(), - info.getComponentInfo().getComponentName())) { - if (activityIndex > highestIndex) { - highestIndex = activityIndex; - result = info; - } - break; - } + ResolveInfo result = info; + int highestIndex = indexOfIntentFilterEntry(pkg, result); + + // Search backwards so that lower results can be removed as they're found + for (int searchIndex = inputList.size() - 1; searchIndex >= index + 1; searchIndex--) { + ResolveInfo searchInfo = inputList.get(searchIndex); + if (!Objects.equals(targetPackageName, searchInfo.getComponentInfo().packageName)) { + continue; } + + int entryIndex = indexOfIntentFilterEntry(pkg, searchInfo); + if (entryIndex > highestIndex) { + highestIndex = entryIndex; + result = searchInfo; + } + + // Always remove the entry so that the current index + // is left as the sole candidate of the target package + inputList.remove(searchIndex); } - // Shouldn't be null, but might as well be safe - if (result != null) { - finalList.add(result); + // Swap the current index for the result, leaving this as + // the only entry with the target package name + inputList.set(index, result); + } + } + + private int indexOfIntentFilterEntry(@NonNull AndroidPackage pkg, + @NonNull ResolveInfo target) { + List<ParsedActivity> activities = pkg.getActivities(); + int activitiesSize = activities.size(); + for (int activityIndex = 0; activityIndex < activitiesSize; activityIndex++) { + if (Objects.equals(activities.get(activityIndex).getComponentName(), + target.getComponentInfo().getComponentName())) { + return activityIndex; } } - return finalList; + return -1; } @Override @@ -1472,8 +1510,7 @@ public class DomainVerificationService extends SystemService @NonNull List<ResolveInfo> candidates, @PackageManager.ResolveInfoFlags int resolveInfoFlags, @UserIdInt int userId) { String packageName = pkgSetting.getName(); - if (!DomainVerificationUtils.isDomainVerificationIntent(intent, candidates, - resolveInfoFlags)) { + if (!DomainVerificationUtils.isDomainVerificationIntent(intent, resolveInfoFlags)) { if (DEBUG_APPROVAL) { debugApproval(packageName, intent, userId, false, "not valid intent"); } @@ -1542,7 +1579,7 @@ public class DomainVerificationService extends SystemService ArrayMap<String, Integer> stateMap = pkgState.getStateMap(); // Check if the exact host matches Integer state = stateMap.get(host); - if (state != null && DomainVerificationManager.isStateVerified(state)) { + if (state != null && DomainVerificationState.isVerified(state)) { if (DEBUG_APPROVAL) { debugApproval(packageName, debugObject, userId, true, "host verified exactly"); @@ -1553,7 +1590,7 @@ public class DomainVerificationService extends SystemService // Otherwise see if the host matches a verified domain by wildcard int stateMapSize = stateMap.size(); for (int index = 0; index < stateMapSize; index++) { - if (!DomainVerificationManager.isStateVerified(stateMap.valueAt(index))) { + if (!DomainVerificationState.isVerified(stateMap.valueAt(index))) { continue; } @@ -1672,4 +1709,40 @@ public class DomainVerificationService extends SystemService Slog.d(TAG + "Approval", packageName + " was " + approvalString + " for " + debugObject + " for user " + userId + ": " + reason); } + + private static class GetAttachedResult { + + @Nullable + private DomainVerificationPkgState mPkgState; + + private int mErrorCode; + + GetAttachedResult(@Nullable DomainVerificationPkgState pkgState, int errorCode) { + mPkgState = pkgState; + mErrorCode = errorCode; + } + + @NonNull + static GetAttachedResult error(@DomainVerificationManager.Error int errorCode) { + return new GetAttachedResult(null, errorCode); + } + + @NonNull + static GetAttachedResult success(@NonNull DomainVerificationPkgState pkgState) { + return new GetAttachedResult(pkgState, DomainVerificationManager.STATUS_OK); + } + + @NonNull + DomainVerificationPkgState getPkgState() { + return mPkgState; + } + + boolean isError() { + return mErrorCode != DomainVerificationManager.STATUS_OK; + } + + public int getErrorCode() { + return mErrorCode; + } + } } diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java index f3d1dbb1f6ad..8b59da7bb944 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java @@ -256,16 +256,16 @@ class DomainVerificationSettings { } @Nullable - public DomainVerificationPkgState getPendingState(@NonNull String pkgName) { + public DomainVerificationPkgState removePendingState(@NonNull String pkgName) { synchronized (mLock) { - return mPendingPkgStates.get(pkgName); + return mPendingPkgStates.remove(pkgName); } } @Nullable - public DomainVerificationPkgState getRestoredState(@NonNull String pkgName) { + public DomainVerificationPkgState removeRestoredState(@NonNull String pkgName) { synchronized (mLock) { - return mRestoredPkgStates.get(pkgName); + return mRestoredPkgStates.remove(pkgName); } } } diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java index 94767f555574..7e755fa384db 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java @@ -38,6 +38,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; +import java.util.function.Function; public class DomainVerificationShell { @@ -226,22 +228,19 @@ public class DomainVerificationShell { userId = translateUserId(userId, "runSetAppLinksUserState"); - String enabledString = commandHandler.getNextArgRequired(); + String enabledArg = commandHandler.getNextArg(); + if (TextUtils.isEmpty(enabledArg)) { + commandHandler.getErrPrintWriter().println("Error: enabled param not specified"); + return false; + } - // Manually ensure that "true" and "false" are the only options, to ensure a domain isn't - // accidentally parsed as a boolean boolean enabled; - switch (enabledString) { - case "true": - enabled = true; - break; - case "false": - enabled = false; - break; - default: - commandHandler.getErrPrintWriter().println( - "Invalid enabled param: " + enabledString); - return false; + try { + enabled = parseEnabled(enabledArg); + } catch (IllegalArgumentException e) { + commandHandler.getErrPrintWriter() + .println("Error: invalid enabled param: " + e.getMessage()); + return false; } ArraySet<String> domains = new ArraySet<>(getRemainingArgs(commandHandler)); @@ -255,8 +254,8 @@ public class DomainVerificationShell { } try { - mCallback.setDomainVerificationUserSelectionInternal(userId, - packageName, enabled, domains); + mCallback.setDomainVerificationUserSelectionInternal(userId, packageName, enabled, + domains); } catch (NameNotFoundException e) { commandHandler.getErrPrintWriter().println("Package not found: " + packageName); return false; @@ -362,15 +361,12 @@ public class DomainVerificationShell { private boolean runSetAppLinksAllowed(@NonNull BasicShellCommandHandler commandHandler) { String packageName = null; Integer userId = null; - Boolean allowed = null; String option; while ((option = commandHandler.getNextOption()) != null) { if (option.equals("--package")) { - packageName = commandHandler.getNextArgRequired(); - } if (option.equals("--user")) { + packageName = commandHandler.getNextArg(); + } else if (option.equals("--user")) { userId = UserHandle.parseUserArg(commandHandler.getNextArgRequired()); - } else if (allowed == null) { - allowed = Boolean.valueOf(option); } else { commandHandler.getErrPrintWriter().println("Error: unexpected option: " + option); return false; @@ -389,11 +385,21 @@ public class DomainVerificationShell { return false; } - if (allowed == null) { + String allowedArg = commandHandler.getNextArg(); + if (TextUtils.isEmpty(allowedArg)) { commandHandler.getErrPrintWriter().println("Error: allowed setting not specified"); return false; } + boolean allowed; + try { + allowed = parseEnabled(allowedArg); + } catch (IllegalArgumentException e) { + commandHandler.getErrPrintWriter() + .println("Error: invalid allowed setting: " + e.getMessage()); + return false; + } + userId = translateUserId(userId, "runSetAppLinksAllowed"); try { @@ -422,6 +428,22 @@ public class DomainVerificationShell { } /** + * Manually ensure that "true" and "false" are the only options, to ensure a domain isn't + * accidentally parsed as a boolean. + */ + @NonNull + private boolean parseEnabled(@NonNull String arg) throws IllegalArgumentException { + switch (arg.toLowerCase(Locale.US)) { + case "true": + return true; + case "false": + return false; + default: + throw new IllegalArgumentException(arg + " is not a valid boolean"); + } + } + + /** * Separated interface from {@link DomainVerificationManagerInternal} to hide methods that are * even more internal, and so that testing is easier. */ @@ -498,7 +520,8 @@ public class DomainVerificationShell { void verifyPackages(@Nullable List<String> packageNames, boolean reVerify); /** - * @see DomainVerificationManagerInternal#printState(IndentingPrintWriter, String, Integer) + * @see DomainVerificationManagerInternal#printState(IndentingPrintWriter, String, Integer, + * Function) */ void printState(@NonNull IndentingPrintWriter writer, @Nullable String packageName, @Nullable @UserIdInt Integer userId) throws NameNotFoundException; diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java index 883bbad1bd2d..cb3b5c9db7e7 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java @@ -22,7 +22,6 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; import android.os.Binder; import com.android.internal.util.CollectionUtils; @@ -30,7 +29,6 @@ import com.android.server.compat.PlatformCompat; import com.android.server.pm.PackageManagerService; import com.android.server.pm.parsing.pkg.AndroidPackage; -import java.util.List; import java.util.Set; public final class DomainVerificationUtils { @@ -46,7 +44,6 @@ public final class DomainVerificationUtils { } public static boolean isDomainVerificationIntent(Intent intent, - @NonNull List<ResolveInfo> candidates, @PackageManager.ResolveInfoFlags int resolveInfoFlags) { if (!intent.isWebIntent()) { return false; @@ -63,42 +60,18 @@ public final class DomainVerificationUtils { && intent.hasCategory(Intent.CATEGORY_BROWSABLE); } - // In cases where at least one browser is resolved and only one non-browser is resolved, - // the Intent is coerced into an app links intent, under the assumption the browser can - // be skipped if the app is approved at any level for the domain. - boolean foundBrowser = false; - boolean foundOneApp = false; - - final int candidatesSize = candidates.size(); - for (int index = 0; index < candidatesSize; index++) { - final ResolveInfo info = candidates.get(index); - if (info.handleAllWebDataURI) { - foundBrowser = true; - } else if (foundOneApp) { - // Already true, so duplicate app - foundOneApp = false; - break; - } else { - foundOneApp = true; - } - } - boolean matchDefaultByFlags = (resolveInfoFlags & PackageManager.MATCH_DEFAULT_ONLY) != 0; - boolean onlyOneNonBrowser = foundBrowser && foundOneApp; // Check if matches (BROWSABLE || none) && DEFAULT if (categoriesSize == 0) { - // No categories, run coerce case, matching DEFAULT by flags - return onlyOneNonBrowser && matchDefaultByFlags; - } else if (intent.hasCategory(Intent.CATEGORY_DEFAULT)) { - // Run coerce case, matching by explicit DEFAULT - return onlyOneNonBrowser; + // No categories, only allow matching DEFAULT by flags + return matchDefaultByFlags; } else if (intent.hasCategory(Intent.CATEGORY_BROWSABLE)) { // Intent matches BROWSABLE, must match DEFAULT by flags return matchDefaultByFlags; } else { - // Otherwise not matching any app link categories - return false; + // Otherwise only needs to have DEFAULT + return intent.hasCategory(Intent.CATEGORY_DEFAULT); } } diff --git a/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java b/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java index a089a6022735..40c70915f3ce 100644 --- a/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java +++ b/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java @@ -68,6 +68,12 @@ public class DomainVerificationPkgState { this(packageName, id, hasAutoVerifyDomains, new ArrayMap<>(0), new SparseArray<>(0)); } + public DomainVerificationPkgState(@NonNull DomainVerificationPkgState pkgState, + @NonNull UUID id, boolean hasAutoVerifyDomains) { + this(pkgState.getPackageName(), id, hasAutoVerifyDomains, pkgState.getStateMap(), + pkgState.getUserStates()); + } + @Nullable public DomainVerificationInternalUserState getUserState(@UserIdInt int userId) { return mUserStates.get(userId); @@ -84,10 +90,6 @@ public class DomainVerificationPkgState { return userState; } - public void setId(@NonNull UUID id) { - mId = id; - } - public void removeUser(@UserIdInt int userId) { mUserStates.remove(userId); } diff --git a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java index 18042af139a3..fa36683e4aff 100644 --- a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java +++ b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java @@ -207,20 +207,24 @@ public class DomainVerificationProxyV1 implements DomainVerificationProxy { int callingUid = response.callingUid; if (!successfulDomains.isEmpty()) { try { - mManager.setDomainVerificationStatusInternal(callingUid, domainSetId, - successfulDomains, DomainVerificationState.STATE_SUCCESS); - } catch (DomainVerificationManager.InvalidDomainSetException - | PackageManager.NameNotFoundException e) { + if (mManager.setDomainVerificationStatusInternal(callingUid, domainSetId, + successfulDomains, DomainVerificationState.STATE_SUCCESS) + != DomainVerificationManager.STATUS_OK) { + Slog.e(TAG, "Failure reporting successful domains for " + packageName); + } + } catch (Exception e) { Slog.e(TAG, "Failure reporting successful domains for " + packageName, e); } } if (!failedDomains.isEmpty()) { try { - mManager.setDomainVerificationStatusInternal(callingUid, domainSetId, - failedDomains, DomainVerificationState.STATE_LEGACY_FAILURE); - } catch (DomainVerificationManager.InvalidDomainSetException - | PackageManager.NameNotFoundException e) { + if (mManager.setDomainVerificationStatusInternal(callingUid, domainSetId, + failedDomains, DomainVerificationState.STATE_LEGACY_FAILURE) + != DomainVerificationManager.STATUS_OK) { + Slog.e(TAG, "Failure reporting failed domains for " + packageName); + } + } catch (Exception e) { Slog.e(TAG, "Failure reporting failed domains for " + packageName, e); } } diff --git a/services/tests/PackageManagerServiceTests/unit/Android.bp b/services/tests/PackageManagerServiceTests/unit/Android.bp index 334e53a3aec7..988c02bfb3db 100644 --- a/services/tests/PackageManagerServiceTests/unit/Android.bp +++ b/services/tests/PackageManagerServiceTests/unit/Android.bp @@ -23,14 +23,17 @@ package { android_test { name: "PackageManagerServiceUnitTests", - srcs: ["src/**/*.kt"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], static_libs: [ "androidx.test.rules", "androidx.test.runner", "junit", + "kotlin-test", "services.core", "servicestests-utils", - "testng", "truth-prebuilt", ], platform_apis: true, diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationCollectorTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationCollectorTest.kt index 0fa9a1def381..d5eda203e42f 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationCollectorTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationCollectorTest.kt @@ -25,8 +25,8 @@ import android.os.PatternMatcher import android.util.ArraySet import com.android.server.SystemConfig import com.android.server.compat.PlatformCompat -import com.android.server.pm.verify.domain.DomainVerificationCollector import com.android.server.pm.parsing.pkg.AndroidPackage +import com.android.server.pm.verify.domain.DomainVerificationCollector import com.android.server.testutils.mockThrowOnUnmocked import com.android.server.testutils.whenever import com.google.common.truth.Truth.assertThat diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationCoreApiTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationCoreApiTest.kt index 8ef92393242a..9693f3bab127 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationCoreApiTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationCoreApiTest.kt @@ -19,6 +19,7 @@ package com.android.server.pm.test.verify.domain import android.content.pm.verify.domain.DomainSet import android.content.pm.verify.domain.DomainVerificationInfo import android.content.pm.verify.domain.DomainVerificationRequest +import android.content.pm.verify.domain.DomainVerificationState import android.content.pm.verify.domain.DomainVerificationUserState import android.os.Parcel import android.os.Parcelable @@ -74,7 +75,9 @@ class DomainVerificationCoreApiTest { DomainVerificationInfo( UUID.fromString("703f6d34-6241-4cfd-8176-2e1d23355811"), "com.test.pkg", - massiveSet.withIndex().associate { it.value to it.index } + massiveSet.withIndex().associate { + it.value to DomainVerificationState.convertToInfoState(it.index) + } ) }, unparcel = { DomainVerificationInfo.CREATOR.createFromParcel(it) }, diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt index 53f0ca20e787..7e25901301aa 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt @@ -20,20 +20,21 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageUserState -import android.content.pm.verify.domain.DomainVerificationManager import android.content.pm.parsing.component.ParsedActivity import android.content.pm.parsing.component.ParsedIntentInfo +import android.content.pm.verify.domain.DomainVerificationManager +import android.content.pm.verify.domain.DomainVerificationState import android.os.Build import android.os.Process import android.util.ArraySet import android.util.SparseArray import androidx.test.platform.app.InstrumentationRegistry import com.android.server.pm.PackageSetting +import com.android.server.pm.parsing.pkg.AndroidPackage import com.android.server.pm.verify.domain.DomainVerificationEnforcer import com.android.server.pm.verify.domain.DomainVerificationManagerInternal import com.android.server.pm.verify.domain.DomainVerificationService import com.android.server.pm.verify.domain.proxy.DomainVerificationProxy -import com.android.server.pm.parsing.pkg.AndroidPackage import com.android.server.testutils.mockThrowOnUnmocked import com.android.server.testutils.spyThrowOnUnmocked import com.android.server.testutils.whenever @@ -50,6 +51,8 @@ import java.io.File import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertFailsWith +import kotlin.test.fail @RunWith(Parameterized::class) class DomainVerificationEnforcerTest { @@ -81,47 +84,49 @@ class DomainVerificationEnforcerTest { whenever(filterAppAccess(eq(INVISIBLE_PKG), anyInt(), anyInt())) { true } + whenever(doesUserExist(anyInt())) { (arguments[0] as Int) <= 1 } }) } } - val makeService: (Context) -> Triple<AtomicInteger, AtomicInteger, DomainVerificationService> = - { - val callingUidInt = AtomicInteger(-1) - val callingUserIdInt = AtomicInteger(-1) - - val connection: DomainVerificationManagerInternal.Connection = - mockThrowOnUnmocked { - whenever(callingUid) { callingUidInt.get() } - whenever(callingUserId) { callingUserIdInt.get() } - whenever(getPackageSettingLocked(VISIBLE_PKG)) { visiblePkgSetting } - whenever(getPackageLocked(VISIBLE_PKG)) { visiblePkg } - whenever(getPackageSettingLocked(INVISIBLE_PKG)) { invisiblePkgSetting } - whenever(getPackageLocked(INVISIBLE_PKG)) { invisiblePkg } - whenever(schedule(anyInt(), any())) - whenever(scheduleWriteSettings()) - whenever(filterAppAccess(eq(VISIBLE_PKG), anyInt(), anyInt())) { false } - whenever(filterAppAccess(eq(INVISIBLE_PKG), anyInt(), anyInt())) { - true - } + val makeService: (Context) -> Triple<AtomicInteger, AtomicInteger, + DomainVerificationService> = { + val callingUidInt = AtomicInteger(-1) + val callingUserIdInt = AtomicInteger(-1) + + val connection: DomainVerificationManagerInternal.Connection = + mockThrowOnUnmocked { + whenever(callingUid) { callingUidInt.get() } + whenever(callingUserId) { callingUserIdInt.get() } + whenever(getPackageSettingLocked(VISIBLE_PKG)) { visiblePkgSetting } + whenever(getPackageLocked(VISIBLE_PKG)) { visiblePkg } + whenever(getPackageSettingLocked(INVISIBLE_PKG)) { invisiblePkgSetting } + whenever(getPackageLocked(INVISIBLE_PKG)) { invisiblePkg } + whenever(schedule(anyInt(), any())) + whenever(scheduleWriteSettings()) + whenever(filterAppAccess(eq(VISIBLE_PKG), anyInt(), anyInt())) { false } + whenever(filterAppAccess(eq(INVISIBLE_PKG), anyInt(), anyInt())) { + true } - val service = DomainVerificationService( - it, - mockThrowOnUnmocked { whenever(linkedApps) { ArraySet<String>() } }, - mockThrowOnUnmocked { - whenever( - isChangeEnabled( - anyLong(), - any() - ) - ) { true } - }).apply { - setConnection(connection) + whenever(doesUserExist(anyInt())) { (arguments[0] as Int) <= 1 } } - - Triple(callingUidInt, callingUserIdInt, service) + val service = DomainVerificationService( + it, + mockThrowOnUnmocked { whenever(linkedApps) { ArraySet<String>() } }, + mockThrowOnUnmocked { + whenever( + isChangeEnabled( + anyLong(), + any() + ) + ) { true } + }).apply { + setConnection(connection) } + Triple(callingUidInt, callingUserIdInt, service) + } + fun enforcer( type: Type, name: String, @@ -175,7 +180,7 @@ class DomainVerificationEnforcerTest { service(Type.INTERNAL, "setStatusInternalPackageName") { setDomainVerificationStatusInternal( it.targetPackageName, - DomainVerificationManager.STATE_SUCCESS, + DomainVerificationState.STATE_SUCCESS, ArraySet(setOf("example.com")) ) }, @@ -206,7 +211,7 @@ class DomainVerificationEnforcerTest { setDomainVerificationStatus( it.targetDomainSetId, setOf("example.com"), - DomainVerificationManager.STATE_SUCCESS + DomainVerificationState.STATE_SUCCESS ) }, service(Type.VERIFIER, "setStatusInternalUid") { @@ -214,7 +219,7 @@ class DomainVerificationEnforcerTest { it.callingUid, it.targetDomainSetId, setOf("example.com"), - DomainVerificationManager.STATE_SUCCESS + DomainVerificationState.STATE_SUCCESS ) }, service(Type.SELECTOR_USER, "setLinkHandlingAllowedUserId") { @@ -475,24 +480,7 @@ class DomainVerificationEnforcerTest { fun runTestCases(callingUserId: Int, targetUserId: Int, throws: Boolean) { // User selector makes no distinction by UID val allUids = INTERNAL_UIDS + VERIFIER_UID + NON_VERIFIER_UID - if (throws) { - allUids.forEach { - assertFails { - runMethod(target, it, visible = true, callingUserId, targetUserId) - } - } - } else { - allUids.forEach { - runMethod(target, it, visible = true, callingUserId, targetUserId) - } - } - - // User selector doesn't use QUERY_ALL, so the invisible package should always fail - allUids.forEach { - assertFails { - runMethod(target, it, visible = false, callingUserId, targetUserId) - } - } + runCrossUserMethod(allUids, target, callingUserId, targetUserId, throws) } val callingUserId = 0 @@ -529,24 +517,7 @@ class DomainVerificationEnforcerTest { fun runTestCases(callingUserId: Int, targetUserId: Int, throws: Boolean) { // User selector makes no distinction by UID val allUids = INTERNAL_UIDS + VERIFIER_UID + NON_VERIFIER_UID - if (throws) { - allUids.forEach { - assertFails { - runMethod(target, it, visible = true, callingUserId, targetUserId) - } - } - } else { - allUids.forEach { - runMethod(target, it, visible = true, callingUserId, targetUserId) - } - } - - // User selector doesn't use QUERY_ALL, so the invisible package should always fail - allUids.forEach { - assertFails { - runMethod(target, it, visible = false, callingUserId, targetUserId) - } - } + runCrossUserMethod(allUids, target, callingUserId, targetUserId, throws) } val callingUserId = 0 @@ -590,24 +561,10 @@ class DomainVerificationEnforcerTest { fun runTestCases(callingUserId: Int, targetUserId: Int, throws: Boolean) { // Legacy makes no distinction by UID val allUids = INTERNAL_UIDS + VERIFIER_UID + NON_VERIFIER_UID - if (throws) { - allUids.forEach { - assertFails { - runMethod(target, it, visible = true, callingUserId, targetUserId) - } - } - } else { - allUids.forEach { - runMethod(target, it, visible = true, callingUserId, targetUserId) - } - } - - // Legacy doesn't use QUERY_ALL, so the invisible package should always fail - allUids.forEach { - assertFails { - runMethod(target, it, visible = false, callingUserId, targetUserId) - } - } + // The legacy selector does a silent failure when the user IDs don't match, so it + // cannot verify the non-existent user ID check, as it will not throw an Exception. + runCrossUserMethod(allUids, target, callingUserId, targetUserId, throws, + verifyUserIdCheck = false) } val callingUserId = 0 @@ -642,27 +599,29 @@ class DomainVerificationEnforcerTest { } val target = params.construct(context) - fun runTestCases(callingUserId: Int, targetUserId: Int, throws: Boolean) { - // Legacy makes no distinction by UID - val allUids = INTERNAL_UIDS + VERIFIER_UID + NON_VERIFIER_UID - if (throws) { - allUids.forEach { - assertFails { - runMethod(target, it, visible = true, callingUserId, targetUserId) - } - } - } else { - allUids.forEach { - runMethod(target, it, visible = true, callingUserId, targetUserId) + // Legacy code can return PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED + // as an error code. This is distinct from the class level assertFails as unfortunately + // the same number, 0, is used in opposite contexts, where it does represent a failure + // for this legacy case, but not for the modern APIs. + fun assertFailsLegacy(block: () -> Any?) { + try { + val value = block() + if ((value as? Int) + != PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED + ) { + throw AssertionError("Expected call to return false, was $value") } + } catch (e: SecurityException) { + } catch (e: PackageManager.NameNotFoundException) { + // Any of these 2 exceptions are considered failures, which is expected } + } - // Legacy doesn't use QUERY_ALL, so the invisible package should always fail - allUids.forEach { - assertFails { - runMethod(target, it, visible = false, callingUserId, targetUserId) - } - } + fun runTestCases(callingUserId: Int, targetUserId: Int, throws: Boolean) { + // Legacy makes no distinction by UID + val allUids = INTERNAL_UIDS + VERIFIER_UID + NON_VERIFIER_UID + runCrossUserMethod(allUids, target, callingUserId, targetUserId, throws, + assertFailsMethod = ::assertFailsLegacy) } val callingUserId = 0 @@ -704,17 +663,8 @@ class DomainVerificationEnforcerTest { fun runTestCases(callingUserId: Int, targetUserId: Int, throws: Boolean) { // Owner querent makes no distinction by UID val allUids = INTERNAL_UIDS + VERIFIER_UID + NON_VERIFIER_UID - if (throws) { - allUids.forEach { - assertFails { - runMethod(target, it, visible = true, callingUserId, targetUserId) - } - } - } else { - allUids.forEach { - runMethod(target, it, visible = true, callingUserId, targetUserId) - } - } + runCrossUserMethod(allUids, target, callingUserId, targetUserId, throws, + verifyInvisiblePkg = false) } val callingUserId = 0 @@ -782,22 +732,88 @@ class DomainVerificationEnforcerTest { return params.runMethod(target, callingUid, callingUserId, userId, packageName, uuid, proxy) } + private fun runCrossUserMethod( + allUids: Iterable<Int>, + target: Any, + callingUserId: Int, + targetUserId: Int, + throws: Boolean, + verifyUserIdCheck: Boolean = true, + verifyInvisiblePkg: Boolean = true, + assertFailsMethod: (() -> Any?) -> Unit = ::assertFails, + ) { + if (throws) { + allUids.forEach { + assertFailsMethod { + // When testing a non-user ID failure, send an invalid user ID. + // This ensures the failure occurs before the user ID check is run. + try { + runMethod(target, it, visible = true, callingUserId, 100) + } catch (e: SecurityException) { + if (verifyUserIdCheck) { + e.message?.let { + if (it.contains("user ID", ignoreCase = true) + || it.contains("100")) { + fail( + "Method should not check user existence before permissions" + ) + } + } + } + + // Rethrow to allow normal fail checking logic to run + throw e + } + } + } + } else { + allUids.forEach { + runMethod(target, it, visible = true, callingUserId, targetUserId) + } + } + + if (verifyInvisiblePkg) { + allUids.forEach { + assertFailsMethod { + runMethod(target, it, visible = false, callingUserId, targetUserId) + } + } + } + + if (verifyUserIdCheck) { + // An invalid target user ID should always fail + allUids.forEach { + assertFailsWith(SecurityException::class) { + runMethod(target, it, visible = true, callingUserId, 100) + } + } + + // An invalid calling user ID should always fail, although this cannot happen in prod + allUids.forEach { + assertFailsWith(SecurityException::class) { + runMethod(target, it, visible = true, 100, targetUserId) + } + } + } + } + private fun assertFails(block: () -> Any?) { try { val value = block() - // Some methods return false rather than throwing, so check that as well - if ((value as? Boolean) != false) { - // Can also return default value if it's a legacy call - if ((value as? Int) - != PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED - ) { - throw AssertionError("Expected call to return false, was $value") - } + // Some methods return false or an error rather than throwing, so check that as well + val valueAsBoolean = value as? Boolean + if (valueAsBoolean == false) { + // Expected failure, do not throw + return + } + + val valueAsInt = value as? Int + if (valueAsInt != null && valueAsInt == DomainVerificationManager.STATUS_OK) { + throw AssertionError("Expected call to return false, was $value") } } catch (e: SecurityException) { } catch (e: PackageManager.NameNotFoundException) { - } catch (e: DomainVerificationManager.InvalidDomainSetException) { - // Any of these 3 exceptions are considered failures, which is expected + // Any of these 2 exceptions are considered failures, which is expected } } diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationJavaUtil.java b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationJavaUtil.java new file mode 100644 index 000000000000..8ae4c5ae96a3 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationJavaUtil.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.pm.test.verify.domain; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.pm.PackageManager; + +import com.android.server.pm.verify.domain.DomainVerificationService; + +import java.util.Set; +import java.util.UUID; + +/** + * Proxies Kotlin calls to the Java layer such that null values can be passed for {@link NonNull} + * marked parameters, as Kotlin disallows this at the compiler leveling, preventing the null error + * codes from being tested. + */ +class DomainVerificationJavaUtil { + + static int setStatusForceNullable(@NonNull DomainVerificationService service, + @Nullable UUID domainSetId, @Nullable Set<String> domains, int state) + throws PackageManager.NameNotFoundException { + return service.setDomainVerificationStatus(domainSetId, domains, state); + } + + static int setUserSelectionForceNullable(@NonNull DomainVerificationService service, + @Nullable UUID domainSetId, @Nullable Set<String> domains, boolean enabled, + @UserIdInt int userId) + throws PackageManager.NameNotFoundException { + return service.setDomainVerificationUserSelection(domainSetId, domains, enabled, userId); + } +} diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationLegacySettingsTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationLegacySettingsTest.kt index 9a3bd994eac0..509824d93512 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationLegacySettingsTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationLegacySettingsTest.kt @@ -19,9 +19,9 @@ package com.android.server.pm.test.verify.domain import android.content.pm.IntentFilterVerificationInfo import android.content.pm.PackageManager import android.util.ArraySet -import com.android.server.pm.verify.domain.DomainVerificationLegacySettings import com.android.server.pm.test.verify.domain.DomainVerificationPersistenceTest.Companion.readXml import com.android.server.pm.test.verify.domain.DomainVerificationPersistenceTest.Companion.writeXml +import com.android.server.pm.verify.domain.DomainVerificationLegacySettings import com.google.common.truth.Truth.assertWithMessage import org.junit.Rule import org.junit.Test diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt new file mode 100644 index 000000000000..0e74b65d25d5 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.pm.test.verify.domain + +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.parsing.component.ParsedActivity +import android.content.pm.parsing.component.ParsedIntentInfo +import android.content.pm.verify.domain.DomainVerificationInfo +import android.content.pm.verify.domain.DomainVerificationManager +import android.content.pm.verify.domain.DomainVerificationUserState +import android.os.Build +import android.os.PatternMatcher +import android.os.Process +import android.util.ArraySet +import com.android.server.pm.PackageSetting +import com.android.server.pm.parsing.pkg.AndroidPackage +import com.android.server.pm.verify.domain.DomainVerificationService +import com.android.server.testutils.mockThrowOnUnmocked +import com.android.server.testutils.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString +import java.util.UUID +import kotlin.test.assertFailsWith + +class DomainVerificationManagerApiTest { + + companion object { + private const val PKG_ONE = "com.test.one" + private const val PKG_TWO = "com.test.two" + private const val PKG_THREE = "com.test.three" + + private val UUID_ONE = UUID.fromString("1b041c96-8d37-4932-a858-561bfac5947c") + private val UUID_TWO = UUID.fromString("a3389c16-7f9f-4e86-85e3-500d1249c74c") + private val UUID_THREE = UUID.fromString("0b3260ed-07c4-4b45-840b-237f8fb8b433") + private val UUID_INVALID = UUID.fromString("ad33babc-490b-4965-9d78-7e91248b00f") + + private val DOMAIN_BASE = DomainVerificationManagerApiTest::class.java.packageName + private val DOMAIN_1 = "one.$DOMAIN_BASE" + private val DOMAIN_2 = "two.$DOMAIN_BASE" + private val DOMAIN_3 = "three.$DOMAIN_BASE" + private val DOMAIN_4 = "four.$DOMAIN_BASE" + } + + @Test + fun queryValidVerificationPackageNames() { + val pkgWithDomains = mockPkgSetting(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2)) + val pkgWithoutDomains = mockPkgSetting(PKG_TWO, UUID_TWO, emptyList()) + + val service = makeService(pkgWithDomains, pkgWithoutDomains).apply { + addPackages(pkgWithDomains, pkgWithoutDomains) + } + + assertThat(service.queryValidVerificationPackageNames()) + .containsExactly(pkgWithDomains.getName()) + } + + @Test + fun getDomainVerificationInfoId() { + val pkgWithDomains = mockPkgSetting(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2)) + val pkgWithoutDomains = mockPkgSetting(PKG_TWO, UUID_TWO, emptyList()) + + val service = makeService(pkgWithDomains, pkgWithoutDomains).apply { + addPackages(pkgWithDomains, pkgWithoutDomains) + } + + assertThat(service.getDomainVerificationInfoId(PKG_ONE)).isEqualTo(UUID_ONE) + assertThat(service.getDomainVerificationInfoId(PKG_TWO)).isEqualTo(UUID_TWO) + + assertThat(service.getDomainVerificationInfoId("invalid.pkg.name")).isEqualTo(null) + } + + @Test + fun getDomainVerificationInfo() { + val pkgWithDomains = mockPkgSetting(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2)) + val pkgWithoutDomains = mockPkgSetting(PKG_TWO, UUID_TWO, emptyList()) + + val service = makeService(pkgWithDomains, pkgWithoutDomains).apply { + addPackages(pkgWithDomains, pkgWithoutDomains) + } + + val infoOne = service.getDomainVerificationInfo(pkgWithDomains.getName()) + assertThat(infoOne).isNotNull() + assertThat(infoOne!!.identifier).isEqualTo(pkgWithDomains.domainSetId) + assertThat(infoOne.packageName).isEqualTo(pkgWithDomains.getName()) + assertThat(infoOne.hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DomainVerificationInfo.STATE_NO_RESPONSE, + DOMAIN_2 to DomainVerificationInfo.STATE_NO_RESPONSE, + )) + + assertThat(service.getDomainVerificationInfo(pkgWithoutDomains.getName())).isNull() + + assertFailsWith(PackageManager.NameNotFoundException::class) { + service.getDomainVerificationInfo("invalid.pkg.name") + } + } + + @Test + fun setStatus() { + val pkg1 = mockPkgSetting(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2)) + val pkg2 = mockPkgSetting(PKG_TWO, UUID_TWO, listOf(DOMAIN_3, DOMAIN_4)) + + val map = mutableMapOf(pkg1.getName() to pkg1, pkg2.getName() to pkg2) + val service = makeService(map::get).apply { addPackages(pkg1, pkg2) } + + assertThat(service.setStatus(UUID_ONE, setOf(DOMAIN_2), 1100)) + .isEqualTo(DomainVerificationManager.STATUS_OK) + + assertThat(service.setStatus(UUID_INVALID, setOf(DOMAIN_1), 1100)) + .isEqualTo(DomainVerificationManager.ERROR_DOMAIN_SET_ID_INVALID) + + assertThat(DomainVerificationJavaUtil.setStatusForceNullable(service, null, + setOf(DOMAIN_1), 1100)) + .isEqualTo(DomainVerificationManager.ERROR_DOMAIN_SET_ID_NULL) + + assertThat(DomainVerificationJavaUtil.setStatusForceNullable(service, UUID_ONE, null, + 1100)).isEqualTo(DomainVerificationManager.ERROR_DOMAIN_SET_NULL_OR_EMPTY) + + assertThat(service.setStatus(UUID_ONE, emptySet(), 1100)) + .isEqualTo(DomainVerificationManager.ERROR_DOMAIN_SET_NULL_OR_EMPTY) + + assertThat(service.setStatus(UUID_ONE, setOf(DOMAIN_3), 1100)) + .isEqualTo(DomainVerificationManager.ERROR_UNKNOWN_DOMAIN) + + assertThat(service.setStatus(UUID_ONE, setOf(DOMAIN_1, DOMAIN_2, DOMAIN_3), 1100)) + .isEqualTo(DomainVerificationManager.ERROR_UNKNOWN_DOMAIN) + + assertThat(service.setStatus(UUID_ONE, setOf(DOMAIN_1), 15)) + .isEqualTo(DomainVerificationManager.ERROR_INVALID_STATE_CODE) + + map.clear() + assertFailsWith(PackageManager.NameNotFoundException::class){ + service.setStatus(UUID_ONE, setOf(DOMAIN_1), 1100) + } + } + + @Test + fun setDomainVerificationLinkHandlingAllowed() { + val pkg1 = mockPkgSetting(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2)) + val pkg2 = mockPkgSetting(PKG_TWO, UUID_TWO, listOf(DOMAIN_3, DOMAIN_4)) + + val map = mutableMapOf(pkg1.getName() to pkg1, pkg2.getName() to pkg2) + val service = makeService(map::get).apply { addPackages(pkg1, pkg2) } + + service.setDomainVerificationLinkHandlingAllowed(PKG_ONE, false, 0); + + // Should edit same package, same user + assertThat(service.getDomainVerificationUserState(PKG_ONE, 0) + ?.isLinkHandlingAllowed).isEqualTo(false) + + // Shouldn't edit different user + assertThat(service.getDomainVerificationUserState(PKG_ONE, 1) + ?.isLinkHandlingAllowed).isEqualTo(true) + + // Shouldn't edit different package + assertThat(service.getDomainVerificationUserState(PKG_TWO, 0) + ?.isLinkHandlingAllowed).isEqualTo(true) + + assertFailsWith(PackageManager.NameNotFoundException::class){ + service.setDomainVerificationLinkHandlingAllowed("invalid.pkg.name", false, 0); + } + } + + @Test + fun setUserSelection() { + val pkg1 = mockPkgSetting(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2)) + val pkg2 = mockPkgSetting(PKG_TWO, UUID_TWO, listOf(DOMAIN_3, DOMAIN_4)) + val pkg3 = mockPkgSetting(PKG_THREE, UUID_THREE, listOf(DOMAIN_1, DOMAIN_2)) + + val map = mutableMapOf( + pkg1.getName() to pkg1, + pkg2.getName() to pkg2, + pkg3.getName() to pkg3 + ) + val service = makeService(map::get).apply { addPackages(pkg1, pkg2, pkg3) } + + assertThat(service.setUserSelection(UUID_ONE, setOf(DOMAIN_2), true, 0)) + .isEqualTo(DomainVerificationManager.STATUS_OK) + + assertThat(service.setUserSelection(UUID_INVALID, setOf(DOMAIN_1), true, 0)) + .isEqualTo(DomainVerificationManager.ERROR_DOMAIN_SET_ID_INVALID) + + assertThat(DomainVerificationJavaUtil.setUserSelectionForceNullable(service, null, + setOf(DOMAIN_1), true, 0)) + .isEqualTo(DomainVerificationManager.ERROR_DOMAIN_SET_ID_NULL) + + assertThat(DomainVerificationJavaUtil.setUserSelectionForceNullable(service, UUID_ONE, null, + true, 0)).isEqualTo(DomainVerificationManager.ERROR_DOMAIN_SET_NULL_OR_EMPTY) + + assertThat(service.setUserSelection(UUID_ONE, emptySet(), true, 0)) + .isEqualTo(DomainVerificationManager.ERROR_DOMAIN_SET_NULL_OR_EMPTY) + + assertThat(service.setUserSelection(UUID_ONE, setOf(DOMAIN_3), true, 0)) + .isEqualTo(DomainVerificationManager.ERROR_UNKNOWN_DOMAIN) + + assertThat(service.setUserSelection(UUID_ONE, setOf(DOMAIN_1, DOMAIN_2, DOMAIN_3), true, 0)) + .isEqualTo(DomainVerificationManager.ERROR_UNKNOWN_DOMAIN) + + service.setStatus(UUID_ONE, setOf(DOMAIN_2), DomainVerificationInfo.STATE_SUCCESS) + + assertThat(service.setUserSelection(UUID_THREE, setOf(DOMAIN_2), true, 0)) + .isEqualTo(DomainVerificationManager.ERROR_UNABLE_TO_APPROVE) + + map.clear() + assertFailsWith(PackageManager.NameNotFoundException::class){ + service.setUserSelection(UUID_ONE, setOf(DOMAIN_1), true, 0) + } + } + + @Test + fun getDomainVerificationUserState() { + val pkgWithDomains = mockPkgSetting(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2)) + val pkgWithoutDomains = mockPkgSetting(PKG_TWO, UUID_TWO, emptyList()) + + val service = makeService(pkgWithDomains, pkgWithoutDomains).apply { + addPackages(pkgWithDomains, pkgWithoutDomains) + } + + val infoOne = service.getDomainVerificationUserState(pkgWithDomains.getName(), 0) + assertThat(infoOne).isNotNull() + assertThat(infoOne!!.identifier).isEqualTo(pkgWithDomains.domainSetId) + assertThat(infoOne.packageName).isEqualTo(pkgWithDomains.getName()) + assertThat(infoOne.isLinkHandlingAllowed).isTrue() + assertThat(infoOne.hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DomainVerificationUserState.DOMAIN_STATE_NONE, + DOMAIN_2 to DomainVerificationUserState.DOMAIN_STATE_NONE, + )) + + val infoTwo = service.getDomainVerificationUserState(pkgWithoutDomains.getName(), 0) + assertThat(infoTwo).isNotNull() + assertThat(infoTwo!!.identifier).isEqualTo(pkgWithoutDomains.domainSetId) + assertThat(infoTwo.packageName).isEqualTo(pkgWithoutDomains.getName()) + assertThat(infoOne.isLinkHandlingAllowed).isTrue() + assertThat(infoTwo.hostToStateMap).isEmpty() + + assertFailsWith(PackageManager.NameNotFoundException::class) { + service.getDomainVerificationUserState("invalid.pkg.name", 0) + } + } + + @Test + fun getOwnersForDomain() { + val pkg1 = mockPkgSetting(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2)) + val pkg2 = mockPkgSetting(PKG_TWO, UUID_TWO, listOf(DOMAIN_1, DOMAIN_2)) + + val service = makeService(pkg1, pkg2).apply { + addPackages(pkg1, pkg2) + } + + assertThat(service.getOwnersForDomain(DOMAIN_1, 0)).isEmpty() + + service.setStatus(pkg1.domainSetId, setOf(DOMAIN_1), DomainVerificationInfo.STATE_SUCCESS) + + service.setStatus(pkg2.domainSetId, setOf(DOMAIN_1), DomainVerificationInfo.STATE_SUCCESS) + + service.setUserSelection(pkg1.domainSetId, setOf(DOMAIN_2), true, 0) + + service.getOwnersForDomain(DOMAIN_1, 0).let { + assertThat(it).hasSize(2) + assertThat(it[0].packageName).isEqualTo(pkg1.getName()) + assertThat(it[0].isOverrideable).isEqualTo(false) + assertThat(it[1].packageName).isEqualTo(pkg2.getName()) + assertThat(it[1].isOverrideable).isEqualTo(false) + } + + service.getOwnersForDomain(DOMAIN_2, 0).let { + assertThat(it).hasSize(1) + assertThat(it.single().packageName).isEqualTo(pkg1.getName()) + assertThat(it.single().isOverrideable).isEqualTo(true) + } + assertThat(service.getOwnersForDomain(DOMAIN_2, 1)).isEmpty() + + service.setUserSelection(pkg2.domainSetId, setOf(DOMAIN_2), true, 0) + service.getOwnersForDomain(DOMAIN_2, 0).let { + assertThat(it).hasSize(1) + assertThat(it.single().packageName).isEqualTo(pkg2.getName()) + assertThat(it.single().isOverrideable).isEqualTo(true) + } + assertThat(service.getOwnersForDomain(DOMAIN_2, 1)).isEmpty() + } + + private fun makeService(vararg pkgSettings: PackageSetting) = + makeService { pkgName -> pkgSettings.find { pkgName == it.getName() } } + + private fun makeService(pkgSettingFunction: (String) -> PackageSetting? = { null }) = + DomainVerificationService(mockThrowOnUnmocked { + // Assume the test has every permission necessary + whenever(enforcePermission(anyString(), anyInt(), anyInt(), anyString())) + whenever(checkPermission(anyString(), anyInt(), anyInt())) { + PackageManager.PERMISSION_GRANTED + } + }, mockThrowOnUnmocked { + whenever(linkedApps) { ArraySet<String>() } + }, mockThrowOnUnmocked { + whenever(isChangeEnabled(anyLong(), any())) { true } + }).apply { + setConnection(mockThrowOnUnmocked { + whenever(filterAppAccess(anyString(), anyInt(), anyInt())) { false } + whenever(doesUserExist(0)) { true } + whenever(doesUserExist(1)) { true } + whenever(scheduleWriteSettings()) + + // Need to provide an internal UID so some permission checks are ignored + whenever(callingUid) { Process.ROOT_UID } + whenever(callingUserId) { 0 } + + whenever(getPackageSettingLocked(anyString())) { + pkgSettingFunction(arguments[0] as String) + } + whenever(getPackageLocked(anyString())) { + pkgSettingFunction(arguments[0] as String)?.getPkg() + } + }) + } + + private fun mockPkgSetting(pkgName: String, domainSetId: UUID, domains: List<String> = listOf( + DOMAIN_1, DOMAIN_2 + )) = mockThrowOnUnmocked<PackageSetting> { + val pkg = mockThrowOnUnmocked<AndroidPackage> { + whenever(packageName) { pkgName } + whenever(targetSdkVersion) { Build.VERSION_CODES.S } + + val activityList = listOf( + ParsedActivity().apply { + domains.forEach { + addIntent( + ParsedIntentInfo().apply { + autoVerify = true + addAction(Intent.ACTION_VIEW) + addCategory(Intent.CATEGORY_BROWSABLE) + addCategory(Intent.CATEGORY_DEFAULT) + addDataScheme("http") + addDataScheme("https") + addDataPath("/sub", PatternMatcher.PATTERN_LITERAL) + addDataAuthority(it, null) + } + ) + } + }, + ) + + whenever(activities) { activityList } + } + + whenever(getPkg()) { pkg } + whenever(getName()) { pkgName } + whenever(this.domainSetId) { domainSetId } + whenever(getInstantApp(anyInt())) { false } + whenever(firstInstallTime) { 0L } + } + + fun DomainVerificationService.addPackages(vararg pkgSettings: PackageSetting) = + pkgSettings.forEach(::addPackage) +} diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationModelExtensions.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationModelExtensions.kt index 8c31c65e1b0a..a5db3c578670 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationModelExtensions.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationModelExtensions.kt @@ -16,8 +16,8 @@ package com.android.server.pm.test.verify.domain -import android.content.pm.verify.domain.DomainVerificationRequest import android.content.pm.verify.domain.DomainVerificationInfo +import android.content.pm.verify.domain.DomainVerificationRequest import android.content.pm.verify.domain.DomainVerificationUserState import com.android.server.pm.verify.domain.DomainVerificationPersistence diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt new file mode 100644 index 000000000000..fe3672d06bc0 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.pm.test.verify.domain + +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.parsing.component.ParsedActivity +import android.content.pm.parsing.component.ParsedIntentInfo +import android.content.pm.verify.domain.DomainVerificationInfo.STATE_NO_RESPONSE +import android.content.pm.verify.domain.DomainVerificationInfo.STATE_SUCCESS +import android.content.pm.verify.domain.DomainVerificationInfo.STATE_UNMODIFIABLE +import android.content.pm.verify.domain.DomainVerificationManager +import android.content.pm.verify.domain.DomainVerificationState +import android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE +import android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_SELECTED +import android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_VERIFIED +import android.os.Build +import android.os.PatternMatcher +import android.os.Process +import android.util.ArraySet +import android.util.Xml +import com.android.server.pm.PackageSetting +import com.android.server.pm.parsing.pkg.AndroidPackage +import com.android.server.pm.verify.domain.DomainVerificationService +import com.android.server.testutils.mockThrowOnUnmocked +import com.android.server.testutils.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import java.util.UUID + +class DomainVerificationPackageTest { + + companion object { + private const val PKG_ONE = "com.test.one" + private const val PKG_TWO = "com.test.two" + private val UUID_ONE = UUID.fromString("1b041c96-8d37-4932-a858-561bfac5947c") + private val UUID_TWO = UUID.fromString("a3389c16-7f9f-4e86-85e3-500d1249c74c") + + private val DOMAIN_BASE = DomainVerificationPackageTest::class.java.packageName + private val DOMAIN_1 = "one.$DOMAIN_BASE" + private val DOMAIN_2 = "two.$DOMAIN_BASE" + private val DOMAIN_3 = "three.$DOMAIN_BASE" + private val DOMAIN_4 = "four.$DOMAIN_BASE" + + private const val USER_ID = 0 + } + + private val pkg1 = mockPkgSetting(PKG_ONE, UUID_ONE) + private val pkg2 = mockPkgSetting(PKG_TWO, UUID_TWO) + + @Test + fun addPackageFirstTime() { + val service = makeService(pkg1, pkg2) + service.addPackage(pkg1) + val info = service.getInfo(pkg1.getName()) + assertThat(info.packageName).isEqualTo(pkg1.getName()) + assertThat(info.identifier).isEqualTo(pkg1.domainSetId) + assertThat(info.hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to STATE_NO_RESPONSE, + DOMAIN_2 to STATE_NO_RESPONSE, + )) + + val userState = service.getUserState(pkg1.getName()) + assertThat(userState.packageName).isEqualTo(pkg1.getName()) + assertThat(userState.identifier).isEqualTo(pkg1.domainSetId) + assertThat(userState.isLinkHandlingAllowed).isEqualTo(true) + assertThat(userState.user.identifier).isEqualTo(USER_ID) + assertThat(userState.hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DOMAIN_STATE_NONE, + DOMAIN_2 to DOMAIN_STATE_NONE, + )) + + assertThat(service.queryValidVerificationPackageNames()) + .containsExactly(pkg1.getName()) + } + + @Test + fun addPackageActive() { + // language=XML + val xml = """ + <?xml?> + <domain-verifications> + <active> + <package-state + packageName="${pkg1.getName()}" + id="${pkg1.domainSetId}" + > + <state> + <domain name="$DOMAIN_1" state="$STATE_SUCCESS"/> + </state> + <user-states> + <user-state userId="$USER_ID" allowLinkHandling="false"> + <enabled-hosts> + <host name="$DOMAIN_2"/> + </enabled-hosts> + </user-state> + </user-states> + </package-state> + </active> + </domain-verifications> + """.trimIndent() + + val service = makeService(pkg1, pkg2) + xml.byteInputStream().use { + service.readSettings(Xml.resolvePullParser(it)) + } + + service.addPackage(pkg1) + + val info = service.getInfo(pkg1.getName()) + assertThat(info.packageName).isEqualTo(pkg1.getName()) + assertThat(info.identifier).isEqualTo(pkg1.domainSetId) + assertThat(info.hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to STATE_SUCCESS, + DOMAIN_2 to STATE_NO_RESPONSE, + )) + + val userState = service.getUserState(pkg1.getName()) + assertThat(userState.packageName).isEqualTo(pkg1.getName()) + assertThat(userState.identifier).isEqualTo(pkg1.domainSetId) + assertThat(userState.isLinkHandlingAllowed).isEqualTo(false) + assertThat(userState.user.identifier).isEqualTo(USER_ID) + assertThat(userState.hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DOMAIN_STATE_VERIFIED, + DOMAIN_2 to DOMAIN_STATE_SELECTED, + )) + + assertThat(service.queryValidVerificationPackageNames()) + .containsExactly(pkg1.getName()) + } + + @Test + fun migratePackageDropDomain() { + val pkgName = PKG_ONE + val pkgBefore = mockPkgSetting(pkgName, UUID_ONE, + listOf(DOMAIN_1, DOMAIN_2, DOMAIN_3, DOMAIN_4)) + val pkgAfter = mockPkgSetting(pkgName, UUID_TWO, + listOf(DOMAIN_1, DOMAIN_2)) + + // Test 4 domains: + // 1 will be approved and preserved, 2 will be selected and preserved, + // 3 will be denied and dropped, 4 will be selected and dropped + + val map = mutableMapOf<String, PackageSetting>() + val service = makeService { map[it] } + service.addPackage(pkgBefore) + + // Only insert the package after addPackage call to ensure the service doesn't access + // a live package inside the addPackage logic. It should only use the provided input. + map[pkgName] = pkgBefore + + // To test the approve/denial states, use the internal methods for this variant + service.setDomainVerificationStatusInternal(pkgName, DomainVerificationState.STATE_APPROVED, + ArraySet(setOf(DOMAIN_1))) + service.setDomainVerificationStatusInternal(pkgName, DomainVerificationState.STATE_DENIED, + ArraySet(setOf(DOMAIN_3))) + service.setUserSelection( + UUID_ONE, setOf(DOMAIN_2, DOMAIN_4), true, USER_ID) + + // Check the verifier cannot change the shell approve/deny states + service.setStatus(UUID_ONE, setOf(DOMAIN_1, DOMAIN_3), STATE_SUCCESS) + + assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to STATE_UNMODIFIABLE, + DOMAIN_2 to STATE_NO_RESPONSE, + DOMAIN_3 to STATE_UNMODIFIABLE, + DOMAIN_4 to STATE_NO_RESPONSE, + )) + assertThat(service.getUserState(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DOMAIN_STATE_VERIFIED, + DOMAIN_2 to DOMAIN_STATE_SELECTED, + DOMAIN_3 to DOMAIN_STATE_NONE, + DOMAIN_4 to DOMAIN_STATE_SELECTED, + )) + + // Now remove the package because migrateState shouldn't use it either + map.remove(pkgName) + + map[pkgName] = pkgAfter + + service.migrateState(pkgBefore, pkgAfter) + + assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to STATE_UNMODIFIABLE, + DOMAIN_2 to STATE_NO_RESPONSE, + )) + assertThat(service.getUserState(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DOMAIN_STATE_VERIFIED, + DOMAIN_2 to DOMAIN_STATE_SELECTED, + )) + assertThat(service.queryValidVerificationPackageNames()).containsExactly(pkgName) + } + + @Test + fun migratePackageDropAll() { + val pkgName = PKG_ONE + val pkgBefore = mockPkgSetting(pkgName, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2)) + val pkgAfter = mockPkgSetting(pkgName, UUID_TWO, emptyList()) + + val map = mutableMapOf<String, PackageSetting>() + val service = makeService { map[it] } + service.addPackage(pkgBefore) + + // Only insert the package after addPackage call to ensure the service doesn't access + // a live package inside the addPackage logic. It should only use the provided input. + map[pkgName] = pkgBefore + + assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to STATE_NO_RESPONSE, + DOMAIN_2 to STATE_NO_RESPONSE, + )) + assertThat(service.getUserState(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DOMAIN_STATE_NONE, + DOMAIN_2 to DOMAIN_STATE_NONE, + )) + assertThat(service.queryValidVerificationPackageNames()).containsExactly(pkgName) + + // Now remove the package because migrateState shouldn't use it either + map.remove(pkgName) + + service.migrateState(pkgBefore, pkgAfter) + + map[pkgName] = pkgAfter + + assertThat(service.setStatus(UUID_ONE, setOf(DOMAIN_1), STATE_SUCCESS)) + .isNotEqualTo(DomainVerificationManager.STATUS_OK) + + assertThat(service.setUserSelection(UUID_ONE, setOf(DOMAIN_2), true, USER_ID)) + .isNotEqualTo(DomainVerificationManager.STATUS_OK) + + assertThat(service.getDomainVerificationInfo(pkgName)).isNull() + assertThat(service.getUserState(pkgName).hostToStateMap).isEmpty() + assertThat(service.queryValidVerificationPackageNames()).isEmpty() + } + + @Test + fun migratePackageAddDomain() { + val pkgName = PKG_ONE + val pkgBefore = mockPkgSetting(pkgName, UUID_ONE, + listOf(DOMAIN_1, DOMAIN_2)) + val pkgAfter = mockPkgSetting(pkgName, UUID_TWO, + listOf(DOMAIN_1, DOMAIN_2, DOMAIN_3)) + + // Test 3 domains: + // 1 will be verified and preserved, 2 will be selected and preserved, + // 3 will be new and default + + val map = mutableMapOf<String, PackageSetting>() + val service = makeService { map[it] } + service.addPackage(pkgBefore) + + // Only insert the package after addPackage call to ensure the service doesn't access + // a live package inside the addPackage logic. It should only use the provided input. + map[pkgName] = pkgBefore + + service.setStatus(UUID_ONE, setOf(DOMAIN_1), STATE_SUCCESS) + service.setUserSelection(UUID_ONE, setOf(DOMAIN_2), true, USER_ID) + + assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to STATE_SUCCESS, + DOMAIN_2 to STATE_NO_RESPONSE, + )) + assertThat(service.getUserState(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DOMAIN_STATE_VERIFIED, + DOMAIN_2 to DOMAIN_STATE_SELECTED, + )) + + // Now remove the package because migrateState shouldn't use it either + map.remove(pkgName) + + service.migrateState(pkgBefore, pkgAfter) + + map[pkgName] = pkgAfter + + assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to STATE_SUCCESS, + DOMAIN_2 to STATE_NO_RESPONSE, + DOMAIN_3 to STATE_NO_RESPONSE, + )) + assertThat(service.getUserState(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DOMAIN_STATE_VERIFIED, + DOMAIN_2 to DOMAIN_STATE_SELECTED, + DOMAIN_3 to DOMAIN_STATE_NONE, + )) + assertThat(service.queryValidVerificationPackageNames()).containsExactly(pkgName) + } + + @Test + fun migratePackageAddAll() { + val pkgName = PKG_ONE + val pkgBefore = mockPkgSetting(pkgName, UUID_ONE, emptyList()) + val pkgAfter = mockPkgSetting(pkgName, UUID_TWO, listOf(DOMAIN_1, DOMAIN_2)) + + val map = mutableMapOf<String, PackageSetting>() + val service = makeService { map[it] } + service.addPackage(pkgBefore) + + // Only insert the package after addPackage call to ensure the service doesn't access + // a live package inside the addPackage logic. It should only use the provided input. + map[pkgName] = pkgBefore + + assertThat(service.setStatus(UUID_ONE, setOf(DOMAIN_1), STATE_SUCCESS)) + .isNotEqualTo(DomainVerificationManager.STATUS_OK) + + assertThat(service.setUserSelection(UUID_ONE, setOf(DOMAIN_2), true, USER_ID)) + .isNotEqualTo(DomainVerificationManager.STATUS_OK) + + assertThat(service.getDomainVerificationInfo(pkgName)).isNull() + assertThat(service.getUserState(pkgName).hostToStateMap).isEmpty() + assertThat(service.queryValidVerificationPackageNames()).isEmpty() + + // Now remove the package because migrateState shouldn't use it either + map.remove(pkgName) + + service.migrateState(pkgBefore, pkgAfter) + + map[pkgName] = pkgAfter + + assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to STATE_NO_RESPONSE, + DOMAIN_2 to STATE_NO_RESPONSE, + )) + assertThat(service.getUserState(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DOMAIN_STATE_NONE, + DOMAIN_2 to DOMAIN_STATE_NONE, + )) + assertThat(service.queryValidVerificationPackageNames()).containsExactly(pkgName) + } + + private fun DomainVerificationService.getInfo(pkgName: String) = + getDomainVerificationInfo(pkgName) + .also { assertThat(it).isNotNull() }!! + + private fun DomainVerificationService.getUserState(pkgName: String) = + getDomainVerificationUserState(pkgName, USER_ID) + .also { assertThat(it).isNotNull() }!! + + private fun makeService(vararg pkgSettings: PackageSetting) = + makeService { pkgName -> pkgSettings.find { pkgName == it.getName()} } + + private fun makeService(pkgSettingFunction: (String) -> PackageSetting? = { null }) = + DomainVerificationService(mockThrowOnUnmocked { + // Assume the test has every permission necessary + whenever(enforcePermission(anyString(), anyInt(), anyInt(), anyString())) + whenever(checkPermission(anyString(), anyInt(), anyInt())) { + PackageManager.PERMISSION_GRANTED + } + }, mockThrowOnUnmocked { + whenever(linkedApps) { ArraySet<String>() } + }, mockThrowOnUnmocked { + whenever(isChangeEnabled(ArgumentMatchers.anyLong(), any())) { true } + }).apply { + setConnection(mockThrowOnUnmocked { + whenever(filterAppAccess(anyString(), anyInt(), anyInt())) { false } + whenever(doesUserExist(0)) { true } + whenever(doesUserExist(1)) { true } + whenever(scheduleWriteSettings()) + + // Need to provide an internal UID so some permission checks are ignored + whenever(callingUid) { Process.ROOT_UID } + whenever(callingUserId) { 0 } + + whenever(getPackageSettingLocked(anyString())) { + pkgSettingFunction(arguments[0] as String)!! + } + whenever(getPackageLocked(anyString())) { + pkgSettingFunction(arguments[0] as String)!!.getPkg() + } + }) + } + + private fun mockPkgSetting(pkgName: String, domainSetId: UUID, domains: List<String> = listOf( + DOMAIN_1, DOMAIN_2 + )) = mockThrowOnUnmocked<PackageSetting> { + val pkg = mockThrowOnUnmocked<AndroidPackage> { + whenever(packageName) { pkgName } + whenever(targetSdkVersion) { Build.VERSION_CODES.S } + + val activityList = listOf( + ParsedActivity().apply { + domains.forEach { + addIntent( + ParsedIntentInfo().apply { + autoVerify = true + addAction(Intent.ACTION_VIEW) + addCategory(Intent.CATEGORY_BROWSABLE) + addCategory(Intent.CATEGORY_DEFAULT) + addDataScheme("http") + addDataScheme("https") + addDataPath("/sub", PatternMatcher.PATTERN_LITERAL) + addDataAuthority(it, null) + } + ) + } + }, + ) + + whenever(activities) { activityList } + } + + whenever(getPkg()) { pkg } + whenever(getName()) { pkgName } + whenever(this.domainSetId) { domainSetId } + whenever(getInstantApp(anyInt())) { false } + whenever(firstInstallTime) { 0L } + } +} diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt index 6597577cf14f..f8fda12f806d 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt @@ -16,7 +16,7 @@ package com.android.server.pm.test.verify.domain -import android.content.pm.verify.domain.DomainVerificationManager +import android.content.pm.verify.domain.DomainVerificationState import android.util.ArrayMap import android.util.TypedXmlPullParser import android.util.TypedXmlSerializer @@ -117,11 +117,11 @@ class DomainVerificationPersistenceTest { @Test fun readMalformed() { val stateZero = mockEmptyPkgState(0).apply { - stateMap["example.com"] = DomainVerificationManager.STATE_SUCCESS - stateMap["example.org"] = DomainVerificationManager.STATE_FIRST_VERIFIER_DEFINED + stateMap["example.com"] = DomainVerificationState.STATE_SUCCESS + stateMap["example.org"] = DomainVerificationState.STATE_FIRST_VERIFIER_DEFINED // A domain without a written state falls back to default - stateMap["missing-state.com"] = DomainVerificationManager.STATE_NO_RESPONSE + stateMap["missing-state.com"] = DomainVerificationState.STATE_NO_RESPONSE userStates[1] = DomainVerificationInternalUserState(1).apply { addHosts(setOf("example-user1.com", "example-user1.org")) @@ -159,9 +159,9 @@ class DomainVerificationPersistenceTest { > <state> <domain name="example.com" state="${ - DomainVerificationManager.STATE_SUCCESS}"/> + DomainVerificationState.STATE_SUCCESS}"/> <domain name="example.org" state="${ - DomainVerificationManager.STATE_FIRST_VERIFIER_DEFINED}"/> + DomainVerificationState.STATE_FIRST_VERIFIER_DEFINED}"/> <not-domain name="not-domain.com" state="1"/> <domain name="missing-state.com"/> </state> diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationProxyTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationProxyTest.kt index 91e5beccee09..a9b77ea6d95b 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationProxyTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationProxyTest.kt @@ -21,20 +21,20 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager +import android.content.pm.verify.domain.DomainVerificationInfo import android.content.pm.verify.domain.DomainVerificationManager import android.content.pm.verify.domain.DomainVerificationRequest -import android.content.pm.verify.domain.DomainVerificationInfo import android.content.pm.verify.domain.DomainVerificationState import android.os.Bundle import android.os.UserHandle import android.util.ArraySet import com.android.server.DeviceIdleInternal +import com.android.server.pm.parsing.pkg.AndroidPackage import com.android.server.pm.verify.domain.DomainVerificationCollector import com.android.server.pm.verify.domain.DomainVerificationManagerInternal import com.android.server.pm.verify.domain.proxy.DomainVerificationProxy import com.android.server.pm.verify.domain.proxy.DomainVerificationProxyV1 import com.android.server.pm.verify.domain.proxy.DomainVerificationProxyV2 -import com.android.server.pm.parsing.pkg.AndroidPackage import com.android.server.testutils.mockThrowOnUnmocked import com.android.server.testutils.whenever import com.google.common.truth.Truth.assertThat @@ -106,20 +106,22 @@ class DomainVerificationProxyTest { when (val pkgName = arguments[0] as String) { TEST_PKG_NAME_TARGET_ONE -> DomainVerificationInfo( TEST_UUID_ONE, pkgName, mapOf( - "example1.com" to DomainVerificationManager.STATE_NO_RESPONSE, - "example2.com" to DomainVerificationManager.STATE_NO_RESPONSE + "example1.com" to DomainVerificationInfo.STATE_NO_RESPONSE, + "example2.com" to DomainVerificationInfo.STATE_NO_RESPONSE ) ) TEST_PKG_NAME_TARGET_TWO -> DomainVerificationInfo( TEST_UUID_TWO, pkgName, mapOf( - "example3.com" to DomainVerificationManager.STATE_NO_RESPONSE, - "example4.com" to DomainVerificationManager.STATE_NO_RESPONSE + "example3.com" to DomainVerificationInfo.STATE_NO_RESPONSE, + "example4.com" to DomainVerificationInfo.STATE_NO_RESPONSE ) ) else -> throw IllegalArgumentException("Unexpected package name $pkgName") } } - whenever(setDomainVerificationStatusInternal(anyInt(), any(), any(), anyInt())) + whenever(setDomainVerificationStatusInternal(anyInt(), any(), any(), anyInt())) { + DomainVerificationManager.STATUS_OK + } } collector = mockThrowOnUnmocked { whenever(collectValidAutoVerifyDomains(any())) { @@ -316,7 +318,7 @@ class DomainVerificationProxyTest { eq(TEST_CALLING_UID_ACCEPT), idCaptor.capture(), hostCaptor.capture(), - eq(DomainVerificationManager.STATE_SUCCESS) + eq(DomainVerificationState.STATE_SUCCESS) ) assertThat(idCaptor.allValues).containsExactly(TEST_UUID_ONE, TEST_UUID_TWO) diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationServiceUtil.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationServiceUtil.kt new file mode 100644 index 000000000000..6859b113298d --- /dev/null +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationServiceUtil.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.pm.test.verify.domain + +import android.annotation.UserIdInt +import com.android.server.pm.verify.domain.DomainVerificationService +import java.util.UUID + +fun DomainVerificationService.setStatus(domainSetId: UUID, domains: Set<String>, state: Int) = + setDomainVerificationStatus(domainSetId, domains.toMutableSet(), state) + +fun DomainVerificationService.setUserSelection( + domainSetId: UUID, + domains: Set<String>, + enabled: Boolean, + @UserIdInt userId: Int +) = setDomainVerificationUserSelection(domainSetId, domains.toMutableSet(), enabled, userId) diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt index 0d8f275be09c..377bae15e2d5 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt @@ -20,28 +20,27 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageUserState -import android.content.pm.verify.domain.DomainVerificationManager import android.content.pm.parsing.component.ParsedActivity import android.content.pm.parsing.component.ParsedIntentInfo +import android.content.pm.verify.domain.DomainVerificationState import android.os.Build import android.os.Process import android.util.ArraySet import android.util.SparseArray import com.android.server.pm.PackageSetting +import com.android.server.pm.parsing.pkg.AndroidPackage import com.android.server.pm.verify.domain.DomainVerificationManagerInternal import com.android.server.pm.verify.domain.DomainVerificationService import com.android.server.pm.verify.domain.proxy.DomainVerificationProxy -import com.android.server.pm.parsing.pkg.AndroidPackage import com.android.server.testutils.mockThrowOnUnmocked import com.android.server.testutils.spyThrowOnUnmocked import com.android.server.testutils.whenever import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized -import org.mockito.Mockito +import org.mockito.Mockito.any import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyLong -import org.mockito.Mockito.any import org.mockito.Mockito.anyString import org.mockito.Mockito.eq import org.mockito.Mockito.verify @@ -129,13 +128,13 @@ class DomainVerificationSettingsMutationTest { setDomainVerificationStatus( TEST_UUID, setOf("example.com"), - DomainVerificationManager.STATE_SUCCESS + DomainVerificationState.STATE_SUCCESS ) }, service("setStatusInternalPackageName") { setDomainVerificationStatusInternal( TEST_PKG, - DomainVerificationManager.STATE_SUCCESS, + DomainVerificationState.STATE_SUCCESS, ArraySet(setOf("example.com")) ) }, @@ -144,7 +143,7 @@ class DomainVerificationSettingsMutationTest { TEST_UID, TEST_UUID, setOf("example.com"), - DomainVerificationManager.STATE_SUCCESS + DomainVerificationState.STATE_SUCCESS ) }, service("setLinkHandlingAllowedUserId") { @@ -266,5 +265,7 @@ class DomainVerificationSettingsMutationTest { // This doesn't check for visibility; that's done in the enforcer test whenever(filterAppAccess(anyString(), anyInt(), anyInt())) { false } + whenever(doesUserExist(0)) { true } + whenever(doesUserExist(10)) { true } } } diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt index 0576125748fb..44c1b8f3fbb9 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt @@ -21,6 +21,7 @@ import android.content.pm.PackageManager import android.content.pm.parsing.component.ParsedActivity import android.content.pm.parsing.component.ParsedIntentInfo import android.content.pm.verify.domain.DomainVerificationManager +import android.content.pm.verify.domain.DomainVerificationState import android.content.pm.verify.domain.DomainVerificationUserState import android.os.Build import android.os.PatternMatcher @@ -74,6 +75,8 @@ class DomainVerificationUserStateOverrideTest { }).apply { setConnection(mockThrowOnUnmocked { whenever(filterAppAccess(anyString(), anyInt(), anyInt())) { false } + whenever(doesUserExist(0)) { true } + whenever(doesUserExist(1)) { true } whenever(scheduleWriteSettings()) // Need to provide an internal UID so some permission checks are ignored @@ -154,19 +157,20 @@ class DomainVerificationUserStateOverrideTest { .containsExactly(PKG_TWO) } - @Test(expected = IllegalArgumentException::class) + @Test fun anotherPackageTakeoverFailure() { val service = makeService() // Verify 1 to give it a higher approval level service.setDomainVerificationStatus(UUID_ONE, setOf(DOMAIN_ONE), - DomainVerificationManager.STATE_SUCCESS) + DomainVerificationState.STATE_SUCCESS) assertThat(service.stateFor(PKG_ONE, DOMAIN_ONE)).isEqualTo(STATE_VERIFIED) assertThat(service.getOwnersForDomain(DOMAIN_ONE, USER_ID).map { it.packageName }) .containsExactly(PKG_ONE) // Attempt override by package 2 - service.setDomainVerificationUserSelection(UUID_TWO, setOf(DOMAIN_ONE), true, USER_ID) + assertThat(service.setDomainVerificationUserSelection(UUID_TWO, setOf(DOMAIN_ONE), true, + USER_ID)).isEqualTo(DomainVerificationManager.ERROR_UNABLE_TO_APPROVE) } private fun DomainVerificationService.stateFor(pkgName: String, host: String) = |