Merge "Add rule for toggling changes at test time"
diff --git a/JavaLibrary.bp b/JavaLibrary.bp
index db04186..afba8e9 100644
--- a/JavaLibrary.bp
+++ b/JavaLibrary.bp
@@ -486,6 +486,25 @@
     system_modules: "core-all-system-modules",
 }
 
+// Builds platform_compat test rules
+java_library_static {
+    name: "platform_compat-test-rules",
+    visibility: ["//visibility:public"],
+    srcs: [
+        "luni/src/main/java/android/compat/**/*.java",
+        "test-rules/src/platform_compat/**/*.java",
+        "luni/src/main/java/libcore/api/CorePlatformApi.java",
+        "luni/src/main/java/libcore/api/IntraCoreApi.java",
+
+    ],
+    static_libs: [
+        "junit",
+        "guava",
+        "android-support-test",
+    ],
+    platform_apis: true,
+}
+
 // Builds the core-tests-support library used by various tests.
 java_library_static {
     name: "core-tests-support",
diff --git a/luni/src/main/java/android/compat/Compatibility.java b/luni/src/main/java/android/compat/Compatibility.java
index baf46bf..a0cc0a0 100644
--- a/luni/src/main/java/android/compat/Compatibility.java
+++ b/luni/src/main/java/android/compat/Compatibility.java
@@ -21,6 +21,11 @@
 import libcore.api.CorePlatformApi;
 import libcore.api.IntraCoreApi;
 
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
 /**
  * Internal APIs for logging and gating compatibility changes.
  *
@@ -78,7 +83,28 @@
 
     @CorePlatformApi
     public static void setCallbacks(Callbacks callbacks) {
-        sCallbacks = callbacks;
+        sCallbacks = Objects.requireNonNull(callbacks);
+    }
+
+    @CorePlatformApi
+    public static void setOverrides(ChangeConfig overrides) {
+        // Setting overrides twice in a row does not need to be supported because
+        // this method is only for enabling/disabling changes for the duration of
+        // a single test.
+        // In production, the app is restarted when changes get enabled or disabled,
+        // and the ChangeConfig is then set exactly once on that app process.
+        if (sCallbacks instanceof OverrideCallbacks) {
+            throw new IllegalStateException("setOverrides has already been called!");
+        }
+        sCallbacks = new OverrideCallbacks(sCallbacks, overrides);
+    }
+
+    @CorePlatformApi
+    public static void clearOverrides() {
+        if (!(sCallbacks instanceof OverrideCallbacks)) {
+            throw new IllegalStateException("No overrides set");
+        }
+        sCallbacks = ((OverrideCallbacks) sCallbacks).delegate;
     }
 
     /**
@@ -95,15 +121,111 @@
         }
         @CorePlatformApi
         protected void reportChange(long changeId) {
-            System.logW(String.format(
+            throw new IllegalStateException(String.format(
                     "No Compatibility callbacks set! Reporting change %d", changeId));
         }
         @CorePlatformApi
         protected boolean isChangeEnabled(long changeId) {
-            System.logW(String.format(
+            throw new IllegalStateException(String.format(
                     "No Compatibility callbacks set! Querying change %d", changeId));
-            return true;
         }
     }
 
+    @CorePlatformApi
+    @IntraCoreApi
+    public static final class ChangeConfig {
+        private final Set<Long> enabled;
+        private final Set<Long> disabled;
+
+        public ChangeConfig(Set<Long> enabled, Set<Long> disabled) {
+            this.enabled = Objects.requireNonNull(enabled);
+            this.disabled = Objects.requireNonNull(disabled);
+            if (enabled.contains(null)) {
+                throw new NullPointerException();
+            }
+            if (disabled.contains(null)) {
+                throw new NullPointerException();
+            }
+            Set<Long> intersection = new HashSet<>(enabled);
+            intersection.retainAll(disabled);
+            if (!intersection.isEmpty()) {
+                throw new IllegalArgumentException("Cannot have changes " + intersection
+                        + " enabled and disabled!");
+            }
+        }
+
+        private static long[] toLongArray(Set<Long> values) {
+            long[] result = new long[values.size()];
+            int idx = 0;
+            for (Long value: values) {
+                result[idx++] = value;
+            }
+            return result;
+        }
+
+        public long[] forceEnabledChangesArray() {
+            return toLongArray(enabled);
+        }
+
+        public long[] forceDisabledChangesArray() {
+            return toLongArray(disabled);
+        }
+
+        public Set<Long> forceEnabledSet() {
+            return Collections.unmodifiableSet(enabled);
+        }
+
+        public Set<Long> forceDisabledSet() {
+            return Collections.unmodifiableSet(disabled);
+        }
+
+        public boolean isForceEnabled(long changeId) {
+            return enabled.contains(changeId);
+        }
+
+        public boolean isForceDisabled(long changeId) {
+            return disabled.contains(changeId);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof ChangeConfig)) {
+                return false;
+            }
+            ChangeConfig that = (ChangeConfig) o;
+            return enabled.equals(that.enabled) &&
+                    disabled.equals(that.disabled);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(enabled, disabled);
+        }
+
+        @Override
+        public String toString() {
+            return "ChangeConfig{enabled=" + enabled + ", disabled=" + disabled + '}';
+        }
+    }
+
+    private static class OverrideCallbacks extends Callbacks {
+        private final Callbacks delegate;
+        private final ChangeConfig changeConfig;
+
+        private OverrideCallbacks(Callbacks delegate, ChangeConfig changeConfig) {
+            this.delegate = Objects.requireNonNull(delegate);
+            this.changeConfig = Objects.requireNonNull(changeConfig);
+        }
+        @Override
+        protected boolean isChangeEnabled(long changeId) {
+           if (changeConfig.isForceEnabled(changeId)) {
+               return true;
+           }
+           if (changeConfig.isForceDisabled(changeId)) {
+               return false;
+           }
+           return delegate.isChangeEnabled(changeId);
+        }
+    }
 }
diff --git a/mmodules/core_platform_api/api/platform/current-api.txt b/mmodules/core_platform_api/api/platform/current-api.txt
index 0041b36..14d96eb 100644
--- a/mmodules/core_platform_api/api/platform/current-api.txt
+++ b/mmodules/core_platform_api/api/platform/current-api.txt
@@ -2,9 +2,11 @@
 package android.compat {
 
   public final class Compatibility {
+    method public static void clearOverrides();
     method public static boolean isChangeEnabled(@android.compat.annotation.ChangeId long);
     method public static void reportChange(@android.compat.annotation.ChangeId long);
     method public static void setCallbacks(android.compat.Compatibility.Callbacks);
+    method public static void setOverrides(android.compat.Compatibility.ChangeConfig);
   }
 
   public static class Compatibility.Callbacks {
@@ -13,6 +15,16 @@
     method protected void reportChange(long);
   }
 
+  public static final class Compatibility.ChangeConfig {
+    ctor public Compatibility.ChangeConfig(java.util.Set<java.lang.Long>, java.util.Set<java.lang.Long>);
+    method public long[] forceDisabledChangesArray();
+    method public java.util.Set<java.lang.Long> forceDisabledSet();
+    method public long[] forceEnabledChangesArray();
+    method public java.util.Set<java.lang.Long> forceEnabledSet();
+    method public boolean isForceDisabled(long);
+    method public boolean isForceEnabled(long);
+  }
+
 }
 
 package android.compat.annotation {
diff --git a/mmodules/intracoreapi/api/intra/current-api.txt b/mmodules/intracoreapi/api/intra/current-api.txt
index 5e914d2..02cedb4 100644
--- a/mmodules/intracoreapi/api/intra/current-api.txt
+++ b/mmodules/intracoreapi/api/intra/current-api.txt
@@ -6,6 +6,16 @@
     method @libcore.api.CorePlatformApi @libcore.api.IntraCoreApi public static void reportChange(@android.compat.annotation.ChangeId long);
   }
 
+  @libcore.api.CorePlatformApi @libcore.api.IntraCoreApi public static final class Compatibility.ChangeConfig {
+    ctor public Compatibility.ChangeConfig(java.util.Set<java.lang.Long>, java.util.Set<java.lang.Long>);
+    method public long[] forceDisabledChangesArray();
+    method public java.util.Set<java.lang.Long> forceDisabledSet();
+    method public long[] forceEnabledChangesArray();
+    method public java.util.Set<java.lang.Long> forceEnabledSet();
+    method public boolean isForceDisabled(long);
+    method public boolean isForceEnabled(long);
+  }
+
 }
 
 package android.compat.annotation {
diff --git a/test-rules/src/platform_compat/java/android/compat/CompatChangeRule.java b/test-rules/src/platform_compat/java/android/compat/CompatChangeRule.java
new file mode 100644
index 0000000..2586f9c
--- /dev/null
+++ b/test-rules/src/platform_compat/java/android/compat/CompatChangeRule.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.compat;
+
+import android.app.Instrumentation;
+import android.compat.Compatibility.Callbacks;
+import android.compat.Compatibility.ChangeConfig;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.support.test.InstrumentationRegistry;
+import android.util.ArraySet;
+
+import com.android.internal.compat.CompatibilityChangeConfig;
+import com.android.internal.compat.IPlatformCompat;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import com.google.common.primitives.Longs;
+
+
+/**
+ * Allows tests to specify the which change to disable.
+ *
+ * <p>To use add the following to the test class. It will only change the behavior of a test method
+ * if it is annotated with {@link EnableCompatChanges} and/or {@link DisableCompatChanges}.
+ *
+ * <pre>
+ * &#64;Rule
+ * public TestRule compatChangeRule = new CompatChangeRule();
+ * </pre>
+ *
+ * <p>Each test method that needs to disable a specific change needs to be annotated
+ * with {@link EnableCompatChanges} and/or {@link DisableCompatChanges} specifying the change id.
+ * e.g.:
+ *
+ * <pre>
+ *   &#64;Test
+ *   &#64;DisableCompatChanges({42})
+ *   public void testAsIfChange42Disabled() {
+ *     // check behavior
+ *   }
+ *
+ *   &#64;Test
+ *   &#64;EnableCompatChanges({42})
+ *   public void testAsIfChange42Enabled() {
+ *     // check behavior
+ *
+ * </pre>
+ */
+public class CompatChangeRule implements TestRule {
+    @Override
+    public Statement apply(final Statement statement, Description description) {
+        Set<Long> enabled = new HashSet<>();
+        Set<Long> disabled = new HashSet<>();
+        EnableCompatChanges enableCompatChanges = description.getAnnotation(
+                EnableCompatChanges.class);
+        DisableCompatChanges disableCompatChanges = description.getAnnotation(
+                DisableCompatChanges.class);
+        if (enableCompatChanges != null) {
+            enabled.addAll(Longs.asList(enableCompatChanges.value()));
+        }
+        if (disableCompatChanges != null) {
+            disabled.addAll(Longs.asList(disableCompatChanges.value()));
+        }
+        ArraySet<Long> intersection = new ArraySet<>(enabled);
+        intersection.retainAll(disabled);
+        if (!intersection.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "Changes " + intersection + " are both enabled and disabled.");
+        }
+        if (enabled.isEmpty() && disabled.isEmpty()) {
+            throw new IllegalStateException("Added a CompatChangeRule without specifying any "
+                + "@EnableCompatChanges or @DisableCompatChanges !");
+        }
+        return new CompatChangeStatement(statement, new ChangeConfig(enabled, disabled));
+    }
+
+    private static class CompatChangeStatement extends Statement {
+        private final Statement testStatement;
+        private final ChangeConfig config;
+
+        private CompatChangeStatement(Statement testStatement, ChangeConfig config) {
+            this.testStatement = testStatement;
+            this.config = config;
+        }
+
+        @Override
+        public void evaluate() throws Throwable {
+            Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+            String packageName = instrumentation.getTargetContext().getPackageName();
+            IPlatformCompat platformCompat = IPlatformCompat.Stub
+                .asInterface(ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
+            if (platformCompat == null) {
+                throw new IllegalStateException("Could not get IPlatformCompat service!");
+            }
+            Compatibility.setOverrides(config);
+            try {
+                platformCompat.setOverrides(new CompatibilityChangeConfig(config), packageName);
+                try {
+                    testStatement.evaluate();
+                } finally {
+                    platformCompat.clearOverrides(packageName);
+                }
+            } catch(RemoteException e) {
+                throw new RuntimeException("Could not call IPlatformCompat binder method!", e);
+            } finally {
+                Compatibility.clearOverrides();
+            }
+        }
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    public @interface EnableCompatChanges {
+        long[] value();
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    public @interface DisableCompatChanges {
+        long[] value();
+    }
+}