1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package android.compat; 18 19 import android.compat.annotation.ChangeId; 20 21 import libcore.api.CorePlatformApi; 22 import libcore.api.IntraCoreApi; 23 24 import java.util.Collections; 25 import java.util.HashSet; 26 import java.util.Objects; 27 import java.util.Set; 28 29 /** 30 * Internal APIs for logging and gating compatibility changes. 31 * 32 * @see ChangeId 33 * 34 * @hide 35 */ 36 @CorePlatformApi(status = CorePlatformApi.Status.STABLE) 37 @IntraCoreApi 38 public final class Compatibility { 39 Compatibility()40 private Compatibility() {} 41 42 /** 43 * Reports that a compatibility change is affecting the current process now. 44 * 45 * <p>Calls to this method from a non-app process are ignored. This allows code implementing 46 * APIs that are used by apps and by other code (e.g. the system server) to report changes 47 * regardless of the process it's running in. When called in a non-app process, this method is 48 * a no-op. 49 * 50 * <p>Note: for changes that are gated using {@link #isChangeEnabled(long)}, you do not need to 51 * call this API directly. The change will be reported for you in the case that 52 * {@link #isChangeEnabled(long)} returns {@code true}. 53 * 54 * @param changeId The ID of the compatibility change taking effect. 55 */ 56 @CorePlatformApi(status = CorePlatformApi.Status.STABLE) 57 @IntraCoreApi reportChange(@hangeId long changeId)58 public static void reportChange(@ChangeId long changeId) { 59 sCallbacks.reportChange(changeId); 60 } 61 62 /** 63 * Query if a given compatibility change is enabled for the current process. This method should 64 * only be called by code running inside a process of the affected app. 65 * 66 * <p>If this method returns {@code true}, the calling code should implement the compatibility 67 * change, resulting in differing behaviour compared to earlier releases. If this method returns 68 * {@code false}, the calling code should behave as it did in earlier releases. 69 * 70 * <p>When this method returns {@code true}, it will also report the change as 71 * {@link #reportChange(long)} would, so there is no need to call that method directly. 72 * 73 * @param changeId The ID of the compatibility change in question. 74 * @return {@code true} if the change is enabled for the current app. 75 */ 76 @CorePlatformApi(status = CorePlatformApi.Status.STABLE) 77 @IntraCoreApi isChangeEnabled(@hangeId long changeId)78 public static boolean isChangeEnabled(@ChangeId long changeId) { 79 return sCallbacks.isChangeEnabled(changeId); 80 } 81 82 private volatile static Callbacks sCallbacks = new Callbacks(); 83 84 @CorePlatformApi(status = CorePlatformApi.Status.STABLE) setCallbacks(Callbacks callbacks)85 public static void setCallbacks(Callbacks callbacks) { 86 sCallbacks = Objects.requireNonNull(callbacks); 87 } 88 89 @CorePlatformApi(status = CorePlatformApi.Status.STABLE) setOverrides(ChangeConfig overrides)90 public static void setOverrides(ChangeConfig overrides) { 91 // Setting overrides twice in a row does not need to be supported because 92 // this method is only for enabling/disabling changes for the duration of 93 // a single test. 94 // In production, the app is restarted when changes get enabled or disabled, 95 // and the ChangeConfig is then set exactly once on that app process. 96 if (sCallbacks instanceof OverrideCallbacks) { 97 throw new IllegalStateException("setOverrides has already been called!"); 98 } 99 sCallbacks = new OverrideCallbacks(sCallbacks, overrides); 100 } 101 102 @CorePlatformApi(status = CorePlatformApi.Status.STABLE) clearOverrides()103 public static void clearOverrides() { 104 if (!(sCallbacks instanceof OverrideCallbacks)) { 105 throw new IllegalStateException("No overrides set"); 106 } 107 sCallbacks = ((OverrideCallbacks) sCallbacks).delegate; 108 } 109 110 /** 111 * Base class for compatibility API implementations. The default implementation logs a warning 112 * to logcat. 113 * 114 * This is provided as a class rather than an interface to allow new methods to be added without 115 * breaking @CorePlatformApi binary compatibility. 116 */ 117 @CorePlatformApi(status = CorePlatformApi.Status.STABLE) 118 public static class Callbacks { 119 @CorePlatformApi(status = CorePlatformApi.Status.STABLE) Callbacks()120 protected Callbacks() { 121 } 122 @CorePlatformApi(status = CorePlatformApi.Status.STABLE) reportChange(long changeId)123 protected void reportChange(long changeId) { 124 // Do not use String.format here (b/160912695) 125 System.logW("No Compatibility callbacks set! Reporting change " + changeId); 126 } 127 @CorePlatformApi(status = CorePlatformApi.Status.STABLE) isChangeEnabled(long changeId)128 protected boolean isChangeEnabled(long changeId) { 129 // Do not use String.format here (b/160912695) 130 System.logW("No Compatibility callbacks set! Querying change " + changeId); 131 return true; 132 } 133 } 134 135 @CorePlatformApi(status = CorePlatformApi.Status.STABLE) 136 @IntraCoreApi 137 public static final class ChangeConfig { 138 private final Set<Long> enabled; 139 private final Set<Long> disabled; 140 ChangeConfig(Set<Long> enabled, Set<Long> disabled)141 public ChangeConfig(Set<Long> enabled, Set<Long> disabled) { 142 this.enabled = Objects.requireNonNull(enabled); 143 this.disabled = Objects.requireNonNull(disabled); 144 if (enabled.contains(null)) { 145 throw new NullPointerException(); 146 } 147 if (disabled.contains(null)) { 148 throw new NullPointerException(); 149 } 150 Set<Long> intersection = new HashSet<>(enabled); 151 intersection.retainAll(disabled); 152 if (!intersection.isEmpty()) { 153 throw new IllegalArgumentException("Cannot have changes " + intersection 154 + " enabled and disabled!"); 155 } 156 } 157 isEmpty()158 public boolean isEmpty() { 159 return enabled.isEmpty() && disabled.isEmpty(); 160 } 161 toLongArray(Set<Long> values)162 private static long[] toLongArray(Set<Long> values) { 163 long[] result = new long[values.size()]; 164 int idx = 0; 165 for (Long value: values) { 166 result[idx++] = value; 167 } 168 return result; 169 } 170 forceEnabledChangesArray()171 public long[] forceEnabledChangesArray() { 172 return toLongArray(enabled); 173 } 174 forceDisabledChangesArray()175 public long[] forceDisabledChangesArray() { 176 return toLongArray(disabled); 177 } 178 forceEnabledSet()179 public Set<Long> forceEnabledSet() { 180 return Collections.unmodifiableSet(enabled); 181 } 182 forceDisabledSet()183 public Set<Long> forceDisabledSet() { 184 return Collections.unmodifiableSet(disabled); 185 } 186 isForceEnabled(long changeId)187 public boolean isForceEnabled(long changeId) { 188 return enabled.contains(changeId); 189 } 190 isForceDisabled(long changeId)191 public boolean isForceDisabled(long changeId) { 192 return disabled.contains(changeId); 193 } 194 195 @Override equals(Object o)196 public boolean equals(Object o) { 197 if (this == o) return true; 198 if (!(o instanceof ChangeConfig)) { 199 return false; 200 } 201 ChangeConfig that = (ChangeConfig) o; 202 return enabled.equals(that.enabled) && 203 disabled.equals(that.disabled); 204 } 205 206 @Override hashCode()207 public int hashCode() { 208 return Objects.hash(enabled, disabled); 209 } 210 211 @Override toString()212 public String toString() { 213 return "ChangeConfig{enabled=" + enabled + ", disabled=" + disabled + '}'; 214 } 215 } 216 217 private static class OverrideCallbacks extends Callbacks { 218 private final Callbacks delegate; 219 private final ChangeConfig changeConfig; 220 OverrideCallbacks(Callbacks delegate, ChangeConfig changeConfig)221 private OverrideCallbacks(Callbacks delegate, ChangeConfig changeConfig) { 222 this.delegate = Objects.requireNonNull(delegate); 223 this.changeConfig = Objects.requireNonNull(changeConfig); 224 } 225 @Override isChangeEnabled(long changeId)226 protected boolean isChangeEnabled(long changeId) { 227 if (changeConfig.isForceEnabled(changeId)) { 228 return true; 229 } 230 if (changeConfig.isForceDisabled(changeId)) { 231 return false; 232 } 233 return delegate.isChangeEnabled(changeId); 234 } 235 } 236 } 237