diff --git a/core/java/com/android/internal/compat/OverrideAllowedState.java b/core/java/com/android/internal/compat/OverrideAllowedState.java index 9a78ad2011cf4..c0bbe5082131b 100644 --- a/core/java/com/android/internal/compat/OverrideAllowedState.java +++ b/core/java/com/android/internal/compat/OverrideAllowedState.java @@ -33,7 +33,7 @@ public final class OverrideAllowedState implements Parcelable { DISABLED_NOT_DEBUGGABLE, DISABLED_NON_TARGET_SDK, DISABLED_TARGET_SDK_TOO_HIGH, - PACKAGE_DOES_NOT_EXIST, + DEFERRED_VERIFICATION, LOGGING_ONLY_CHANGE }) @Retention(RetentionPolicy.SOURCE) @@ -57,10 +57,10 @@ public final class OverrideAllowedState implements Parcelable { * Change cannot be overridden, due to the app's targetSdk being above the change's targetSdk. */ public static final int DISABLED_TARGET_SDK_TOO_HIGH = 3; - /** - * Package does not exist. + /** + * Change override decision is currently being deferred, due to the app not being installed yet. */ - public static final int PACKAGE_DOES_NOT_EXIST = 4; + public static final int DEFERRED_VERIFICATION = 4; /** * Change is marked as logging only, and cannot be toggled. */ @@ -106,6 +106,7 @@ public final class OverrideAllowedState implements Parcelable { throws SecurityException { switch (state) { case ALLOWED: + case DEFERRED_VERIFICATION: return; case DISABLED_NOT_DEBUGGABLE: throw new SecurityException( @@ -118,11 +119,6 @@ public final class OverrideAllowedState implements Parcelable { "Cannot override %1$d for %2$s because the app's targetSdk (%3$d) is " + "above the change's targetSdk threshold (%4$d)", changeId, packageName, appTargetSdk, changeIdTargetSdk)); - case PACKAGE_DOES_NOT_EXIST: - throw new SecurityException(String.format( - "Cannot override %1$d for %2$s because the package does not exist, and " - + "the change is targetSdk gated.", - changeId, packageName)); case LOGGING_ONLY_CHANGE: throw new SecurityException(String.format( "Cannot override %1$d because it is marked as a logging-only change.", @@ -170,8 +166,8 @@ public final class OverrideAllowedState implements Parcelable { return "DISABLED_NON_TARGET_SDK"; case DISABLED_TARGET_SDK_TOO_HIGH: return "DISABLED_TARGET_SDK_TOO_HIGH"; - case PACKAGE_DOES_NOT_EXIST: - return "PACKAGE_DOES_NOT_EXIST"; + case DEFERRED_VERIFICATION: + return "DEFERRED_VERIFICATION"; case LOGGING_ONLY_CHANGE: return "LOGGING_ONLY_CHANGE"; } diff --git a/services/core/java/com/android/server/compat/CompatChange.java b/services/core/java/com/android/server/compat/CompatChange.java index a8aa9aada607a..c4ff99bae6947 100644 --- a/services/core/java/com/android/server/compat/CompatChange.java +++ b/services/core/java/com/android/server/compat/CompatChange.java @@ -61,6 +61,7 @@ public final class CompatChange extends CompatibilityChangeInfo { ChangeListener mListener = null; private Map mPackageOverrides; + private Map mDeferredOverrides; public CompatChange(long changeId) { this(changeId, null, -1, -1, false, false, null); @@ -120,6 +121,56 @@ public final class CompatChange extends CompatibilityChangeInfo { notifyListener(pname); } + /** + * Tentatively set the state of this change for a given package name. + * The override will only take effect after that package is installed, if applicable. + * + *

Note, this method is not thread safe so callers must ensure thread safety. + * + * @param packageName Package name to tentatively enable the change for. + * @param enabled Whether or not to enable the change. + */ + void addPackageDeferredOverride(String packageName, boolean enabled) { + if (getLoggingOnly()) { + throw new IllegalArgumentException( + "Can't add overrides for a logging only change " + toString()); + } + if (mDeferredOverrides == null) { + mDeferredOverrides = new HashMap<>(); + } + mDeferredOverrides.put(packageName, enabled); + } + + /** + * Rechecks an existing (and possibly deferred) override. + * + *

For deferred overrides, check if they can be promoted to a regular override. For regular + * overrides, check if they need to be demoted to deferred.

+ * + * @param packageName Package name to apply deferred overrides for. + * @param allowed Whether the override is allowed. + * + * @return {@code true} if the recheck yielded a result that requires invalidating caches + * (a deferred override was consolidated or a regular override was removed). + */ + boolean recheckOverride(String packageName, boolean allowed) { + // A deferred override now is allowed by the policy, so promote it to a regular override. + if (hasDeferredOverride(packageName) && allowed) { + boolean overrideValue = mDeferredOverrides.remove(packageName); + addPackageOverride(packageName, overrideValue); + return true; + } + // A previously set override is no longer allowed by the policy, so make it deferred. + if (hasOverride(packageName) && !allowed) { + boolean overrideValue = mPackageOverrides.remove(packageName); + addPackageDeferredOverride(packageName, overrideValue); + // Notify because the override was removed. + notifyListener(packageName); + return true; + } + return false; + } + /** * Remove any package override for the given package name, restoring the default behaviour. * @@ -133,6 +184,9 @@ public final class CompatChange extends CompatibilityChangeInfo { notifyListener(pname); } } + if (mDeferredOverrides != null) { + mDeferredOverrides.remove(pname); + } } /** @@ -176,6 +230,15 @@ public final class CompatChange extends CompatibilityChangeInfo { return mPackageOverrides != null && mPackageOverrides.containsKey(packageName); } + /** + * Checks whether a change has a deferred override for a package. + * @param packageName name of the package + * @return true if there is such a deferred override + */ + boolean hasDeferredOverride(String packageName) { + return mDeferredOverrides != null && mDeferredOverrides.containsKey(packageName); + } + @Override public String toString() { StringBuilder sb = new StringBuilder("ChangeId(") @@ -195,6 +258,9 @@ public final class CompatChange extends CompatibilityChangeInfo { if (mPackageOverrides != null && mPackageOverrides.size() > 0) { sb.append("; packageOverrides=").append(mPackageOverrides); } + if (mDeferredOverrides != null && mDeferredOverrides.size() > 0) { + sb.append("; deferredOverrides=").append(mDeferredOverrides); + } return sb.append(")").toString(); } diff --git a/services/core/java/com/android/server/compat/CompatConfig.java b/services/core/java/com/android/server/compat/CompatConfig.java index 8511118cc840a..1cbaa1e3451ea 100644 --- a/services/core/java/com/android/server/compat/CompatConfig.java +++ b/services/core/java/com/android/server/compat/CompatConfig.java @@ -173,7 +173,17 @@ final class CompatConfig { c = new CompatChange(changeId); addChange(c); } - c.addPackageOverride(packageName, enabled); + switch (allowedState.state) { + case OverrideAllowedState.ALLOWED: + c.addPackageOverride(packageName, enabled); + break; + case OverrideAllowedState.DEFERRED_VERIFICATION: + c.addPackageDeferredOverride(packageName, enabled); + break; + default: + throw new IllegalStateException("Should only be able to override changes that " + + "are allowed or can be deferred."); + } invalidateCache(); } return alreadyKnown; @@ -248,22 +258,22 @@ final class CompatConfig { boolean overrideExists = false; synchronized (mChanges) { CompatChange c = mChanges.get(changeId); - try { - if (c != null) { - overrideExists = c.hasOverride(packageName); - if (overrideExists) { - OverrideAllowedState allowedState = - mOverrideValidator.getOverrideAllowedState(changeId, packageName); - allowedState.enforce(changeId, packageName); - c.removePackageOverride(packageName); - } + if (c != null) { + // Always allow removing a deferred override. + if (c.hasDeferredOverride(packageName)) { + c.removePackageOverride(packageName); + overrideExists = true; + } else if (c.hasOverride(packageName)) { + // Regular overrides need to pass the policy. + overrideExists = true; + OverrideAllowedState allowedState = + mOverrideValidator.getOverrideAllowedState(changeId, packageName); + allowedState.enforce(changeId, packageName); + c.removePackageOverride(packageName); } - } catch (RemoteException e) { - // Should never occur, since validator is in the same process. - throw new RuntimeException("Unable to call override validator!", e); } - invalidateCache(); } + invalidateCache(); return overrideExists; } @@ -293,29 +303,15 @@ final class CompatConfig { * Removes all overrides previously added via {@link #addOverride(long, String, boolean)} or * {@link #addOverrides(CompatibilityChangeConfig, String)} for a certain package. * - *

This restores the default behaviour for the given change and app, once any app - * processes have been restarted. + *

This restores the default behaviour for the given app. * * @param packageName The package for which the overrides should be purged. */ void removePackageOverrides(String packageName) throws RemoteException, SecurityException { synchronized (mChanges) { for (int i = 0; i < mChanges.size(); ++i) { - try { - CompatChange change = mChanges.valueAt(i); - if (change.hasOverride(packageName)) { - OverrideAllowedState allowedState = - mOverrideValidator.getOverrideAllowedState(change.getId(), - packageName); - allowedState.enforce(change.getId(), packageName); - if (change != null) { - mChanges.valueAt(i).removePackageOverride(packageName); - } - } - } catch (RemoteException e) { - // Should never occur, since validator is in the same process. - throw new RuntimeException("Unable to call override validator!", e); - } + CompatChange change = mChanges.valueAt(i); + removeOverride(change.getId(), packageName); } invalidateCache(); } @@ -340,7 +336,7 @@ final class CompatConfig { } } catch (RemoteException e) { // Should never occur, since validator is in the same process. - throw new RuntimeException("Unable to call override validator!", e); + throw new AssertionError("Unable to call override validator!", e); } } } @@ -511,4 +507,27 @@ final class CompatConfig { private void invalidateCache() { ChangeIdStateCache.invalidate(); } + /** + * Rechecks all the existing overrides for a package. + */ + void recheckOverrides(String packageName) { + synchronized (mChanges) { + boolean shouldInvalidateCache = false; + for (int idx = 0; idx < mChanges.size(); ++idx) { + CompatChange c = mChanges.valueAt(idx); + try { + OverrideAllowedState allowedState = + mOverrideValidator.getOverrideAllowedState(c.getId(), packageName); + boolean allowedOverride = (allowedState.state == OverrideAllowedState.ALLOWED); + shouldInvalidateCache |= c.recheckOverride(packageName, allowedOverride); + } catch (RemoteException e) { + // Should never occur, since validator is in the same process. + throw new AssertionError("Unable to call override validator!", e); + } + } + if (shouldInvalidateCache) { + invalidateCache(); + } + } + } } diff --git a/services/core/java/com/android/server/compat/OverrideValidatorImpl.java b/services/core/java/com/android/server/compat/OverrideValidatorImpl.java index 79a13ca242c17..58250407b8643 100644 --- a/services/core/java/com/android/server/compat/OverrideValidatorImpl.java +++ b/services/core/java/com/android/server/compat/OverrideValidatorImpl.java @@ -17,11 +17,11 @@ package com.android.server.compat; import static com.android.internal.compat.OverrideAllowedState.ALLOWED; +import static com.android.internal.compat.OverrideAllowedState.DEFERRED_VERIFICATION; import static com.android.internal.compat.OverrideAllowedState.DISABLED_NON_TARGET_SDK; import static com.android.internal.compat.OverrideAllowedState.DISABLED_NOT_DEBUGGABLE; import static com.android.internal.compat.OverrideAllowedState.DISABLED_TARGET_SDK_TOO_HIGH; import static com.android.internal.compat.OverrideAllowedState.LOGGING_ONLY_CHANGE; -import static com.android.internal.compat.OverrideAllowedState.PACKAGE_DOES_NOT_EXIST; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -73,7 +73,7 @@ public class OverrideValidatorImpl extends IOverrideValidator.Stub { try { applicationInfo = packageManager.getApplicationInfo(packageName, 0); } catch (NameNotFoundException e) { - return new OverrideAllowedState(PACKAGE_DOES_NOT_EXIST, -1, -1); + return new OverrideAllowedState(DEFERRED_VERIFICATION, -1, -1); } int appTargetSdk = applicationInfo.targetSdkVersion; // Only allow overriding debuggable apps. diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java index aa85f7f0f55f9..fada37bce82c1 100644 --- a/services/core/java/com/android/server/compat/PlatformCompat.java +++ b/services/core/java/com/android/server/compat/PlatformCompat.java @@ -25,9 +25,13 @@ import static android.os.Process.SYSTEM_UID; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.IActivityManager; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManagerInternal; +import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.RemoteException; @@ -74,6 +78,7 @@ public class PlatformCompat extends IPlatformCompat.Stub { mChangeReporter = new ChangeReporter( ChangeReporter.SOURCE_SYSTEM_SERVER); mCompatConfig = compatConfig; + registerPackageReceiver(context); } @Override @@ -389,4 +394,34 @@ public class PlatformCompat extends IPlatformCompat.Stub { } return true; } + + /** + * Registers a broadcast receiver that listens for package install, replace or remove. + * @param context the context where the receiver should be registered. + */ + public void registerPackageReceiver(Context context) { + final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null) { + return; + } + final Uri packageData = intent.getData(); + if (packageData == null) { + return; + } + final String packageName = packageData.getSchemeSpecificPart(); + if (packageName == null) { + return; + } + mCompatConfig.recheckOverrides(packageName); + } + }; + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + context.registerReceiver(receiver, filter); + } } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index b5c5bb52d0501..59b24f887f2c2 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -910,6 +910,9 @@ public final class SystemServer { mActivityManagerService.setSystemProcess(); t.traceEnd(); + // The package receiver depends on the activity service in order to get registered. + platformCompat.registerPackageReceiver(mSystemContext); + // Complete the watchdog setup with an ActivityManager instance and listen for reboots // Do this only after the ActivityManagerService is properly started as a system process t.traceBegin("InitWatchdog"); diff --git a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java index 8c63bfcf1407d..6cb2dbc7cbff8 100644 --- a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java @@ -28,6 +28,7 @@ import android.app.compat.ChangeIdStateCache; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import androidx.test.runner.AndroidJUnit4; @@ -229,6 +230,83 @@ public class CompatConfigTest { assertThat(compatConfig.isChangeEnabled(1234L, applicationInfo)).isFalse(); } + @Test + public void testApplyDeferredOverridesAfterInstallingApp() throws Exception { + ApplicationInfo applicationInfo = ApplicationInfoBuilder.create() + .withPackageName("com.notinstalled.foo") + .debuggable().build(); + when(mPackageManager.getApplicationInfo(eq("com.notinstalled.foo"), anyInt())) + .thenThrow(new NameNotFoundException()); + CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext) + .addDisabledChangeWithId(1234L).build(); + when(mBuildClassifier.isDebuggableBuild()).thenReturn(false); + when(mBuildClassifier.isFinalBuild()).thenReturn(true); + + // Add override before the app is available. + compatConfig.addOverride(1234L, "com.notinstalled.foo", true); + assertThat(compatConfig.isChangeEnabled(1234L, applicationInfo)).isFalse(); + + // Pretend the app is now installed. + when(mPackageManager.getApplicationInfo(eq("com.notinstalled.foo"), anyInt())) + .thenReturn(applicationInfo); + + compatConfig.recheckOverrides("com.notinstalled.foo"); + assertThat(compatConfig.isChangeEnabled(1234L, applicationInfo)).isTrue(); + } + + @Test + public void testApplyDeferredOverrideClearsOverrideAfterUninstall() throws Exception { + ApplicationInfo applicationInfo = ApplicationInfoBuilder.create() + .withPackageName("com.installedapp.foo") + .debuggable().build(); + when(mPackageManager.getApplicationInfo(eq("com.installedapp.foo"), anyInt())) + .thenReturn(applicationInfo); + + CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext) + .addDisabledChangeWithId(1234L).build(); + when(mBuildClassifier.isDebuggableBuild()).thenReturn(false); + when(mBuildClassifier.isFinalBuild()).thenReturn(true); + + // Add override when app is installed. + compatConfig.addOverride(1234L, "com.installedapp.foo", true); + assertThat(compatConfig.isChangeEnabled(1234L, applicationInfo)).isTrue(); + + // Pretend the app is now uninstalled. + when(mPackageManager.getApplicationInfo(eq("com.installedapp.foo"), anyInt())) + .thenThrow(new NameNotFoundException()); + + compatConfig.recheckOverrides("com.installedapp.foo"); + assertThat(compatConfig.isChangeEnabled(1234L, applicationInfo)).isFalse(); + } + + @Test + public void testApplyDeferredOverrideClearsOverrideAfterChange() throws Exception { + ApplicationInfo debuggableApp = ApplicationInfoBuilder.create() + .withPackageName("com.installedapp.foo") + .debuggable().build(); + ApplicationInfo releaseApp = ApplicationInfoBuilder.create() + .withPackageName("com.installedapp.foo") + .build(); + when(mPackageManager.getApplicationInfo(eq("com.installedapp.foo"), anyInt())) + .thenReturn(debuggableApp); + + CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext) + .addDisabledChangeWithId(1234L).build(); + when(mBuildClassifier.isDebuggableBuild()).thenReturn(false); + when(mBuildClassifier.isFinalBuild()).thenReturn(true); + + // Add override for debuggable app. + compatConfig.addOverride(1234L, "com.installedapp.foo", true); + assertThat(compatConfig.isChangeEnabled(1234L, debuggableApp)).isTrue(); + + // Pretend the app now is no longer debuggable, but has the same package. + when(mPackageManager.getApplicationInfo(eq("com.installedapp.foo"), anyInt())) + .thenReturn(releaseApp); + + compatConfig.recheckOverrides("com.installedapp.foo"); + assertThat(compatConfig.isChangeEnabled(1234L, releaseApp)).isFalse(); + } + @Test public void testLoggingOnlyChangePreventAddOverride() throws Exception { CompatConfig compatConfig = CompatConfigBuilder.create(mBuildClassifier, mContext) @@ -259,7 +337,7 @@ public class CompatConfigTest { // Reject all override attempts. // Force the validator to prevent overriding the change by using a user build. when(mBuildClassifier.isDebuggableBuild()).thenReturn(false); - when(mBuildClassifier.isFinalBuild()).thenReturn(true); + when(mBuildClassifier.isFinalBuild()).thenReturn(false); // Try to turn off change, but validator prevents it. assertThrows(SecurityException.class, () -> compatConfig.removeOverride(1234L, "com.some.package")); diff --git a/services/tests/servicestests/src/com/android/server/compat/OverrideValidatorImplTest.java b/services/tests/servicestests/src/com/android/server/compat/OverrideValidatorImplTest.java index c53b29a08a4a8..019c615fed381 100644 --- a/services/tests/servicestests/src/com/android/server/compat/OverrideValidatorImplTest.java +++ b/services/tests/servicestests/src/com/android/server/compat/OverrideValidatorImplTest.java @@ -17,6 +17,7 @@ package com.android.server.compat; import static com.android.internal.compat.OverrideAllowedState.ALLOWED; +import static com.android.internal.compat.OverrideAllowedState.DEFERRED_VERIFICATION; import static com.android.internal.compat.OverrideAllowedState.DISABLED_NON_TARGET_SDK; import static com.android.internal.compat.OverrideAllowedState.DISABLED_NOT_DEBUGGABLE; import static com.android.internal.compat.OverrideAllowedState.DISABLED_TARGET_SDK_TOO_HIGH; @@ -31,6 +32,8 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; + import androidx.test.runner.AndroidJUnit4; @@ -409,4 +412,44 @@ public class OverrideValidatorImplTest { assertThat(stateDLoggingOnlyChange) .isEqualTo(new OverrideAllowedState(LOGGING_ONLY_CHANGE, -1, -1)); } + @Test + public void getOverrideAllowedState_finalBuildAnyChangeNotInstalledApp_deferOverride() + throws Exception { + CompatConfig config = CompatConfigBuilder.create(finalBuild(), mContext) + .addEnableAfterSdkChangeWithId(TARGET_SDK_BEFORE, 1) + .addEnableAfterSdkChangeWithId(TARGET_SDK, 2) + .addEnableAfterSdkChangeWithId(TARGET_SDK_AFTER, 3) + .addEnabledChangeWithId(4) + .addDisabledChangeWithId(5) + .addLoggingOnlyChangeWithId(6).build(); + IOverrideValidator overrideValidator = config.getOverrideValidator(); + when(mPackageManager.getApplicationInfo(eq(PACKAGE_NAME), anyInt())) + .thenThrow(new NameNotFoundException()); + + OverrideAllowedState stateTargetSdkLessChange = + overrideValidator.getOverrideAllowedState(1, PACKAGE_NAME); + OverrideAllowedState stateTargetSdkEqualChange = + overrideValidator.getOverrideAllowedState(2, PACKAGE_NAME); + OverrideAllowedState stateTargetSdkAfterChange = + overrideValidator.getOverrideAllowedState(3, PACKAGE_NAME); + OverrideAllowedState stateEnabledChange = + overrideValidator.getOverrideAllowedState(4, PACKAGE_NAME); + OverrideAllowedState stateDisabledChange = + overrideValidator.getOverrideAllowedState(5, PACKAGE_NAME); + OverrideAllowedState stateDLoggingOnlyChange = + overrideValidator.getOverrideAllowedState(6, PACKAGE_NAME); + + assertThat(stateTargetSdkLessChange) + .isEqualTo(new OverrideAllowedState(DEFERRED_VERIFICATION, -1, -1)); + assertThat(stateTargetSdkEqualChange) + .isEqualTo(new OverrideAllowedState(DEFERRED_VERIFICATION, -1, -1)); + assertThat(stateTargetSdkAfterChange) + .isEqualTo(new OverrideAllowedState(DEFERRED_VERIFICATION, -1, -1)); + assertThat(stateEnabledChange) + .isEqualTo(new OverrideAllowedState(DEFERRED_VERIFICATION, -1, -1)); + assertThat(stateDisabledChange) + .isEqualTo(new OverrideAllowedState(DEFERRED_VERIFICATION, -1, -1)); + assertThat(stateDLoggingOnlyChange) + .isEqualTo(new OverrideAllowedState(LOGGING_ONLY_CHANGE, -1, -1)); + } }