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