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 com.android.server.compat;
18 
19 import android.compat.Compatibility.ChangeConfig;
20 import android.content.Context;
21 import android.content.pm.ApplicationInfo;
22 import android.os.Environment;
23 import android.os.RemoteException;
24 import android.text.TextUtils;
25 import android.util.LongArray;
26 import android.util.LongSparseArray;
27 import android.util.Slog;
28 
29 import com.android.internal.annotations.GuardedBy;
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.internal.compat.AndroidBuildClassifier;
32 import com.android.internal.compat.CompatibilityChangeConfig;
33 import com.android.internal.compat.CompatibilityChangeInfo;
34 import com.android.internal.compat.IOverrideValidator;
35 import com.android.internal.compat.OverrideAllowedState;
36 import com.android.server.compat.config.Change;
37 import com.android.server.compat.config.XmlParser;
38 
39 import org.xmlpull.v1.XmlPullParserException;
40 
41 import java.io.BufferedInputStream;
42 import java.io.File;
43 import java.io.FileInputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.PrintWriter;
47 import java.util.HashSet;
48 import java.util.Set;
49 
50 import javax.xml.datatype.DatatypeConfigurationException;
51 
52 /**
53  * This class maintains state relating to platform compatibility changes.
54  *
55  * <p>It stores the default configuration for each change, and any per-package overrides that have
56  * been configured.
57  */
58 final class CompatConfig {
59 
60     private static final String TAG = "CompatConfig";
61 
62     @GuardedBy("mChanges")
63     private final LongSparseArray<CompatChange> mChanges = new LongSparseArray<>();
64 
65     private IOverrideValidator mOverrideValidator;
66 
67     @VisibleForTesting
CompatConfig(AndroidBuildClassifier androidBuildClassifier, Context context)68     CompatConfig(AndroidBuildClassifier androidBuildClassifier, Context context) {
69         mOverrideValidator = new OverrideValidatorImpl(androidBuildClassifier, context, this);
70     }
71 
72     /**
73      * Add a change. This is intended to be used by code that reads change config from the
74      * filesystem. This should be done at system startup time.
75      *
76      * @param change The change to add. Any change with the same ID will be overwritten.
77      */
addChange(CompatChange change)78     void addChange(CompatChange change) {
79         synchronized (mChanges) {
80             mChanges.put(change.getId(), change);
81         }
82     }
83 
84     /**
85      * Retrieves the set of disabled changes for a given app. Any change ID not in the returned
86      * array is by default enabled for the app.
87      *
88      * @param app The app in question
89      * @return A sorted long array of change IDs. We use a primitive array to minimize memory
90      * footprint: Every app process will store this array statically so we aim to reduce
91      * overhead as much as possible.
92      */
getDisabledChanges(ApplicationInfo app)93     long[] getDisabledChanges(ApplicationInfo app) {
94         LongArray disabled = new LongArray();
95         synchronized (mChanges) {
96             for (int i = 0; i < mChanges.size(); ++i) {
97                 CompatChange c = mChanges.valueAt(i);
98                 if (!c.isEnabled(app)) {
99                     disabled.add(c.getId());
100                 }
101             }
102         }
103         // Note: we don't need to explicitly sort the array, as the behaviour of LongSparseArray
104         // (mChanges) ensures it's already sorted.
105         return disabled.toArray();
106     }
107 
108     /**
109      * Look up a change ID by name.
110      *
111      * @param name Name of the change to look up
112      * @return The change ID, or {@code -1} if no change with that name exists.
113      */
lookupChangeId(String name)114     long lookupChangeId(String name) {
115         synchronized (mChanges) {
116             for (int i = 0; i < mChanges.size(); ++i) {
117                 if (TextUtils.equals(mChanges.valueAt(i).getName(), name)) {
118                     return mChanges.keyAt(i);
119                 }
120             }
121         }
122         return -1;
123     }
124 
125     /**
126      * Find if a given change is enabled for a given application.
127      *
128      * @param changeId The ID of the change in question
129      * @param app      App to check for
130      * @return {@code true} if the change is enabled for this app. Also returns {@code true} if the
131      * change ID is not known, as unknown changes are enabled by default.
132      */
isChangeEnabled(long changeId, ApplicationInfo app)133     boolean isChangeEnabled(long changeId, ApplicationInfo app) {
134         synchronized (mChanges) {
135             CompatChange c = mChanges.get(changeId);
136             if (c == null) {
137                 // we know nothing about this change: default behaviour is enabled.
138                 return true;
139             }
140             return c.isEnabled(app);
141         }
142     }
143 
144     /**
145      * Overrides the enabled state for a given change and app. This method is intended to be used
146      * *only* for debugging purposes, ultimately invoked either by an adb command, or from some
147      * developer settings UI.
148      *
149      * <p>Note, package overrides are not persistent and will be lost on system or runtime restart.
150      *
151      * @param changeId    The ID of the change to be overridden. Note, this call will succeed even
152      *                    if
153      *                    this change is not known; it will only have any effect if any code in the
154      *                    platform is gated on the ID given.
155      * @param packageName The app package name to override the change for.
156      * @param enabled     If the change should be enabled or disabled.
157      * @return {@code true} if the change existed before adding the override.
158      */
addOverride(long changeId, String packageName, boolean enabled)159     boolean addOverride(long changeId, String packageName, boolean enabled)
160             throws RemoteException, SecurityException {
161         boolean alreadyKnown = true;
162         OverrideAllowedState allowedState =
163                 mOverrideValidator.getOverrideAllowedState(changeId, packageName);
164         allowedState.enforce(changeId, packageName);
165         synchronized (mChanges) {
166             CompatChange c = mChanges.get(changeId);
167             if (c == null) {
168                 alreadyKnown = false;
169                 c = new CompatChange(changeId);
170                 addChange(c);
171             }
172             c.addPackageOverride(packageName, enabled);
173         }
174         return alreadyKnown;
175     }
176 
177     /**
178      * Check whether the change is known to the compat config.
179      *
180      * @return {@code true} if the change is known.
181      */
isKnownChangeId(long changeId)182     boolean isKnownChangeId(long changeId) {
183         synchronized (mChanges) {
184             CompatChange c = mChanges.get(changeId);
185             return c != null;
186         }
187     }
188 
189     /**
190      * Returns the minimum sdk version for which this change should be enabled (or 0 if it is not
191      * target sdk gated).
192      */
minTargetSdkForChangeId(long changeId)193     int minTargetSdkForChangeId(long changeId) {
194         synchronized (mChanges) {
195             CompatChange c = mChanges.get(changeId);
196             if (c == null) {
197                 return 0;
198             }
199             return c.getEnableAfterTargetSdk();
200         }
201     }
202 
203     /**
204      * Returns whether the change is marked as logging only.
205      */
isLoggingOnly(long changeId)206     boolean isLoggingOnly(long changeId) {
207         synchronized (mChanges) {
208             CompatChange c = mChanges.get(changeId);
209             if (c == null) {
210                 return false;
211             }
212             return c.getLoggingOnly();
213         }
214     }
215 
216     /**
217      * Returns whether the change is marked as disabled.
218      */
isDisabled(long changeId)219     boolean isDisabled(long changeId) {
220         synchronized (mChanges) {
221             CompatChange c = mChanges.get(changeId);
222             if (c == null) {
223                 return false;
224             }
225             return c.getDisabled();
226         }
227     }
228 
229     /**
230      * Removes an override previously added via {@link #addOverride(long, String, boolean)}. This
231      * restores the default behaviour for the given change and app, once any app processes have been
232      * restarted.
233      *
234      * @param changeId    The ID of the change that was overridden.
235      * @param packageName The app package name that was overridden.
236      * @return {@code true} if an override existed;
237      */
removeOverride(long changeId, String packageName)238     boolean removeOverride(long changeId, String packageName)
239             throws RemoteException, SecurityException {
240         boolean overrideExists = false;
241         synchronized (mChanges) {
242             CompatChange c = mChanges.get(changeId);
243             try {
244                 if (c != null) {
245                     overrideExists = c.hasOverride(packageName);
246                     if (overrideExists) {
247                         OverrideAllowedState allowedState =
248                                 mOverrideValidator.getOverrideAllowedState(changeId, packageName);
249                         allowedState.enforce(changeId, packageName);
250                         c.removePackageOverride(packageName);
251                     }
252                 }
253             } catch (RemoteException e) {
254                 // Should never occur, since validator is in the same process.
255                 throw new RuntimeException("Unable to call override validator!", e);
256             }
257         }
258         return overrideExists;
259     }
260 
261     /**
262      * Overrides the enabled state for a given change and app.
263      *
264      * <p>Note, package overrides are not persistent and will be lost on system or runtime restart.
265      *
266      * @param overrides   list of overrides to default changes config.
267      * @param packageName app for which the overrides will be applied.
268      */
addOverrides(CompatibilityChangeConfig overrides, String packageName)269     void addOverrides(CompatibilityChangeConfig overrides, String packageName)
270             throws RemoteException, SecurityException {
271         synchronized (mChanges) {
272             for (Long changeId : overrides.enabledChanges()) {
273                 addOverride(changeId, packageName, true);
274             }
275             for (Long changeId : overrides.disabledChanges()) {
276                 addOverride(changeId, packageName, false);
277 
278             }
279         }
280     }
281 
282     /**
283      * Removes all overrides previously added via {@link #addOverride(long, String, boolean)} or
284      * {@link #addOverrides(CompatibilityChangeConfig, String)} for a certain package.
285      *
286      * <p>This restores the default behaviour for the given change and app, once any app
287      * processes have been restarted.
288      *
289      * @param packageName The package for which the overrides should be purged.
290      */
removePackageOverrides(String packageName)291     void removePackageOverrides(String packageName) throws RemoteException, SecurityException {
292         synchronized (mChanges) {
293             for (int i = 0; i < mChanges.size(); ++i) {
294                 try {
295                     CompatChange change = mChanges.valueAt(i);
296                     if (change.hasOverride(packageName)) {
297                         OverrideAllowedState allowedState =
298                                 mOverrideValidator.getOverrideAllowedState(change.getId(),
299                                         packageName);
300                         allowedState.enforce(change.getId(), packageName);
301                         if (change != null) {
302                             mChanges.valueAt(i).removePackageOverride(packageName);
303                         }
304                     }
305                 } catch (RemoteException e) {
306                     // Should never occur, since validator is in the same process.
307                     throw new RuntimeException("Unable to call override validator!", e);
308                 }
309             }
310         }
311     }
312 
getAllowedChangesAfterTargetSdkForPackage(String packageName, int targetSdkVersion)313     private long[] getAllowedChangesAfterTargetSdkForPackage(String packageName,
314                                                              int targetSdkVersion)
315                     throws RemoteException {
316         LongArray allowed = new LongArray();
317         synchronized (mChanges) {
318             for (int i = 0; i < mChanges.size(); ++i) {
319                 try {
320                     CompatChange change = mChanges.valueAt(i);
321                     if (change.getEnableAfterTargetSdk() != targetSdkVersion) {
322                         continue;
323                     }
324                     OverrideAllowedState allowedState =
325                             mOverrideValidator.getOverrideAllowedState(change.getId(),
326                                                                        packageName);
327                     if (allowedState.state == OverrideAllowedState.ALLOWED) {
328                         allowed.add(change.getId());
329                     }
330                 } catch (RemoteException e) {
331                     // Should never occur, since validator is in the same process.
332                     throw new RuntimeException("Unable to call override validator!", e);
333                 }
334             }
335         }
336         return allowed.toArray();
337     }
338 
339     /**
340      * Enables all changes with enabledAfterTargetSdk == {@param targetSdkVersion} for
341      * {@param packageName}.
342      *
343      * @return The number of changes that were toggled.
344      */
enableTargetSdkChangesForPackage(String packageName, int targetSdkVersion)345     int enableTargetSdkChangesForPackage(String packageName, int targetSdkVersion)
346             throws RemoteException {
347         long[] changes = getAllowedChangesAfterTargetSdkForPackage(packageName, targetSdkVersion);
348         for (long changeId : changes) {
349             addOverride(changeId, packageName, true);
350         }
351         return changes.length;
352     }
353 
354 
355     /**
356      * Disables all changes with enabledAfterTargetSdk == {@param targetSdkVersion} for
357      * {@param packageName}.
358      *
359      * @return The number of changes that were toggled.
360      */
disableTargetSdkChangesForPackage(String packageName, int targetSdkVersion)361     int disableTargetSdkChangesForPackage(String packageName, int targetSdkVersion)
362             throws RemoteException {
363         long[] changes = getAllowedChangesAfterTargetSdkForPackage(packageName, targetSdkVersion);
364         for (long changeId : changes) {
365             addOverride(changeId, packageName, false);
366         }
367         return changes.length;
368     }
369 
registerListener(long changeId, CompatChange.ChangeListener listener)370     boolean registerListener(long changeId, CompatChange.ChangeListener listener) {
371         boolean alreadyKnown = true;
372         synchronized (mChanges) {
373             CompatChange c = mChanges.get(changeId);
374             if (c == null) {
375                 alreadyKnown = false;
376                 c = new CompatChange(changeId);
377                 addChange(c);
378             }
379             c.registerListener(listener);
380         }
381         return alreadyKnown;
382     }
383 
384     @VisibleForTesting
clearChanges()385     void clearChanges() {
386         synchronized (mChanges) {
387             mChanges.clear();
388         }
389     }
390 
391     /**
392      * Dumps the current list of compatibility config information.
393      *
394      * @param pw The {@link PrintWriter} instance to which the information will be dumped.
395      */
dumpConfig(PrintWriter pw)396     void dumpConfig(PrintWriter pw) {
397         synchronized (mChanges) {
398             if (mChanges.size() == 0) {
399                 pw.println("No compat overrides.");
400                 return;
401             }
402             for (int i = 0; i < mChanges.size(); ++i) {
403                 CompatChange c = mChanges.valueAt(i);
404                 pw.println(c.toString());
405             }
406         }
407     }
408 
409     /**
410      * Get the config for a given app.
411      *
412      * @param applicationInfo the {@link ApplicationInfo} for which the info should be dumped.
413      * @return A {@link CompatibilityChangeConfig} which contains the compat config info for the
414      * given app.
415      */
416 
getAppConfig(ApplicationInfo applicationInfo)417     CompatibilityChangeConfig getAppConfig(ApplicationInfo applicationInfo) {
418         Set<Long> enabled = new HashSet<>();
419         Set<Long> disabled = new HashSet<>();
420         synchronized (mChanges) {
421             for (int i = 0; i < mChanges.size(); ++i) {
422                 CompatChange c = mChanges.valueAt(i);
423                 if (c.isEnabled(applicationInfo)) {
424                     enabled.add(c.getId());
425                 } else {
426                     disabled.add(c.getId());
427                 }
428             }
429         }
430         return new CompatibilityChangeConfig(new ChangeConfig(enabled, disabled));
431     }
432 
433     /**
434      * Dumps all the compatibility change information.
435      *
436      * @return An array of {@link CompatibilityChangeInfo} with the current changes.
437      */
dumpChanges()438     CompatibilityChangeInfo[] dumpChanges() {
439         synchronized (mChanges) {
440             CompatibilityChangeInfo[] changeInfos = new CompatibilityChangeInfo[mChanges.size()];
441             for (int i = 0; i < mChanges.size(); ++i) {
442                 CompatChange change = mChanges.valueAt(i);
443                 changeInfos[i] = new CompatibilityChangeInfo(change.getId(),
444                         change.getName(),
445                         change.getEnableAfterTargetSdk(),
446                         change.getDisabled(),
447                         change.getLoggingOnly(),
448                         change.getDescription());
449             }
450             return changeInfos;
451         }
452     }
453 
create(AndroidBuildClassifier androidBuildClassifier, Context context)454     static CompatConfig create(AndroidBuildClassifier androidBuildClassifier, Context context) {
455         CompatConfig config = new CompatConfig(androidBuildClassifier, context);
456         config.initConfigFromLib(Environment.buildPath(
457                 Environment.getRootDirectory(), "etc", "compatconfig"));
458         return config;
459     }
460 
initConfigFromLib(File libraryDir)461     void initConfigFromLib(File libraryDir) {
462         if (!libraryDir.exists() || !libraryDir.isDirectory()) {
463             Slog.e(TAG, "No directory " + libraryDir + ", skipping");
464             return;
465         }
466         for (File f : libraryDir.listFiles()) {
467             Slog.d(TAG, "Found a config file: " + f.getPath());
468             //TODO(b/138222363): Handle duplicate ids across config files.
469             readConfig(f);
470         }
471     }
472 
readConfig(File configFile)473     private void readConfig(File configFile) {
474         try (InputStream in = new BufferedInputStream(new FileInputStream(configFile))) {
475             for (Change change : XmlParser.read(in).getCompatChange()) {
476                 Slog.d(TAG, "Adding: " + change.toString());
477                 addChange(new CompatChange(change));
478             }
479         } catch (IOException | DatatypeConfigurationException | XmlPullParserException e) {
480             Slog.e(TAG, "Encountered an error while reading/parsing compat config file", e);
481         }
482     }
483 
getOverrideValidator()484     IOverrideValidator getOverrideValidator() {
485         return mOverrideValidator;
486     }
487 }
488