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