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