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 static android.annotation.SystemApi.Client.MODULE_LIBRARIES; 20 21 import android.annotation.SystemApi; 22 import android.compat.annotation.ChangeId; 23 24 import libcore.api.IntraCoreApi; 25 26 import java.util.Collections; 27 import java.util.HashSet; 28 import java.util.Objects; 29 import java.util.Set; 30 import libcore.util.NonNull; 31 32 /** 33 * Internal APIs for logging and gating compatibility changes. 34 * 35 * @see ChangeId 36 * 37 * @hide 38 */ 39 @SystemApi(client = MODULE_LIBRARIES) 40 @IntraCoreApi 41 public final class Compatibility { 42 Compatibility()43 private Compatibility() {} 44 45 /** 46 * Reports that a compatibility change is affecting the current process now. 47 * 48 * <p>Calls to this method from a non-app process are ignored. This allows code implementing 49 * APIs that are used by apps and by other code (e.g. the system server) to report changes 50 * regardless of the process it's running in. When called in a non-app process, this method is 51 * a no-op. 52 * 53 * <p>Note: for changes that are gated using {@link #isChangeEnabled(long)}, you do not need to 54 * call this API directly. The change will be reported for you in the case that 55 * {@link #isChangeEnabled(long)} returns {@code true}. 56 * 57 * @param changeId The ID of the compatibility change taking effect. 58 * 59 * @hide 60 */ 61 @SystemApi(client = MODULE_LIBRARIES) 62 @IntraCoreApi reportUnconditionalChange(@hangeId long changeId)63 public static void reportUnconditionalChange(@ChangeId long changeId) { 64 sCallbacks.onChangeReported(changeId); 65 } 66 67 /** 68 * Query if a given compatibility change is enabled for the current process. This method should 69 * only be called by code running inside a process of the affected app. 70 * 71 * <p>If this method returns {@code true}, the calling code should implement the compatibility 72 * change, resulting in differing behaviour compared to earlier releases. If this method returns 73 * {@code false}, the calling code should behave as it did in earlier releases. 74 * 75 * <p>When this method returns {@code true}, it will also report the change as 76 * {@link #reportUnconditionalChange(long)} would, so there is no need to call that method 77 * directly. 78 * 79 * @param changeId The ID of the compatibility change in question. 80 * @return {@code true} if the change is enabled for the current app. 81 * 82 * @hide 83 */ 84 @SystemApi(client = MODULE_LIBRARIES) 85 @IntraCoreApi isChangeEnabled(@hangeId long changeId)86 public static boolean isChangeEnabled(@ChangeId long changeId) { 87 return sCallbacks.isChangeEnabled(changeId); 88 } 89 90 private static final BehaviorChangeDelegate DEFAULT_CALLBACKS = new BehaviorChangeDelegate(){}; 91 92 private volatile static BehaviorChangeDelegate sCallbacks = DEFAULT_CALLBACKS; 93 94 /** 95 * Sets the behavior change delegate. 96 * 97 * All changes reported via the {@link Compatibility} class will be forwarded to this class. 98 * 99 * @hide 100 */ 101 @SystemApi(client = MODULE_LIBRARIES) setBehaviorChangeDelegate(BehaviorChangeDelegate callbacks)102 public static void setBehaviorChangeDelegate(BehaviorChangeDelegate callbacks) { 103 sCallbacks = Objects.requireNonNull(callbacks); 104 } 105 106 /** 107 * Removes a behavior change delegate previously set via {@link #setBehaviorChangeDelegate}. 108 * 109 * @hide 110 */ 111 @SystemApi(client = MODULE_LIBRARIES) clearBehaviorChangeDelegate()112 public static void clearBehaviorChangeDelegate() { 113 sCallbacks = DEFAULT_CALLBACKS; 114 } 115 116 /** 117 * Return the behavior change delegate 118 * 119 * @hide 120 */ 121 // VisibleForTesting 122 @NonNull getBehaviorChangeDelegate()123 public static BehaviorChangeDelegate getBehaviorChangeDelegate() { 124 return sCallbacks; 125 } 126 127 /** 128 * For use by tests only. Causes values from {@code overrides} to be returned instead of the 129 * real value. 130 * 131 * @hide 132 */ 133 @SystemApi(client = MODULE_LIBRARIES) setOverrides(ChangeConfig overrides)134 public static void setOverrides(ChangeConfig overrides) { 135 // Setting overrides twice in a row does not need to be supported because 136 // this method is only for enabling/disabling changes for the duration of 137 // a single test. 138 // In production, the app is restarted when changes get enabled or disabled, 139 // and the ChangeConfig is then set exactly once on that app process. 140 if (sCallbacks instanceof OverrideCallbacks) { 141 throw new IllegalStateException("setOverrides has already been called!"); 142 } 143 sCallbacks = new OverrideCallbacks(sCallbacks, overrides); 144 } 145 146 /** 147 * For use by tests only. Removes overrides set by {@link #setOverrides}. 148 * 149 * @hide 150 */ 151 @SystemApi(client = MODULE_LIBRARIES) clearOverrides()152 public static void clearOverrides() { 153 if (!(sCallbacks instanceof OverrideCallbacks)) { 154 throw new IllegalStateException("No overrides set"); 155 } 156 sCallbacks = ((OverrideCallbacks) sCallbacks).delegate; 157 } 158 159 /** 160 * Base class for compatibility API implementations. The default implementation logs a warning 161 * to logcat. 162 * 163 * This is provided as a class rather than an interface to allow new methods to be added without 164 * breaking @SystemApi binary compatibility. 165 * 166 * @hide 167 */ 168 @SystemApi(client = MODULE_LIBRARIES) 169 public interface BehaviorChangeDelegate { 170 /** 171 * Called when a change is reported via {@link Compatibility#reportUnconditionalChange} 172 * 173 * @hide 174 */ 175 @SystemApi(client = MODULE_LIBRARIES) onChangeReported(long changeId)176 default void onChangeReported(long changeId) { 177 // Do not use String.format here (b/160912695) 178 System.logW("No Compatibility callbacks set! Reporting change " + changeId); 179 } 180 181 /** 182 * Called when a change is queried via {@link Compatibility#isChangeEnabled} 183 * 184 * @hide 185 */ 186 @SystemApi(client = MODULE_LIBRARIES) isChangeEnabled(long changeId)187 default boolean isChangeEnabled(long changeId) { 188 // Do not use String.format here (b/160912695) 189 // TODO(b/289900411): Rate limit this log if it's necessary in the release build. 190 // System.logW("No Compatibility callbacks set! Querying change " + changeId); 191 return true; 192 } 193 } 194 195 /** 196 * @hide 197 */ 198 @SystemApi(client = MODULE_LIBRARIES) 199 @IntraCoreApi 200 public static final class ChangeConfig { 201 private final Set<Long> enabled; 202 private final Set<Long> disabled; 203 204 /** 205 * @hide 206 */ 207 @SystemApi(client = MODULE_LIBRARIES) 208 @IntraCoreApi ChangeConfig(@onNull Set<@NonNull Long> enabled, @NonNull Set<@NonNull Long> disabled)209 public ChangeConfig(@NonNull Set<@NonNull Long> enabled, @NonNull Set<@NonNull Long> disabled) { 210 this.enabled = Objects.requireNonNull(enabled); 211 this.disabled = Objects.requireNonNull(disabled); 212 if (enabled.contains(null)) { 213 throw new NullPointerException(); 214 } 215 if (disabled.contains(null)) { 216 throw new NullPointerException(); 217 } 218 Set<Long> intersection = new HashSet<>(enabled); 219 intersection.retainAll(disabled); 220 if (!intersection.isEmpty()) { 221 throw new IllegalArgumentException("Cannot have changes " + intersection 222 + " enabled and disabled!"); 223 } 224 } 225 226 /** 227 * @hide 228 */ 229 @SystemApi(client = MODULE_LIBRARIES) 230 @IntraCoreApi isEmpty()231 public boolean isEmpty() { 232 return enabled.isEmpty() && disabled.isEmpty(); 233 } 234 toLongArray(Set<Long> values)235 private static long[] toLongArray(Set<Long> values) { 236 long[] result = new long[values.size()]; 237 int idx = 0; 238 for (Long value: values) { 239 result[idx++] = value; 240 } 241 return result; 242 } 243 244 /** 245 * @hide 246 */ 247 @SystemApi(client = MODULE_LIBRARIES) 248 @IntraCoreApi getEnabledChangesArray()249 public @NonNull long[] getEnabledChangesArray() { 250 return toLongArray(enabled); 251 } 252 253 254 /** 255 * @hide 256 */ 257 @SystemApi(client = MODULE_LIBRARIES) 258 @IntraCoreApi getDisabledChangesArray()259 public @NonNull long[] getDisabledChangesArray() { 260 return toLongArray(disabled); 261 } 262 263 264 /** 265 * @hide 266 */ 267 @SystemApi(client = MODULE_LIBRARIES) 268 @IntraCoreApi getEnabledSet()269 public @NonNull Set<@NonNull Long> getEnabledSet() { 270 return Collections.unmodifiableSet(enabled); 271 } 272 273 274 /** 275 * @hide 276 */ 277 @SystemApi(client = MODULE_LIBRARIES) 278 @IntraCoreApi getDisabledSet()279 public @NonNull Set<@NonNull Long> getDisabledSet() { 280 return Collections.unmodifiableSet(disabled); 281 } 282 283 284 /** 285 * @hide 286 */ 287 @SystemApi(client = MODULE_LIBRARIES) 288 @IntraCoreApi isForceEnabled(long changeId)289 public boolean isForceEnabled(long changeId) { 290 return enabled.contains(changeId); 291 } 292 293 294 /** 295 * @hide 296 */ 297 @SystemApi(client = MODULE_LIBRARIES) 298 @IntraCoreApi isForceDisabled(long changeId)299 public boolean isForceDisabled(long changeId) { 300 return disabled.contains(changeId); 301 } 302 303 304 /** 305 * @hide 306 */ 307 @Override equals(Object o)308 public boolean equals(Object o) { 309 if (this == o) return true; 310 if (!(o instanceof ChangeConfig)) { 311 return false; 312 } 313 ChangeConfig that = (ChangeConfig) o; 314 return enabled.equals(that.enabled) && 315 disabled.equals(that.disabled); 316 } 317 318 /** 319 * @hide 320 */ 321 @Override hashCode()322 public int hashCode() { 323 return Objects.hash(enabled, disabled); 324 } 325 326 327 /** 328 * @hide 329 */ 330 @Override toString()331 public String toString() { 332 return "ChangeConfig{enabled=" + enabled + ", disabled=" + disabled + '}'; 333 } 334 } 335 336 private static class OverrideCallbacks implements BehaviorChangeDelegate { 337 private final BehaviorChangeDelegate delegate; 338 private final ChangeConfig changeConfig; 339 OverrideCallbacks(BehaviorChangeDelegate delegate, ChangeConfig changeConfig)340 private OverrideCallbacks(BehaviorChangeDelegate delegate, ChangeConfig changeConfig) { 341 this.delegate = Objects.requireNonNull(delegate); 342 this.changeConfig = Objects.requireNonNull(changeConfig); 343 } 344 @Override isChangeEnabled(long changeId)345 public boolean isChangeEnabled(long changeId) { 346 if (changeConfig.isForceEnabled(changeId)) { 347 return true; 348 } 349 if (changeConfig.isForceDisabled(changeId)) { 350 return false; 351 } 352 return delegate.isChangeEnabled(changeId); 353 } 354 } 355 } 356