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