diff options
| author | 2021-01-27 16:42:38 -0800 | |
|---|---|---|
| committer | 2021-02-09 10:19:54 -0800 | |
| commit | 6821adee46bf220d87505be2acf8631a5afc3d30 (patch) | |
| tree | 5face92f2760a3699fa2fa437bf963ad1adb5f56 | |
| parent | 1afc2cdb469aa97c8825becadfc5d44ebf7926c1 (diff) | |
Fix DomainVerificationEnforcer for app visibility
Cleans up the permission logic and introduces app filtering.
This will require non-user targeted APIs that hit a package name
to hold QUERY_ALL_PACKAGES. This ensures the caller can see the packages
without needing to check instant app state by passing a userId.
Currently it's not clear how to handle instant app visibility, but it's
assumed that the verification agent and settings have visibility.
Bug: 171251883
Test: atest DomainVerificationEnforcerTest
Change-Id: I3ffe2cc0307c9efa97dfcf8474a620622e7cfcfe
7 files changed, 584 insertions, 267 deletions
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 965f68bdcaf3..0027e9dd75cc 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -1747,6 +1747,11 @@ public class PackageManagerService extends IPackageManager.Stub public AndroidPackage getPackage(@NonNull String packageName) { return getPackageLocked(packageName); } + + @Override + public boolean filterAppAccess(String packageName, int callingUid, int userId) { + return mPmInternal.filterAppAccess(packageName, callingUid, userId); + } } /** @@ -16156,8 +16161,7 @@ public class PackageManagerService extends IPackageManager.Stub @Deprecated @Override public boolean updateIntentVerificationStatus(String packageName, int status, int userId) { - mDomainVerificationManager.setLegacyUserState(packageName, userId, status); - return true; + return mDomainVerificationManager.setLegacyUserState(packageName, userId, status); } @Deprecated diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index fb033e6594b8..3e76f5126774 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -105,9 +105,6 @@ import com.android.permission.persistence.RuntimePermissionsState; import com.android.server.LocalServices; import com.android.server.backup.PreferredActivityBackupHelper; import com.android.server.pm.Installer.InstallerException; -import com.android.server.pm.verify.domain.DomainVerificationLegacySettings; -import com.android.server.pm.verify.domain.DomainVerificationManagerInternal; -import com.android.server.pm.verify.domain.DomainVerificationPersistence; import com.android.server.pm.parsing.PackageInfoUtils; import com.android.server.pm.parsing.pkg.AndroidPackage; import com.android.server.pm.parsing.pkg.AndroidPackageUtils; @@ -115,6 +112,9 @@ import com.android.server.pm.permission.LegacyPermissionDataProvider; import com.android.server.pm.permission.LegacyPermissionSettings; import com.android.server.pm.permission.LegacyPermissionState; import com.android.server.pm.permission.LegacyPermissionState.PermissionState; +import com.android.server.pm.verify.domain.DomainVerificationLegacySettings; +import com.android.server.pm.verify.domain.DomainVerificationManagerInternal; +import com.android.server.pm.verify.domain.DomainVerificationPersistence; import com.android.server.utils.Snappable; import com.android.server.utils.TimingsTraceAndSlog; import com.android.server.utils.Watchable; @@ -2707,7 +2707,6 @@ public final class Settings implements Watchable, Snappable { writeSigningKeySetLPr(serializer, pkg.keySetData); writeUpgradeKeySetsLPr(serializer, pkg.keySetData); writeKeySetAliasesLPr(serializer, pkg.keySetData); - mDomainVerificationManager.writeLegacySettings(serializer, pkg.name); writeMimeGroupLPr(serializer, pkg.mimeGroups); serializer.endTag(null, "package"); 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 c521f828ade9..275dd053fdde 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 @@ -18,8 +18,10 @@ package com.android.server.pm.verify.domain; import android.Manifest; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.Context; +import android.content.pm.PackageManager; import android.os.Binder; import android.os.Process; @@ -30,10 +32,17 @@ public class DomainVerificationEnforcer { @NonNull private final Context mContext; + @NonNull + private Callback mCallback; + public DomainVerificationEnforcer(@NonNull Context context) { mContext = context; } + public void setCallback(@NonNull Callback callback) { + mCallback = callback; + } + /** * Enforced when mutating any state from shell or internally in the system process. */ @@ -67,6 +76,11 @@ public class DomainVerificationEnforcer { "Caller " + callingUid + " is not allowed to query domain verification state"); } + + mContext.enforcePermission(android.Manifest.permission.QUERY_ALL_PACKAGES, + Binder.getCallingPid(), callingUid, + "Caller " + callingUid + " does not hold " + + android.Manifest.permission.QUERY_ALL_PACKAGES); break; } } @@ -84,28 +98,42 @@ public class DomainVerificationEnforcer { isAllowed = true; break; default: - // TODO(b/159952358): Remove permission check? The component package should - // have been checked when the verifier component was first scanned in PMS. - mContext.enforcePermission( - android.Manifest.permission.DOMAIN_VERIFICATION_AGENT, - Binder.getCallingPid(), callingUid, - "Caller " + callingUid + " does not hold DOMAIN_VERIFICATION_AGENT"); + final int callingPid = Binder.getCallingPid(); + boolean isLegacyVerificationAgent = false; + if (mContext.checkPermission( + android.Manifest.permission.DOMAIN_VERIFICATION_AGENT, callingPid, + callingUid) != PackageManager.PERMISSION_GRANTED) { + isLegacyVerificationAgent = mContext.checkPermission( + android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT, + callingPid, callingUid) == PackageManager.PERMISSION_GRANTED; + if (!isLegacyVerificationAgent) { + throw new SecurityException("Caller " + callingUid + " does not hold " + + android.Manifest.permission.DOMAIN_VERIFICATION_AGENT); + } + } + + // If the caller isn't a legacy verifier, it needs the QUERY_ALL permission + if (!isLegacyVerificationAgent) { + mContext.enforcePermission(android.Manifest.permission.QUERY_ALL_PACKAGES, + callingPid, callingUid, "Caller " + callingUid + " does not hold " + + android.Manifest.permission.QUERY_ALL_PACKAGES); + } + isAllowed = proxy.isCallerVerifier(callingUid); break; } if (!isAllowed) { throw new SecurityException("Caller " + callingUid - + " is not the approved domain verification agent, isVerifier = " - + proxy.isCallerVerifier(callingUid)); + + " is not the approved domain verification agent"); } } /** * Enforced when mutating user selection state inside an exposed API method. */ - public void assertApprovedUserSelector(int callingUid, @UserIdInt int callingUserId, - @UserIdInt int targetUserId) throws SecurityException { + public boolean assertApprovedUserSelector(int callingUid, @UserIdInt int callingUserId, + @Nullable String packageName, @UserIdInt int targetUserId) throws SecurityException { if (callingUserId != targetUserId) { mContext.enforcePermission( Manifest.permission.INTERACT_ACROSS_USERS, @@ -117,12 +145,51 @@ public class DomainVerificationEnforcer { android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION, Binder.getCallingPid(), callingUid, "Caller is not allowed to edit user selections"); + + if (packageName == null) { + return true; + } + + return !mCallback.filterAppAccess(packageName, callingUid, targetUserId); } - public void callerIsLegacyUserSelector(int callingUid) { + public boolean callerIsLegacyUserSelector(int callingUid, @UserIdInt int callingUserId, + @NonNull String packageName, @UserIdInt int targetUserId) { mContext.enforcePermission( android.Manifest.permission.SET_PREFERRED_APPLICATIONS, Binder.getCallingPid(), callingUid, "Caller is not allowed to edit user state"); + + if (callingUserId != targetUserId) { + if (mContext.checkPermission( + Manifest.permission.INTERACT_ACROSS_USERS, + Binder.getCallingPid(), callingUid) != PackageManager.PERMISSION_GRANTED) { + // Legacy API did not enforce this, so for backwards compatibility, fail silently + return false; + } + } + + return !mCallback.filterAppAccess(packageName, callingUid, targetUserId); + } + + public boolean callerIsLegacyUserQuerent(int callingUid, @UserIdInt int callingUserId, + @NonNull String packageName, @UserIdInt int targetUserId) { + if (callingUserId != targetUserId) { + // The legacy API enforces the _FULL variant, so maintain that here + mContext.enforcePermission( + Manifest.permission.INTERACT_ACROSS_USERS_FULL, + Binder.getCallingPid(), callingUid, + "Caller is not allowed to edit other users"); + } + + return !mCallback.filterAppAccess(packageName, callingUid, targetUserId); + } + + public interface Callback { + /** + * @return true if access to the given package should be filtered and the method failed as + * if the package was not installed + */ + boolean filterAppAccess(@NonNull String packageName, int callingUid, @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 0474d78a3e53..50fd6e3ddea1 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 @@ -174,8 +174,10 @@ public interface DomainVerificationManagerInternal extends DomainVerificationMan * Set aside a legacy user selection that will be restored to a pending * {@link DomainVerificationPkgState} once it's added through * {@link #addPackage(PackageSetting)}. + * + * @return true if state changed successfully */ - void setLegacyUserState(@NonNull String packageName, @UserIdInt int userId, int state); + boolean setLegacyUserState(@NonNull String packageName, @UserIdInt int userId, int state); /** * Until the legacy APIs are entirely removed, returns the legacy state from the previously @@ -184,12 +186,6 @@ public interface DomainVerificationManagerInternal extends DomainVerificationMan int getLegacyState(@NonNull String packageName, @UserIdInt int userId); /** - * Serialize a legacy setting that wasn't attached yet. - * TODO: Does this even matter? Should consider for removal. - */ - void writeLegacySettings(TypedXmlSerializer serializer, String name); - - /** * Print the verification state and user selection state of a package. * * @param packageName the package whose state to change, or all packages if none is @@ -235,7 +231,8 @@ public interface DomainVerificationManagerInternal extends DomainVerificationMan throws IllegalArgumentException, NameNotFoundException; - interface Connection extends Function<String, PackageSetting> { + interface Connection extends DomainVerificationEnforcer.Callback, + Function<String, PackageSetting> { /** * Notify that a settings change has been made and that eventually 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 e24e5bbfa4f6..fa0327414914 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 @@ -47,12 +47,12 @@ import com.android.server.SystemConfig; import com.android.server.SystemService; import com.android.server.compat.PlatformCompat; import com.android.server.pm.PackageSetting; +import com.android.server.pm.parsing.pkg.AndroidPackage; import com.android.server.pm.verify.domain.models.DomainVerificationPkgState; import com.android.server.pm.verify.domain.models.DomainVerificationStateMap; import com.android.server.pm.verify.domain.models.DomainVerificationUserState; import com.android.server.pm.verify.domain.proxy.DomainVerificationProxy; import com.android.server.pm.verify.domain.proxy.DomainVerificationProxyUnavailable; -import com.android.server.pm.parsing.pkg.AndroidPackage; import org.xmlpull.v1.XmlPullParserException; @@ -92,9 +92,9 @@ public class DomainVerificationService extends SystemService * immediately attached once its available. * <p> * Generally this should be not accessed directly. Prefer calling {@link - * #getAndValidateAttachedLocked(UUID, Set, boolean)}. + * #getAndValidateAttachedLocked(UUID, Set, boolean, int, Integer)}. * - * @see #getAndValidateAttachedLocked(UUID, Set, boolean) + * @see #getAndValidateAttachedLocked(UUID, Set, boolean, int, Integer) **/ @GuardedBy("mLock") @NonNull @@ -160,6 +160,7 @@ public class DomainVerificationService extends SystemService @Override public void setConnection(@NonNull Connection connection) { mConnection = connection; + mEnforcer.setCallback(mConnection); } @NonNull @@ -285,7 +286,7 @@ public class DomainVerificationService extends SystemService mEnforcer.assertApprovedVerifier(callingUid, mProxy); synchronized (mLock) { DomainVerificationPkgState pkgState = getAndValidateAttachedLocked(domainSetId, domains, - true /* forAutoVerify */); + true /* forAutoVerify */, callingUid, null /* userId */); ArrayMap<String, Integer> stateMap = pkgState.getStateMap(); for (String domain : domains) { Integer previousState = stateMap.get(domain); @@ -389,8 +390,10 @@ public class DomainVerificationService extends SystemService public void setDomainVerificationLinkHandlingAllowed(@NonNull String packageName, boolean allowed, @UserIdInt int userId) throws NameNotFoundException { - mEnforcer.assertApprovedUserSelector(mConnection.getCallingUid(), - mConnection.getCallingUserId(), userId); + if (!mEnforcer.assertApprovedUserSelector(mConnection.getCallingUid(), + mConnection.getCallingUserId(), packageName, userId)) { + throw DomainVerificationUtils.throwPackageUnavailable(packageName); + } synchronized (mLock) { DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName); if (pkgState == null) { @@ -455,11 +458,18 @@ public class DomainVerificationService extends SystemService public void setDomainVerificationUserSelection(@NonNull UUID domainSetId, @NonNull Set<String> domains, boolean enabled, @UserIdInt int userId) throws InvalidDomainSetException, NameNotFoundException { - mEnforcer.assertApprovedUserSelector(mConnection.getCallingUid(), - mConnection.getCallingUserId(), userId); synchronized (mLock) { + final int callingUid = mConnection.getCallingUid(); + // Pass null for package name here and do the app visibility enforcement inside + // getAndValidateAttachedLocked instead, since this has to fail with the same invalid + // 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); + } DomainVerificationPkgState pkgState = getAndValidateAttachedLocked(domainSetId, domains, - false /* forAutoVerify */); + false /* forAutoVerify */, callingUid, userId); DomainVerificationUserState userState = pkgState.getOrCreateUserSelectionState(userId); if (enabled) { userState.addHosts(domains); @@ -556,8 +566,10 @@ public class DomainVerificationService extends SystemService @Override public DomainVerificationUserSelection getDomainVerificationUserSelection( @NonNull String packageName, @UserIdInt int userId) throws NameNotFoundException { - mEnforcer.assertApprovedUserSelector(mConnection.getCallingUid(), - mConnection.getCallingUserId(), userId); + if (!mEnforcer.assertApprovedUserSelector(mConnection.getCallingUid(), + mConnection.getCallingUserId(), packageName, userId)) { + throw DomainVerificationUtils.throwPackageUnavailable(packageName); + } synchronized (mLock) { DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName); if (pkgState == null) { @@ -844,23 +856,27 @@ public class DomainVerificationService extends SystemService } @Override - public void setLegacyUserState(@NonNull String packageName, @UserIdInt int userId, int state) { - mEnforcer.callerIsLegacyUserSelector(mConnection.getCallingUid()); + public boolean setLegacyUserState(@NonNull String packageName, @UserIdInt int userId, + int state) { + if (!mEnforcer.callerIsLegacyUserSelector(mConnection.getCallingUid(), + mConnection.getCallingUserId(), packageName, userId)) { + return false; + } mLegacySettings.add(packageName, userId, state); mConnection.scheduleWriteSettings(); + return true; } @Override public int getLegacyState(@NonNull String packageName, @UserIdInt int userId) { + if (!mEnforcer.callerIsLegacyUserQuerent(mConnection.getCallingUid(), + mConnection.getCallingUserId(), packageName, userId)) { + return PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; + } return mLegacySettings.getUserState(packageName, userId); } @Override - public void writeLegacySettings(TypedXmlSerializer serializer, String name) { - - } - - @Override public void clearPackage(@NonNull String packageName) { synchronized (mLock) { mAttachedPkgStates.remove(packageName); @@ -935,10 +951,14 @@ public class DomainVerificationService extends SystemService * Validates parameters provided by an external caller. Checks that an ID is still live and that * any provided domains are valid. Should be called at the beginning of each API that takes in a * {@link UUID} domain set ID. + * + * @param userIdForFilter which user to filter app access to, or null if the caller has already + * validated package visibility */ @GuardedBy("mLock") private DomainVerificationPkgState getAndValidateAttachedLocked(@NonNull UUID domainSetId, - @NonNull Set<String> domains, boolean forAutoVerify) + @NonNull Set<String> domains, boolean forAutoVerify, int callingUid, + @Nullable Integer userIdForFilter) throws InvalidDomainSetException, NameNotFoundException { if (domainSetId == null) { throw new InvalidDomainSetException(null, null, @@ -952,6 +972,13 @@ public class DomainVerificationService extends SystemService } String pkgName = pkgState.getPackageName(); + + if (userIdForFilter != null + && mConnection.filterAppAccess(pkgName, callingUid, userIdForFilter)) { + throw new InvalidDomainSetException(domainSetId, null, + InvalidDomainSetException.REASON_ID_INVALID); + } + PackageSetting pkgSetting = mConnection.getPackageSettingLocked(pkgName); if (pkgSetting == null || pkgSetting.getPkg() == null) { throw DomainVerificationUtils.throwPackageUnavailable(pkgName); 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 d863194d6889..c2e0b776fc60 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 @@ -18,6 +18,7 @@ package com.android.server.pm.test.verify.domain 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 @@ -25,7 +26,6 @@ import android.content.pm.parsing.component.ParsedIntentInfo import android.os.Build import android.os.Process import android.util.ArraySet -import android.util.Singleton import android.util.SparseArray import androidx.test.platform.app.InstrumentationRegistry import com.android.server.pm.PackageSetting @@ -46,14 +46,11 @@ import org.mockito.Mockito.anyLong import org.mockito.Mockito.anyString import org.mockito.Mockito.eq import org.mockito.Mockito.verifyNoMoreInteractions -import org.testng.Assert.assertThrows import java.io.File import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger -private typealias Enforcer = DomainVerificationEnforcer - @RunWith(Parameterized::class) class DomainVerificationEnforcerTest { @@ -64,64 +61,27 @@ class DomainVerificationEnforcerTest { private const val VERIFIER_UID = Process.FIRST_APPLICATION_UID + 1 private const val NON_VERIFIER_UID = Process.FIRST_APPLICATION_UID + 2 - private const val TEST_PKG = "com.test" + private const val VISIBLE_PKG = "com.test.visible" + private val VISIBLE_UUID = UUID.fromString("8db01272-270d-4606-a3db-bb35228ff9a2") + private const val INVISIBLE_PKG = "com.test.invisible" + private val INVISIBLE_UUID = UUID.fromString("16dcb029-d96c-4a19-833a-4c9d72e2ebc3") @JvmStatic @Parameterized.Parameters(name = "{0}") fun parameters(): Array<Any> { - val makeEnforcer: (Context) -> DomainVerificationEnforcer = { - DomainVerificationEnforcer(it) - } + val visiblePkg = mockPkg(VISIBLE_PKG) + val visiblePkgSetting = mockPkgSetting(VISIBLE_PKG, VISIBLE_UUID) + val invisiblePkg = mockPkg(INVISIBLE_PKG) + val invisiblePkgSetting = mockPkgSetting(INVISIBLE_PKG, INVISIBLE_UUID) - val mockPkg = mockThrowOnUnmocked<AndroidPackage> { - whenever(packageName) { TEST_PKG } - whenever(targetSdkVersion) { Build.VERSION_CODES.S } - whenever(activities) { - listOf( - ParsedActivity().apply { - addIntent( - ParsedIntentInfo().apply { - autoVerify = true - addAction(Intent.ACTION_VIEW) - addCategory(Intent.CATEGORY_BROWSABLE) - addCategory(Intent.CATEGORY_DEFAULT) - addDataScheme("https") - addDataAuthority("example.com", null) - } - ) + val makeEnforcer: (Context) -> DomainVerificationEnforcer = { + DomainVerificationEnforcer(it).apply { + setCallback(mockThrowOnUnmocked { + whenever(filterAppAccess(eq(VISIBLE_PKG), anyInt(), anyInt())) { false } + whenever(filterAppAccess(eq(INVISIBLE_PKG), anyInt(), anyInt())) { + true } - ) - } - } - - val uuid = UUID.randomUUID() - - // TODO: PackageSetting field encapsulation to move to whenever(name) - val mockPkgSetting = spyThrowOnUnmocked( - PackageSetting( - TEST_PKG, - TEST_PKG, - File("/test"), - null, - null, - null, - null, - 1, - 0, - 0, - 0, - null, - null, - null, - uuid - ) - ) { - whenever(getPkg()) { mockPkg } - whenever(domainSetId) { uuid } - whenever(userState) { - SparseArray<PackageUserState>().apply { - this[0] = PackageUserState() - } + }) } } @@ -129,142 +89,160 @@ class DomainVerificationEnforcerTest { { val callingUidInt = AtomicInteger(-1) val callingUserIdInt = AtomicInteger(-1) - Triple( - callingUidInt, callingUserIdInt, DomainVerificationService( - it, - mockThrowOnUnmocked { whenever(linkedApps) { ArraySet<String>() } }, - mockThrowOnUnmocked { - whenever( - isChangeEnabled( - anyLong(), - any() - ) - ) { true } - }).apply { - setConnection(mockThrowOnUnmocked { - whenever(callingUid) { callingUidInt.get() } - whenever(callingUserId) { callingUserIdInt.get() } - whenever(getPackageSettingLocked(TEST_PKG)) { mockPkgSetting } - whenever(getPackageLocked(TEST_PKG)) { mockPkg } - whenever(schedule(anyInt(), any())) - whenever(scheduleWriteSettings()) - }) + + 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) + } + + Triple(callingUidInt, callingUserIdInt, service) } fun enforcer( type: Type, name: String, - block: DomainVerificationEnforcer.( - callingUid: Int, callingUserId: Int, userId: Int, proxy: DomainVerificationProxy - ) -> Unit - ) = Params( - type, - makeEnforcer, - name - ) { enforcer, callingUid, callingUserId, userId, proxy -> - enforcer.block(callingUid, callingUserId, userId, proxy) + block: DomainVerificationEnforcer.(Params.Input<DomainVerificationEnforcer>) -> Any? + ) = Params(type, makeEnforcer, name) { + it.target.block(it) } fun service( type: Type, name: String, - block: DomainVerificationService.( - callingUid: Int, callingUserId: Int, userId: Int - ) -> Unit - ) = Params( - type, - makeService, - name - ) { uidAndUserIdAndService, callingUid, callingUserId, userId, proxy -> - val (callingUidInt, callingUserIdInt, service) = uidAndUserIdAndService - callingUidInt.set(callingUid) - callingUserIdInt.set(callingUserId) - service.setProxy(proxy) - service.addPackage(mockPkgSetting) - service.block(callingUid, callingUserId, userId) + block: DomainVerificationService.(Params.Input<Triple<AtomicInteger, AtomicInteger, DomainVerificationService>>) -> Any? + ) = Params(type, makeService, name) { + val (callingUidInt, callingUserIdInt, service) = it.target + callingUidInt.set(it.callingUid) + callingUserIdInt.set(it.callingUserId) + service.proxy = it.proxy + service.addPackage(visiblePkgSetting) + service.addPackage(invisiblePkgSetting) + service.block(it) } return arrayOf( - enforcer(Type.INTERNAL, "internal") { callingUid, _, _, _ -> - assertInternal(callingUid) + enforcer(Type.INTERNAL, "internal") { + assertInternal(it.callingUid) }, - enforcer(Type.QUERENT, "approvedQuerent") { callingUid, _, _, proxy -> - assertApprovedQuerent(callingUid, proxy) + enforcer(Type.QUERENT, "approvedQuerent") { + assertApprovedQuerent(it.callingUid, it.proxy) }, - enforcer(Type.VERIFIER, "approvedVerifier") { callingUid, _, _, proxy -> - assertApprovedVerifier(callingUid, proxy) + enforcer(Type.VERIFIER, "approvedVerifier") { + assertApprovedVerifier(it.callingUid, it.proxy) }, enforcer( Type.SELECTOR, "approvedUserSelector" - ) { callingUid, callingUserId, userId, _ -> - assertApprovedUserSelector(callingUid, callingUserId, userId) + ) { + assertApprovedUserSelector( + it.callingUid, it.callingUserId, + it.targetPackageName, it.userId + ) }, - - service(Type.INTERNAL, "setStatusInternalPackageName") { _, _, _ -> + service(Type.INTERNAL, "setStatusInternalPackageName") { setDomainVerificationStatusInternal( - TEST_PKG, + it.targetPackageName, DomainVerificationManager.STATE_SUCCESS, ArraySet(setOf("example.com")) ) }, - service(Type.INTERNAL, "setUserSelectionInternal") { _, _, userId -> + service(Type.INTERNAL, "setUserSelectionInternal") { setDomainVerificationUserSelectionInternal( - userId, - TEST_PKG, + it.userId, + it.targetPackageName, false, ArraySet(setOf("example.com")) ) }, - service(Type.INTERNAL, "verifyPackages") { _, _, _ -> - verifyPackages(listOf(TEST_PKG), true) + service(Type.INTERNAL, "verifyPackages") { + verifyPackages(listOf(it.targetPackageName), true) }, - service(Type.INTERNAL, "clearState") { _, _, _ -> - clearDomainVerificationState(listOf(TEST_PKG)) + service(Type.INTERNAL, "clearState") { + clearDomainVerificationState(listOf(it.targetPackageName)) }, - service(Type.INTERNAL, "clearUserSelections") { _, _, userId -> - clearUserSelections(listOf(TEST_PKG), userId) + service(Type.INTERNAL, "clearUserSelections") { + clearUserSelections(listOf(it.targetPackageName), it.userId) }, - service(Type.VERIFIER, "getPackageNames") { _, _, _ -> + service(Type.VERIFIER, "getPackageNames") { validVerificationPackageNames }, - service(Type.QUERENT, "getInfo") { _, _, _ -> - getDomainVerificationInfo(TEST_PKG) + service(Type.QUERENT, "getInfo") { + getDomainVerificationInfo(it.targetPackageName) }, - service(Type.VERIFIER, "setStatus") { _, _, _ -> + service(Type.VERIFIER, "setStatus") { setDomainVerificationStatus( - uuid, + it.targetDomainSetId, setOf("example.com"), DomainVerificationManager.STATE_SUCCESS ) }, - service(Type.VERIFIER, "setStatusInternalUid") { callingUid, _, _ -> + service(Type.VERIFIER, "setStatusInternalUid") { setDomainVerificationStatusInternal( - callingUid, - uuid, + it.callingUid, + it.targetDomainSetId, setOf("example.com"), DomainVerificationManager.STATE_SUCCESS ) }, - service(Type.SELECTOR, "setLinkHandlingAllowed") { _, _, _ -> - setDomainVerificationLinkHandlingAllowed(TEST_PKG, true) + service(Type.SELECTOR, "setLinkHandlingAllowed") { + setDomainVerificationLinkHandlingAllowed(it.targetPackageName, true) }, - service(Type.SELECTOR_USER, "setLinkHandlingAllowedUserId") { _, _, userId -> - setDomainVerificationLinkHandlingAllowed(TEST_PKG, true, userId) + service(Type.SELECTOR_USER, "setLinkHandlingAllowedUserId") { + setDomainVerificationLinkHandlingAllowed(it.targetPackageName, true, it.userId) }, - service(Type.SELECTOR, "getUserSelection") { _, _, _ -> - getDomainVerificationUserSelection(TEST_PKG) + service(Type.SELECTOR, "getUserSelection") { + getDomainVerificationUserSelection(it.targetPackageName) }, - service(Type.SELECTOR_USER, "getUserSelectionUserId") { _, _, userId -> - getDomainVerificationUserSelection(TEST_PKG, userId) + service(Type.SELECTOR_USER, "getUserSelectionUserId") { + getDomainVerificationUserSelection(it.targetPackageName, it.userId) }, - service(Type.SELECTOR, "setUserSelection") { _, _, _ -> - setDomainVerificationUserSelection(uuid, setOf("example.com"), true) + service(Type.SELECTOR, "setUserSelection") { + setDomainVerificationUserSelection( + it.targetDomainSetId, + setOf("example.com"), + true + ) + }, + service(Type.SELECTOR_USER, "setUserSelectionUserId") { + setDomainVerificationUserSelection( + it.targetDomainSetId, + setOf("example.com"), + true, + it.userId + ) }, - service(Type.SELECTOR_USER, "setUserSelectionUserId") { _, _, userId -> - setDomainVerificationUserSelection(uuid, setOf("example.com"), true, userId) + service(Type.LEGACY_SELECTOR, "setLegacyUserState") { + setLegacyUserState( + it.targetPackageName, it.userId, + PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER + ) + }, + service(Type.LEGACY_QUERENT, "getLegacyUserState") { + getLegacyState(it.targetPackageName, it.userId) }, ) } @@ -273,9 +251,7 @@ class DomainVerificationEnforcerTest { val type: Type, val construct: (context: Context) -> T, val name: String, - private val method: ( - T, callingUid: Int, callingUserId: Int, userId: Int, proxy: DomainVerificationProxy - ) -> Unit + private val method: (Input<T>) -> Any? ) { override fun toString() = "${type}_$name" @@ -284,10 +260,79 @@ class DomainVerificationEnforcerTest { callingUid: Int, callingUserId: Int, userId: Int, + targetPackageName: String, + targetDomainSetId: UUID, proxy: DomainVerificationProxy - ) { - @Suppress("UNCHECKED_CAST") - method(target as T, callingUid, callingUserId, userId, proxy) + ): Any? = method( + Input( + @Suppress("UNCHECKED_CAST") + target as T, + callingUid, + callingUserId, + userId, + targetPackageName, + targetDomainSetId, + proxy + ) + ) + + data class Input<T>( + val target: T, + val callingUid: Int, + val callingUserId: Int, + val userId: Int, + val targetPackageName: String, + val targetDomainSetId: UUID, + val proxy: DomainVerificationProxy + ) + } + + fun mockPkg(packageName: String) = mockThrowOnUnmocked<AndroidPackage> { + whenever(this.packageName) { packageName } + whenever(targetSdkVersion) { Build.VERSION_CODES.S } + whenever(activities) { + listOf( + ParsedActivity().apply { + addIntent( + ParsedIntentInfo().apply { + autoVerify = true + addAction(Intent.ACTION_VIEW) + addCategory(Intent.CATEGORY_BROWSABLE) + addCategory(Intent.CATEGORY_DEFAULT) + addDataScheme("https") + addDataAuthority("example.com", null) + } + ) + } + ) + } + } + + fun mockPkgSetting(packageName: String, domainSetId: UUID) = spyThrowOnUnmocked( + PackageSetting( + packageName, + packageName, + File("/test"), + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + null, + domainSetId + ) + ) { + whenever(getPkg()) { mockPkg(packageName) } + whenever(this.domainSetId) { domainSetId } + whenever(userState) { + SparseArray<PackageUserState>().apply { + this[0] = PackageUserState() + } } } } @@ -309,6 +354,8 @@ class DomainVerificationEnforcerTest { Type.VERIFIER -> approvedVerifier() Type.SELECTOR -> approvedUserSelector(verifyCrossUser = false) Type.SELECTOR_USER -> approvedUserSelector(verifyCrossUser = true) + Type.LEGACY_QUERENT -> legacyQuerent() + Type.LEGACY_SELECTOR -> legacyUserSelector() }.run { /*exhaust*/ } } @@ -316,24 +363,30 @@ class DomainVerificationEnforcerTest { val context: Context = mockThrowOnUnmocked() val target = params.construct(context) - INTERNAL_UIDS.forEach { runMethod(target, it) } - assertThrows(SecurityException::class.java) { runMethod(target, VERIFIER_UID) } - assertThrows(SecurityException::class.java) { runMethod(target, NON_VERIFIER_UID) } + // Internal doesn't care about visibility + listOf(true, false).forEach { visible -> + INTERNAL_UIDS.forEach { runMethod(target, it, visible) } + assertFails { runMethod(target, VERIFIER_UID, visible) } + assertFails { + runMethod(target, NON_VERIFIER_UID, visible) + } + } } fun approvedQuerent() { val allowUserSelection = AtomicBoolean(false) + val allowPreferredApps = AtomicBoolean(false) + val allowQueryAll = AtomicBoolean(false) val context: Context = mockThrowOnUnmocked { - whenever( - enforcePermission( - eq(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION), - anyInt(), anyInt(), anyString() - ) - ) { - if (!allowUserSelection.get()) { - throw SecurityException() - } - } + initPermission( + allowUserSelection, + android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION + ) + initPermission( + allowPreferredApps, + android.Manifest.permission.SET_PREFERRED_APPLICATIONS + ) + initPermission(allowQueryAll, android.Manifest.permission.QUERY_ALL_PACKAGES) } val target = params.construct(context) @@ -341,27 +394,41 @@ class DomainVerificationEnforcerTest { verifyNoMoreInteractions(context) + assertFails { runMethod(target, VERIFIER_UID) } + assertFails { runMethod(target, NON_VERIFIER_UID) } + + // Check that the verifier only needs QUERY_ALL to pass + allowQueryAll.set(true) runMethod(target, VERIFIER_UID) - assertThrows(SecurityException::class.java) { runMethod(target, NON_VERIFIER_UID) } + allowQueryAll.set(false) + + allowPreferredApps.set(true) + + assertFails { runMethod(target, NON_VERIFIER_UID) } allowUserSelection.set(true) + assertFails { runMethod(target, NON_VERIFIER_UID) } + + allowQueryAll.set(true) + runMethod(target, NON_VERIFIER_UID) } fun approvedVerifier() { - val shouldThrow = AtomicBoolean(false) + val allowDomainVerificationAgent = AtomicBoolean(false) + val allowIntentVerificationAgent = AtomicBoolean(false) + val allowQueryAll = AtomicBoolean(false) val context: Context = mockThrowOnUnmocked { - whenever( - enforcePermission( - eq(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT), - anyInt(), anyInt(), anyString() - ) - ) { - if (shouldThrow.get()) { - throw SecurityException() - } - } + initPermission( + allowDomainVerificationAgent, + android.Manifest.permission.DOMAIN_VERIFICATION_AGENT + ) + initPermission( + allowIntentVerificationAgent, + android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT + ) + initPermission(allowQueryAll, android.Manifest.permission.QUERY_ALL_PACKAGES) } val target = params.construct(context) @@ -369,94 +436,241 @@ class DomainVerificationEnforcerTest { verifyNoMoreInteractions(context) + assertFails { runMethod(target, VERIFIER_UID) } + assertFails { runMethod(target, NON_VERIFIER_UID) } + + allowDomainVerificationAgent.set(true) + + assertFails { runMethod(target, VERIFIER_UID) } + assertFails { runMethod(target, NON_VERIFIER_UID) } + + allowQueryAll.set(true) + runMethod(target, VERIFIER_UID) - assertThrows(SecurityException::class.java) { runMethod(target, NON_VERIFIER_UID) } + assertFails { runMethod(target, NON_VERIFIER_UID) } - shouldThrow.set(true) + // Check that v1 verifiers are also allowed through + allowDomainVerificationAgent.set(false) + allowIntentVerificationAgent.set(true) - assertThrows(SecurityException::class.java) { runMethod(target, VERIFIER_UID) } - assertThrows(SecurityException::class.java) { runMethod(target, NON_VERIFIER_UID) } + runMethod(target, VERIFIER_UID) + assertFails { runMethod(target, NON_VERIFIER_UID) } } fun approvedUserSelector(verifyCrossUser: Boolean) { - val allowUserSelection = AtomicBoolean(true) - val allowInteractAcrossUsers = AtomicBoolean(true) + val allowUserSelection = AtomicBoolean(false) + val allowInteractAcrossUsers = AtomicBoolean(false) val context: Context = mockThrowOnUnmocked { - whenever( - enforcePermission( - eq(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION), - anyInt(), anyInt(), anyString() - ) - ) { - if (!allowUserSelection.get()) { - throw SecurityException() + initPermission( + allowUserSelection, + android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION + ) + initPermission( + allowInteractAcrossUsers, + android.Manifest.permission.INTERACT_ACROSS_USERS + ) + } + val target = params.construct(context) + + 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) + } } - } - whenever( - enforcePermission( - eq(android.Manifest.permission.INTERACT_ACROSS_USERS), - anyInt(), anyInt(), anyString() - ) - ) { - if (!allowInteractAcrossUsers.get()) { - throw SecurityException() + } else { + allUids.forEach { + runMethod(target, it, visible = true, callingUserId, targetUserId) } } - } - val target = params.construct(context) - fun runEachTestCaseWrapped( - callingUserId: Int, - targetUserId: Int, - block: (testCase: () -> Unit) -> Unit = { it.invoke() } - ) { - block { runMethod(target, VERIFIER_UID, callingUserId, targetUserId) } - block { runMethod(target, NON_VERIFIER_UID, 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) + } + } } val callingUserId = 0 val notCallingUserId = 1 - runEachTestCaseWrapped(callingUserId, callingUserId) + runTestCases(callingUserId, callingUserId, throws = true) if (verifyCrossUser) { - runEachTestCaseWrapped(callingUserId, notCallingUserId) + runTestCases(callingUserId, notCallingUserId, throws = true) } - allowInteractAcrossUsers.set(false) + allowUserSelection.set(true) + + runTestCases(callingUserId, callingUserId, throws = false) + if (verifyCrossUser) { + runTestCases(callingUserId, notCallingUserId, throws = true) + } - runEachTestCaseWrapped(callingUserId, callingUserId) + allowInteractAcrossUsers.set(true) + runTestCases(callingUserId, callingUserId, throws = false) if (verifyCrossUser) { - runEachTestCaseWrapped(callingUserId, notCallingUserId) { - assertThrows(SecurityException::class.java, it) + runTestCases(callingUserId, notCallingUserId, throws = false) + } + } + + private fun legacyUserSelector() { + val allowInteractAcrossUsers = AtomicBoolean(false) + val allowPreferredApps = AtomicBoolean(false) + val context: Context = mockThrowOnUnmocked { + initPermission( + allowInteractAcrossUsers, + android.Manifest.permission.INTERACT_ACROSS_USERS + ) + initPermission( + allowPreferredApps, + android.Manifest.permission.SET_PREFERRED_APPLICATIONS + ) + } + 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 doesn't use QUERY_ALL, so the invisible package should always fail + allUids.forEach { + assertFails { + runMethod(target, it, visible = false, callingUserId, targetUserId) + } } } - allowUserSelection.set(false) + val callingUserId = 0 + val notCallingUserId = 1 + + runTestCases(callingUserId, callingUserId, throws = true) + runTestCases(callingUserId, notCallingUserId, throws = true) + + allowPreferredApps.set(true) - runEachTestCaseWrapped(callingUserId, callingUserId) { - assertThrows(SecurityException::class.java, it) + runTestCases(callingUserId, callingUserId, throws = false) + runTestCases(callingUserId, notCallingUserId, throws = true) + + allowInteractAcrossUsers.set(true) + + runTestCases(callingUserId, callingUserId, throws = false) + runTestCases(callingUserId, notCallingUserId, throws = false) + } + + private fun legacyQuerent() { + val allowInteractAcrossUsers = AtomicBoolean(false) + val allowInteractAcrossUsersFull = AtomicBoolean(false) + val context: Context = mockThrowOnUnmocked { + initPermission( + allowInteractAcrossUsers, + android.Manifest.permission.INTERACT_ACROSS_USERS + ) + initPermission( + allowInteractAcrossUsersFull, + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL + ) } - if (verifyCrossUser) { - runEachTestCaseWrapped(callingUserId, notCallingUserId) { - assertThrows(SecurityException::class.java, it) + 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 doesn't use QUERY_ALL, so the invisible package should always fail + allUids.forEach { + assertFails { + runMethod(target, it, visible = false, callingUserId, targetUserId) + } } } + val callingUserId = 0 + val notCallingUserId = 1 + + runTestCases(callingUserId, callingUserId, throws = false) + runTestCases(callingUserId, notCallingUserId, throws = true) + + // Legacy requires the _FULL permission, so this should continue to fail allowInteractAcrossUsers.set(true) + runTestCases(callingUserId, callingUserId, throws = false) + runTestCases(callingUserId, notCallingUserId, throws = true) + + allowInteractAcrossUsersFull.set(true) + runTestCases(callingUserId, callingUserId, throws = false) + runTestCases(callingUserId, notCallingUserId, throws = false) + } - runEachTestCaseWrapped(callingUserId, callingUserId) { - assertThrows(SecurityException::class.java, it) + private fun Context.initPermission(boolean: AtomicBoolean, permission: String) { + whenever(enforcePermission(eq(permission), anyInt(), anyInt(), anyString())) { + if (!boolean.get()) { + throw SecurityException() + } } - if (verifyCrossUser) { - runEachTestCaseWrapped(callingUserId, notCallingUserId) { - assertThrows(SecurityException::class.java, it) + whenever(checkPermission(eq(permission), anyInt(), anyInt())) { + if (boolean.get()) { + PackageManager.PERMISSION_GRANTED + } else { + PackageManager.PERMISSION_DENIED } } } - private fun runMethod(target: Any, callingUid: Int, callingUserId: Int = 0, userId: Int = 0) { - params.runMethod(target, callingUid, callingUserId, userId, proxy) + private fun runMethod( + target: Any, + callingUid: Int, + visible: Boolean = true, + callingUserId: Int = 0, + userId: Int = 0 + ): Any? { + val packageName = if (visible) VISIBLE_PKG else INVISIBLE_PKG + val uuid = if (visible) VISIBLE_UUID else INVISIBLE_UUID + return params.runMethod(target, callingUid, callingUserId, userId, packageName, uuid, proxy) + } + + 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") + } + } + } catch (e: SecurityException) { + } catch (e: PackageManager.NameNotFoundException) { + } catch (e: DomainVerificationManager.InvalidDomainSetException) { + // Any of these 3 exceptions are considered failures, which is expected + } } enum class Type { @@ -473,6 +687,12 @@ class DomainVerificationEnforcerTest { SELECTOR, // Holding the user setting permission, but targeting cross user - SELECTOR_USER + SELECTOR_USER, + + // Legacy required no permissions except when cross-user + LEGACY_QUERENT, + + // Holding the legacy preferred apps permission + LEGACY_SELECTOR } } 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 5792e02e37a2..5629d1c1107d 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 @@ -267,5 +267,8 @@ class DomainVerificationSettingsMutationTest { whenever(getPackageLocked(TEST_PKG)) { mockPkg() } whenever(schedule(anyInt(), any())) whenever(scheduleWriteSettings()) + + // This doesn't check for visibility; that's done in the enforcer test + whenever(filterAppAccess(anyString(), anyInt(), anyInt())) { false } } } |