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