/* * 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 static android.annotation.SystemApi.Client.MODULE_LIBRARIES; import android.annotation.SystemApi; import android.compat.annotation.ChangeId; import libcore.api.IntraCoreApi; import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Set; import libcore.util.NonNull; /** * Internal APIs for logging and gating compatibility changes. * * @see ChangeId * * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public final class Compatibility { private Compatibility() {} /** * Reports that a compatibility change is affecting the current process now. * *

Calls to this method from a non-app process are ignored. This allows code implementing * APIs that are used by apps and by other code (e.g. the system server) to report changes * regardless of the process it's running in. When called in a non-app process, this method is * a no-op. * *

Note: for changes that are gated using {@link #isChangeEnabled(long)}, you do not need to * call this API directly. The change will be reported for you in the case that * {@link #isChangeEnabled(long)} returns {@code true}. * * @param changeId The ID of the compatibility change taking effect. * * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public static void reportUnconditionalChange(@ChangeId long changeId) { sCallbacks.onChangeReported(changeId); } /** * Query if a given compatibility change is enabled for the current process. This method should * only be called by code running inside a process of the affected app. * *

If this method returns {@code true}, the calling code should implement the compatibility * change, resulting in differing behaviour compared to earlier releases. If this method returns * {@code false}, the calling code should behave as it did in earlier releases. * *

When this method returns {@code true}, it will also report the change as * {@link #reportUnconditionalChange(long)} would, so there is no need to call that method * directly. * * @param changeId The ID of the compatibility change in question. * @return {@code true} if the change is enabled for the current app. * * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public static boolean isChangeEnabled(@ChangeId long changeId) { return sCallbacks.isChangeEnabled(changeId); } private static final BehaviorChangeDelegate DEFAULT_CALLBACKS = new BehaviorChangeDelegate(){}; private volatile static BehaviorChangeDelegate sCallbacks = DEFAULT_CALLBACKS; /** * Sets the behavior change delegate. * * All changes reported via the {@link Compatibility} class will be forwarded to this class. * * @hide */ @SystemApi(client = MODULE_LIBRARIES) public static void setBehaviorChangeDelegate(BehaviorChangeDelegate callbacks) { sCallbacks = Objects.requireNonNull(callbacks); } /** * Removes a behavior change delegate previously set via {@link #setBehaviorChangeDelegate}. * * @hide */ @SystemApi(client = MODULE_LIBRARIES) public static void clearBehaviorChangeDelegate() { sCallbacks = DEFAULT_CALLBACKS; } /** * Return the behavior change delegate * * @hide */ // VisibleForTesting @NonNull public static BehaviorChangeDelegate getBehaviorChangeDelegate() { return sCallbacks; } /** * For use by tests only. Causes values from {@code overrides} to be returned instead of the * real value. * * @hide */ @SystemApi(client = MODULE_LIBRARIES) 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); } /** * For use by tests only. Removes overrides set by {@link #setOverrides}. * * @hide */ @SystemApi(client = MODULE_LIBRARIES) public static void clearOverrides() { if (!(sCallbacks instanceof OverrideCallbacks)) { throw new IllegalStateException("No overrides set"); } sCallbacks = ((OverrideCallbacks) sCallbacks).delegate; } /** * Base class for compatibility API implementations. The default implementation logs a warning * to logcat. * * This is provided as a class rather than an interface to allow new methods to be added without * breaking @SystemApi binary compatibility. * * @hide */ @SystemApi(client = MODULE_LIBRARIES) public interface BehaviorChangeDelegate { /** * Called when a change is reported via {@link Compatibility#reportUnconditionalChange} * * @hide */ @SystemApi(client = MODULE_LIBRARIES) default void onChangeReported(long changeId) { // Do not use String.format here (b/160912695) System.logW("No Compatibility callbacks set! Reporting change " + changeId); } /** * Called when a change is queried via {@link Compatibility#isChangeEnabled} * * @hide */ @SystemApi(client = MODULE_LIBRARIES) default boolean isChangeEnabled(long changeId) { // Do not use String.format here (b/160912695) // TODO(b/289900411): Rate limit this log if it's necessary in the release build. // System.logW("No Compatibility callbacks set! Querying change " + changeId); return true; } } /** * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public static final class ChangeConfig { private final Set enabled; private final Set disabled; /** * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public ChangeConfig(@NonNull Set<@NonNull Long> enabled, @NonNull Set<@NonNull 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 intersection = new HashSet<>(enabled); intersection.retainAll(disabled); if (!intersection.isEmpty()) { throw new IllegalArgumentException("Cannot have changes " + intersection + " enabled and disabled!"); } } /** * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public boolean isEmpty() { return enabled.isEmpty() && disabled.isEmpty(); } private static long[] toLongArray(Set values) { long[] result = new long[values.size()]; int idx = 0; for (Long value: values) { result[idx++] = value; } return result; } /** * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public @NonNull long[] getEnabledChangesArray() { return toLongArray(enabled); } /** * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public @NonNull long[] getDisabledChangesArray() { return toLongArray(disabled); } /** * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public @NonNull Set<@NonNull Long> getEnabledSet() { return Collections.unmodifiableSet(enabled); } /** * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public @NonNull Set<@NonNull Long> getDisabledSet() { return Collections.unmodifiableSet(disabled); } /** * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public boolean isForceEnabled(long changeId) { return enabled.contains(changeId); } /** * @hide */ @SystemApi(client = MODULE_LIBRARIES) @IntraCoreApi public boolean isForceDisabled(long changeId) { return disabled.contains(changeId); } /** * @hide */ @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); } /** * @hide */ @Override public int hashCode() { return Objects.hash(enabled, disabled); } /** * @hide */ @Override public String toString() { return "ChangeConfig{enabled=" + enabled + ", disabled=" + disabled + '}'; } } private static class OverrideCallbacks implements BehaviorChangeDelegate { private final BehaviorChangeDelegate delegate; private final ChangeConfig changeConfig; private OverrideCallbacks(BehaviorChangeDelegate delegate, ChangeConfig changeConfig) { this.delegate = Objects.requireNonNull(delegate); this.changeConfig = Objects.requireNonNull(changeConfig); } @Override public boolean isChangeEnabled(long changeId) { if (changeConfig.isForceEnabled(changeId)) { return true; } if (changeConfig.isForceDisabled(changeId)) { return false; } return delegate.isChangeEnabled(changeId); } } }