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