1 /* 2 * Copyright (C) 2013 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.app; 18 19 import static android.app.ActivityThread.DEBUG_CONFIGURATION; 20 import static android.view.Display.DEFAULT_DISPLAY; 21 import static android.view.Display.INVALID_DISPLAY; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.compat.annotation.UnsupportedAppUsage; 26 import android.content.pm.ActivityInfo; 27 import android.content.pm.ApplicationInfo; 28 import android.content.res.ApkAssets; 29 import android.content.res.AssetManager; 30 import android.content.res.CompatResources; 31 import android.content.res.CompatibilityInfo; 32 import android.content.res.Configuration; 33 import android.content.res.Resources; 34 import android.content.res.ResourcesImpl; 35 import android.content.res.ResourcesKey; 36 import android.content.res.loader.ResourcesLoader; 37 import android.hardware.display.DisplayManagerGlobal; 38 import android.os.IBinder; 39 import android.os.Process; 40 import android.os.Trace; 41 import android.util.ArrayMap; 42 import android.util.ArraySet; 43 import android.util.DisplayMetrics; 44 import android.util.Log; 45 import android.util.Pair; 46 import android.util.Slog; 47 import android.view.Display; 48 import android.view.DisplayAdjustments; 49 import android.view.DisplayInfo; 50 import android.window.WindowContext; 51 52 import com.android.internal.annotations.VisibleForTesting; 53 import com.android.internal.util.ArrayUtils; 54 import com.android.internal.util.IndentingPrintWriter; 55 56 import java.io.IOException; 57 import java.io.PrintWriter; 58 import java.lang.ref.Reference; 59 import java.lang.ref.ReferenceQueue; 60 import java.lang.ref.WeakReference; 61 import java.util.ArrayList; 62 import java.util.Arrays; 63 import java.util.Collection; 64 import java.util.HashSet; 65 import java.util.List; 66 import java.util.Objects; 67 import java.util.WeakHashMap; 68 import java.util.function.Consumer; 69 import java.util.function.Function; 70 71 /** @hide */ 72 public class ResourcesManager { 73 static final String TAG = "ResourcesManager"; 74 private static final boolean DEBUG = false; 75 76 private static ResourcesManager sResourcesManager; 77 78 /** 79 * Internal lock object 80 */ 81 private final Object mLock = new Object(); 82 83 /** 84 * The global compatibility settings. 85 */ 86 private CompatibilityInfo mResCompatibilityInfo; 87 88 /** 89 * The global configuration upon which all Resources are based. Multi-window Resources 90 * apply their overrides to this configuration. 91 */ 92 @UnsupportedAppUsage 93 private final Configuration mResConfiguration = new Configuration(); 94 95 /** 96 * The display upon which all Resources are based. Activity, window token, and display context 97 * resources apply their overrides to this display id. 98 */ 99 private int mResDisplayId = DEFAULT_DISPLAY; 100 101 /** 102 * ApplicationInfo changes that need to be applied to Resources when the next configuration 103 * change occurs. 104 */ 105 private ArrayList<Pair<String[], ApplicationInfo>> mPendingAppInfoUpdates; 106 107 /** 108 * A mapping of ResourceImpls and their configurations. These are heavy weight objects 109 * which should be reused as much as possible. 110 */ 111 @UnsupportedAppUsage 112 private final ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls = 113 new ArrayMap<>(); 114 115 /** 116 * A list of Resource references that can be reused. 117 */ 118 @UnsupportedAppUsage 119 private final ArrayList<WeakReference<Resources>> mResourceReferences = new ArrayList<>(); 120 private final ReferenceQueue<Resources> mResourcesReferencesQueue = new ReferenceQueue<>(); 121 122 private static class ApkKey { 123 public final String path; 124 public final boolean sharedLib; 125 public final boolean overlay; 126 ApkKey(String path, boolean sharedLib, boolean overlay)127 ApkKey(String path, boolean sharedLib, boolean overlay) { 128 this.path = path; 129 this.sharedLib = sharedLib; 130 this.overlay = overlay; 131 } 132 133 @Override hashCode()134 public int hashCode() { 135 int result = 1; 136 result = 31 * result + this.path.hashCode(); 137 result = 31 * result + Boolean.hashCode(this.sharedLib); 138 result = 31 * result + Boolean.hashCode(this.overlay); 139 return result; 140 } 141 142 @Override equals(@ullable Object obj)143 public boolean equals(@Nullable Object obj) { 144 if (!(obj instanceof ApkKey)) { 145 return false; 146 } 147 ApkKey other = (ApkKey) obj; 148 return this.path.equals(other.path) && this.sharedLib == other.sharedLib 149 && this.overlay == other.overlay; 150 } 151 } 152 153 /** 154 * Loads {@link ApkAssets} and caches them to prevent their garbage collection while the 155 * instance is alive and reachable. 156 */ 157 private class ApkAssetsSupplier { 158 final ArrayMap<ApkKey, ApkAssets> mLocalCache = new ArrayMap<>(); 159 160 /** 161 * Retrieves the {@link ApkAssets} corresponding to the specified key, caches the ApkAssets 162 * within this instance, and inserts the loaded ApkAssets into the {@link #mCachedApkAssets} 163 * cache. 164 */ load(final ApkKey apkKey)165 ApkAssets load(final ApkKey apkKey) throws IOException { 166 ApkAssets apkAssets = mLocalCache.get(apkKey); 167 if (apkAssets == null) { 168 apkAssets = loadApkAssets(apkKey); 169 mLocalCache.put(apkKey, apkAssets); 170 } 171 return apkAssets; 172 } 173 } 174 175 /** 176 * The ApkAssets that are being referenced in the wild that we can reuse. 177 */ 178 private final ArrayMap<ApkKey, WeakReference<ApkAssets>> mCachedApkAssets = new ArrayMap<>(); 179 180 /** 181 * Class containing the base configuration override and set of resources associated with an 182 * {@link Activity} or a {@link WindowContext}. 183 */ 184 private static class ActivityResources { 185 /** 186 * Override config to apply to all resources associated with the token this instance is 187 * based on. 188 * 189 * @see #activityResources 190 * @see #getResources(IBinder, String, String[], String[], String[], String[], Integer, 191 * Configuration, CompatibilityInfo, ClassLoader, List) 192 */ 193 public final Configuration overrideConfig = new Configuration(); 194 195 /** 196 * The display to apply to all resources associated with the token this instance is based 197 * on. 198 */ 199 public int overrideDisplayId; 200 201 /** List of {@link ActivityResource} associated with the token this instance is based on. */ 202 public final ArrayList<ActivityResource> activityResources = new ArrayList<>(); 203 204 public final ReferenceQueue<Resources> activityResourcesQueue = new ReferenceQueue<>(); 205 206 @UnsupportedAppUsage ActivityResources()207 private ActivityResources() {} 208 209 /** Returns the number of live resource references within {@code activityResources}. */ countLiveReferences()210 public int countLiveReferences() { 211 int count = 0; 212 for (int i = 0; i < activityResources.size(); i++) { 213 WeakReference<Resources> resources = activityResources.get(i).resources; 214 if (resources != null && resources.get() != null) { 215 count++; 216 } 217 } 218 return count; 219 } 220 } 221 222 /** 223 * Contains a resource derived from an {@link Activity} or {@link WindowContext} and information 224 * about how this resource expects its configuration to differ from the token's. 225 * 226 * @see ActivityResources 227 */ 228 // TODO: Ideally this class should be called something token related, like TokenBasedResource. 229 private static class ActivityResource { 230 /** 231 * The override configuration applied on top of the token's override config for this 232 * resource. 233 */ 234 public final Configuration overrideConfig = new Configuration(); 235 236 /** 237 * If non-null this resource expects its configuration to override the display from the 238 * token's configuration. 239 * 240 * @see #applyDisplayMetricsToConfiguration(DisplayMetrics, Configuration) 241 */ 242 @Nullable 243 public Integer overrideDisplayId; 244 245 @Nullable 246 public WeakReference<Resources> resources; 247 ActivityResource()248 private ActivityResource() {} 249 } 250 251 /** 252 * Each Activity or WindowToken may has a base override configuration that is applied to each 253 * Resources object, which in turn may have their own override configuration specified. 254 */ 255 @UnsupportedAppUsage 256 private final WeakHashMap<IBinder, ActivityResources> mActivityResourceReferences = 257 new WeakHashMap<>(); 258 259 /** 260 * Callback implementation for handling updates to Resources objects. 261 */ 262 private final UpdateHandler mUpdateCallbacks = new UpdateHandler(); 263 264 /** 265 * The set of APK paths belonging to this process. This is used to disable incremental 266 * installation crash protections on these APKs so the app either behaves as expects or crashes. 267 */ 268 private final ArraySet<String> mApplicationOwnedApks = new ArraySet<>(); 269 270 @UnsupportedAppUsage ResourcesManager()271 public ResourcesManager() { 272 } 273 274 @UnsupportedAppUsage getInstance()275 public static ResourcesManager getInstance() { 276 synchronized (ResourcesManager.class) { 277 if (sResourcesManager == null) { 278 sResourcesManager = new ResourcesManager(); 279 } 280 return sResourcesManager; 281 } 282 } 283 284 /** 285 * Invalidate and destroy any resources that reference content under the 286 * given filesystem path. Typically used when unmounting a storage device to 287 * try as hard as possible to release any open FDs. 288 */ invalidatePath(String path)289 public void invalidatePath(String path) { 290 synchronized (mLock) { 291 int count = 0; 292 293 for (int i = mResourceImpls.size() - 1; i >= 0; i--) { 294 final ResourcesKey key = mResourceImpls.keyAt(i); 295 if (key.isPathReferenced(path)) { 296 ResourcesImpl impl = mResourceImpls.removeAt(i).get(); 297 if (impl != null) { 298 impl.flushLayoutCache(); 299 } 300 count++; 301 } 302 } 303 304 Log.i(TAG, "Invalidated " + count + " asset managers that referenced " + path); 305 306 for (int i = mCachedApkAssets.size() - 1; i >= 0; i--) { 307 final ApkKey key = mCachedApkAssets.keyAt(i); 308 if (key.path.equals(path)) { 309 WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.removeAt(i); 310 if (apkAssetsRef != null && apkAssetsRef.get() != null) { 311 apkAssetsRef.get().close(); 312 } 313 } 314 } 315 } 316 } 317 getConfiguration()318 public Configuration getConfiguration() { 319 synchronized (mLock) { 320 return mResConfiguration; 321 } 322 } 323 324 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) getDisplayMetrics()325 public DisplayMetrics getDisplayMetrics() { 326 return getDisplayMetrics(mResDisplayId, DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS); 327 } 328 329 /** 330 * Protected so that tests can override and returns something a fixed value. 331 */ 332 @VisibleForTesting getDisplayMetrics(int displayId, DisplayAdjustments da)333 protected @NonNull DisplayMetrics getDisplayMetrics(int displayId, DisplayAdjustments da) { 334 final DisplayManagerGlobal displayManagerGlobal = DisplayManagerGlobal.getInstance(); 335 final DisplayMetrics dm = new DisplayMetrics(); 336 final DisplayInfo displayInfo = displayManagerGlobal != null 337 ? displayManagerGlobal.getDisplayInfo(displayId) : null; 338 if (displayInfo != null) { 339 displayInfo.getAppMetrics(dm, da); 340 } else { 341 dm.setToDefaults(); 342 } 343 return dm; 344 } 345 applyDisplayMetricsToConfiguration(@onNull DisplayMetrics dm, @NonNull Configuration config)346 private static void applyDisplayMetricsToConfiguration(@NonNull DisplayMetrics dm, 347 @NonNull Configuration config) { 348 config.touchscreen = Configuration.TOUCHSCREEN_NOTOUCH; 349 config.densityDpi = dm.densityDpi; 350 config.screenWidthDp = (int) (dm.widthPixels / dm.density); 351 config.screenHeightDp = (int) (dm.heightPixels / dm.density); 352 int sl = Configuration.resetScreenLayout(config.screenLayout); 353 if (dm.widthPixels > dm.heightPixels) { 354 config.orientation = Configuration.ORIENTATION_LANDSCAPE; 355 config.screenLayout = Configuration.reduceScreenLayout(sl, 356 config.screenWidthDp, config.screenHeightDp); 357 } else { 358 config.orientation = Configuration.ORIENTATION_PORTRAIT; 359 config.screenLayout = Configuration.reduceScreenLayout(sl, 360 config.screenHeightDp, config.screenWidthDp); 361 } 362 config.smallestScreenWidthDp = Math.min(config.screenWidthDp, config.screenHeightDp); 363 config.compatScreenWidthDp = config.screenWidthDp; 364 config.compatScreenHeightDp = config.screenHeightDp; 365 config.compatSmallestScreenWidthDp = config.smallestScreenWidthDp; 366 } 367 applyCompatConfiguration(int displayDensity, @NonNull Configuration compatConfiguration)368 public boolean applyCompatConfiguration(int displayDensity, 369 @NonNull Configuration compatConfiguration) { 370 synchronized (mLock) { 371 if (mResCompatibilityInfo != null && !mResCompatibilityInfo.supportsScreen()) { 372 mResCompatibilityInfo.applyToConfiguration(displayDensity, compatConfiguration); 373 return true; 374 } 375 return false; 376 } 377 } 378 379 /** 380 * Returns an adjusted {@link Display} object based on the inputs or null if display isn't 381 * available. 382 * 383 * @param displayId display Id. 384 * @param resources The {@link Resources} backing the display adjustments. 385 */ getAdjustedDisplay(final int displayId, Resources resources)386 public Display getAdjustedDisplay(final int displayId, Resources resources) { 387 synchronized (mLock) { 388 final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance(); 389 if (dm == null) { 390 // may be null early in system startup 391 return null; 392 } 393 return dm.getCompatibleDisplay(displayId, resources); 394 } 395 } 396 397 /** 398 * Initializes the set of APKs owned by the application running in this process. 399 */ initializeApplicationPaths(@onNull String sourceDir, @Nullable String[] splitDirs)400 public void initializeApplicationPaths(@NonNull String sourceDir, 401 @Nullable String[] splitDirs) { 402 synchronized (mLock) { 403 if (mApplicationOwnedApks.isEmpty()) { 404 addApplicationPathsLocked(sourceDir, splitDirs); 405 } 406 } 407 } 408 409 /** 410 * Updates the set of APKs owned by the application running in this process. 411 * 412 * This method only appends to the set of APKs owned by this process because the previous APKs 413 * paths still belong to the application running in this process. 414 */ addApplicationPathsLocked(@onNull String sourceDir, @Nullable String[] splitDirs)415 private void addApplicationPathsLocked(@NonNull String sourceDir, 416 @Nullable String[] splitDirs) { 417 mApplicationOwnedApks.add(sourceDir); 418 if (splitDirs != null) { 419 mApplicationOwnedApks.addAll(Arrays.asList(splitDirs)); 420 } 421 } 422 overlayPathToIdmapPath(String path)423 private static String overlayPathToIdmapPath(String path) { 424 return "/data/resource-cache/" + path.substring(1).replace('/', '@') + "@idmap"; 425 } 426 loadApkAssets(@onNull final ApkKey key)427 private @NonNull ApkAssets loadApkAssets(@NonNull final ApkKey key) throws IOException { 428 ApkAssets apkAssets; 429 430 // Optimistically check if this ApkAssets exists somewhere else. 431 synchronized (mLock) { 432 final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.get(key); 433 if (apkAssetsRef != null) { 434 apkAssets = apkAssetsRef.get(); 435 if (apkAssets != null && apkAssets.isUpToDate()) { 436 return apkAssets; 437 } else { 438 // Clean up the reference. 439 mCachedApkAssets.remove(key); 440 } 441 } 442 } 443 444 int flags = 0; 445 if (key.sharedLib) { 446 flags |= ApkAssets.PROPERTY_DYNAMIC; 447 } 448 if (mApplicationOwnedApks.contains(key.path)) { 449 flags |= ApkAssets.PROPERTY_DISABLE_INCREMENTAL_HARDENING; 450 } 451 if (key.overlay) { 452 apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(key.path), flags); 453 } else { 454 apkAssets = ApkAssets.loadFromPath(key.path, flags); 455 } 456 457 synchronized (mLock) { 458 mCachedApkAssets.put(key, new WeakReference<>(apkAssets)); 459 } 460 461 return apkAssets; 462 } 463 464 /** 465 * Retrieves a list of apk keys representing the ApkAssets that should be loaded for 466 * AssetManagers mapped to the {@param key}. 467 */ extractApkKeys(@onNull final ResourcesKey key)468 private static @NonNull ArrayList<ApkKey> extractApkKeys(@NonNull final ResourcesKey key) { 469 final ArrayList<ApkKey> apkKeys = new ArrayList<>(); 470 471 // resDir can be null if the 'android' package is creating a new Resources object. 472 // This is fine, since each AssetManager automatically loads the 'android' package 473 // already. 474 if (key.mResDir != null) { 475 apkKeys.add(new ApkKey(key.mResDir, false /*sharedLib*/, false /*overlay*/)); 476 } 477 478 if (key.mSplitResDirs != null) { 479 for (final String splitResDir : key.mSplitResDirs) { 480 apkKeys.add(new ApkKey(splitResDir, false /*sharedLib*/, false /*overlay*/)); 481 } 482 } 483 484 if (key.mLibDirs != null) { 485 for (final String libDir : key.mLibDirs) { 486 // Avoid opening files we know do not have resources, like code-only .jar files. 487 if (libDir.endsWith(".apk")) { 488 apkKeys.add(new ApkKey(libDir, true /*sharedLib*/, false /*overlay*/)); 489 } 490 } 491 } 492 493 if (key.mOverlayPaths != null) { 494 for (final String idmapPath : key.mOverlayPaths) { 495 apkKeys.add(new ApkKey(idmapPath, false /*sharedLib*/, true /*overlay*/)); 496 } 497 } 498 499 return apkKeys; 500 } 501 502 /** 503 * Creates an AssetManager from the paths within the ResourcesKey. 504 * 505 * This can be overridden in tests so as to avoid creating a real AssetManager with 506 * real APK paths. 507 * @param key The key containing the resource paths to add to the AssetManager. 508 * @return a new AssetManager. 509 */ 510 @VisibleForTesting 511 @UnsupportedAppUsage createAssetManager(@onNull final ResourcesKey key)512 protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) { 513 return createAssetManager(key, /* apkSupplier */ null); 514 } 515 516 /** 517 * Variant of {@link #createAssetManager(ResourcesKey)} that attempts to load ApkAssets 518 * from an {@link ApkAssetsSupplier} if non-null; otherwise ApkAssets are loaded using 519 * {@link #loadApkAssets(ApkKey)}. 520 */ createAssetManager(@onNull final ResourcesKey key, @Nullable ApkAssetsSupplier apkSupplier)521 private @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key, 522 @Nullable ApkAssetsSupplier apkSupplier) { 523 final AssetManager.Builder builder = new AssetManager.Builder(); 524 525 final ArrayList<ApkKey> apkKeys = extractApkKeys(key); 526 for (int i = 0, n = apkKeys.size(); i < n; i++) { 527 final ApkKey apkKey = apkKeys.get(i); 528 try { 529 builder.addApkAssets( 530 (apkSupplier != null) ? apkSupplier.load(apkKey) : loadApkAssets(apkKey)); 531 } catch (IOException e) { 532 if (apkKey.overlay) { 533 Log.w(TAG, String.format("failed to add overlay path '%s'", apkKey.path), e); 534 } else if (apkKey.sharedLib) { 535 Log.w(TAG, String.format( 536 "asset path '%s' does not exist or contains no resources", 537 apkKey.path), e); 538 } else { 539 Log.e(TAG, String.format("failed to add asset path '%s'", apkKey.path), e); 540 return null; 541 } 542 } 543 } 544 545 if (key.mLoaders != null) { 546 for (final ResourcesLoader loader : key.mLoaders) { 547 builder.addLoader(loader); 548 } 549 } 550 551 return builder.build(); 552 } 553 countLiveReferences(Collection<WeakReference<T>> collection)554 private static <T> int countLiveReferences(Collection<WeakReference<T>> collection) { 555 int count = 0; 556 for (WeakReference<T> ref : collection) { 557 final T value = ref != null ? ref.get() : null; 558 if (value != null) { 559 count++; 560 } 561 } 562 return count; 563 } 564 565 /** 566 * @hide 567 */ dump(String prefix, PrintWriter printWriter)568 public void dump(String prefix, PrintWriter printWriter) { 569 synchronized (mLock) { 570 IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " "); 571 for (int i = 0; i < prefix.length() / 2; i++) { 572 pw.increaseIndent(); 573 } 574 575 pw.println("ResourcesManager:"); 576 pw.increaseIndent(); 577 pw.print("total apks: "); 578 pw.println(countLiveReferences(mCachedApkAssets.values())); 579 580 pw.print("resources: "); 581 582 int references = countLiveReferences(mResourceReferences); 583 for (ActivityResources activityResources : mActivityResourceReferences.values()) { 584 references += activityResources.countLiveReferences(); 585 } 586 pw.println(references); 587 588 pw.print("resource impls: "); 589 pw.println(countLiveReferences(mResourceImpls.values())); 590 } 591 } 592 generateConfig(@onNull ResourcesKey key)593 private Configuration generateConfig(@NonNull ResourcesKey key) { 594 Configuration config; 595 final boolean hasOverrideConfig = key.hasOverrideConfiguration(); 596 if (hasOverrideConfig) { 597 config = new Configuration(getConfiguration()); 598 config.updateFrom(key.mOverrideConfiguration); 599 if (DEBUG) Slog.v(TAG, "Applied overrideConfig=" + key.mOverrideConfiguration); 600 } else { 601 config = getConfiguration(); 602 } 603 return config; 604 } 605 generateDisplayId(@onNull ResourcesKey key)606 private int generateDisplayId(@NonNull ResourcesKey key) { 607 return key.mDisplayId != INVALID_DISPLAY ? key.mDisplayId : mResDisplayId; 608 } 609 createResourcesImpl(@onNull ResourcesKey key, @Nullable ApkAssetsSupplier apkSupplier)610 private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key, 611 @Nullable ApkAssetsSupplier apkSupplier) { 612 final AssetManager assets = createAssetManager(key, apkSupplier); 613 if (assets == null) { 614 return null; 615 } 616 617 final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration); 618 daj.setCompatibilityInfo(key.mCompatInfo); 619 620 final Configuration config = generateConfig(key); 621 final DisplayMetrics displayMetrics = getDisplayMetrics(generateDisplayId(key), daj); 622 final ResourcesImpl impl = new ResourcesImpl(assets, displayMetrics, config, daj); 623 624 if (DEBUG) { 625 Slog.d(TAG, "- creating impl=" + impl + " with key: " + key); 626 } 627 return impl; 628 } 629 630 /** 631 * Finds a cached ResourcesImpl object that matches the given ResourcesKey. 632 * 633 * @param key The key to match. 634 * @return a ResourcesImpl if the key matches a cache entry, null otherwise. 635 */ findResourcesImplForKeyLocked(@onNull ResourcesKey key)636 private @Nullable ResourcesImpl findResourcesImplForKeyLocked(@NonNull ResourcesKey key) { 637 WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.get(key); 638 ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null; 639 if (impl != null && impl.getAssets().isUpToDate()) { 640 return impl; 641 } 642 return null; 643 } 644 645 /** 646 * Finds a cached ResourcesImpl object that matches the given ResourcesKey, or 647 * creates a new one and caches it for future use. 648 * @param key The key to match. 649 * @return a ResourcesImpl object matching the key. 650 */ findOrCreateResourcesImplForKeyLocked( @onNull ResourcesKey key)651 private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked( 652 @NonNull ResourcesKey key) { 653 return findOrCreateResourcesImplForKeyLocked(key, /* apkSupplier */ null); 654 } 655 656 /** 657 * Variant of {@link #findOrCreateResourcesImplForKeyLocked(ResourcesKey)} that attempts to 658 * load ApkAssets from a {@link ApkAssetsSupplier} when creating a new ResourcesImpl. 659 */ findOrCreateResourcesImplForKeyLocked( @onNull ResourcesKey key, @Nullable ApkAssetsSupplier apkSupplier)660 private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked( 661 @NonNull ResourcesKey key, @Nullable ApkAssetsSupplier apkSupplier) { 662 ResourcesImpl impl = findResourcesImplForKeyLocked(key); 663 if (impl == null) { 664 impl = createResourcesImpl(key, apkSupplier); 665 if (impl != null) { 666 mResourceImpls.put(key, new WeakReference<>(impl)); 667 } 668 } 669 return impl; 670 } 671 672 /** 673 * Find the ResourcesKey that this ResourcesImpl object is associated with. 674 * @return the ResourcesKey or null if none was found. 675 */ findKeyForResourceImplLocked( @onNull ResourcesImpl resourceImpl)676 private @Nullable ResourcesKey findKeyForResourceImplLocked( 677 @NonNull ResourcesImpl resourceImpl) { 678 int refCount = mResourceImpls.size(); 679 for (int i = 0; i < refCount; i++) { 680 WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i); 681 ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null; 682 if (resourceImpl == impl) { 683 return mResourceImpls.keyAt(i); 684 } 685 } 686 return null; 687 } 688 689 /** 690 * Check if activity resources have same override config as the provided on. 691 * @param activityToken The Activity that resources should be associated with. 692 * @param overrideConfig The override configuration to be checked for equality with. 693 * @return true if activity resources override config matches the provided one or they are both 694 * null, false otherwise. 695 */ isSameResourcesOverrideConfig(@ullable IBinder activityToken, @Nullable Configuration overrideConfig)696 public boolean isSameResourcesOverrideConfig(@Nullable IBinder activityToken, 697 @Nullable Configuration overrideConfig) { 698 synchronized (mLock) { 699 final ActivityResources activityResources 700 = activityToken != null ? mActivityResourceReferences.get(activityToken) : null; 701 if (activityResources == null) { 702 return overrideConfig == null; 703 } else { 704 // The two configurations must either be equal or publicly equivalent to be 705 // considered the same. 706 return Objects.equals(activityResources.overrideConfig, overrideConfig) 707 || (overrideConfig != null && activityResources.overrideConfig != null 708 && 0 == overrideConfig.diffPublicOnly( 709 activityResources.overrideConfig)); 710 } 711 } 712 } 713 getOrCreateActivityResourcesStructLocked( @onNull IBinder activityToken)714 private ActivityResources getOrCreateActivityResourcesStructLocked( 715 @NonNull IBinder activityToken) { 716 ActivityResources activityResources = mActivityResourceReferences.get(activityToken); 717 if (activityResources == null) { 718 activityResources = new ActivityResources(); 719 mActivityResourceReferences.put(activityToken, activityResources); 720 } 721 return activityResources; 722 } 723 724 @Nullable findResourcesForActivityLocked(@onNull IBinder targetActivityToken, @NonNull ResourcesKey targetKey, @NonNull ClassLoader targetClassLoader)725 private Resources findResourcesForActivityLocked(@NonNull IBinder targetActivityToken, 726 @NonNull ResourcesKey targetKey, @NonNull ClassLoader targetClassLoader) { 727 ActivityResources activityResources = getOrCreateActivityResourcesStructLocked( 728 targetActivityToken); 729 730 final int size = activityResources.activityResources.size(); 731 for (int index = 0; index < size; index++) { 732 ActivityResource activityResource = activityResources.activityResources.get(index); 733 Resources resources = activityResource.resources.get(); 734 ResourcesKey key = resources == null ? null : findKeyForResourceImplLocked( 735 resources.getImpl()); 736 737 if (key != null 738 && Objects.equals(resources.getClassLoader(), targetClassLoader) 739 && Objects.equals(key, targetKey)) { 740 return resources; 741 } 742 } 743 744 return null; 745 } 746 747 @NonNull createResourcesForActivityLocked(@onNull IBinder activityToken, @NonNull Configuration initialOverrideConfig, @Nullable Integer overrideDisplayId, @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo)748 private Resources createResourcesForActivityLocked(@NonNull IBinder activityToken, 749 @NonNull Configuration initialOverrideConfig, @Nullable Integer overrideDisplayId, 750 @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl, 751 @NonNull CompatibilityInfo compatInfo) { 752 final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked( 753 activityToken); 754 cleanupReferences(activityResources.activityResources, 755 activityResources.activityResourcesQueue, 756 (r) -> r.resources); 757 758 Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader) 759 : new Resources(classLoader); 760 resources.setImpl(impl); 761 resources.setCallbacks(mUpdateCallbacks); 762 763 ActivityResource activityResource = new ActivityResource(); 764 activityResource.resources = new WeakReference<>(resources, 765 activityResources.activityResourcesQueue); 766 activityResource.overrideConfig.setTo(initialOverrideConfig); 767 activityResource.overrideDisplayId = overrideDisplayId; 768 activityResources.activityResources.add(activityResource); 769 if (DEBUG) { 770 Slog.d(TAG, "- creating new ref=" + resources); 771 Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl); 772 } 773 return resources; 774 } 775 createResourcesLocked(@onNull ClassLoader classLoader, @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo)776 private @NonNull Resources createResourcesLocked(@NonNull ClassLoader classLoader, 777 @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) { 778 cleanupReferences(mResourceReferences, mResourcesReferencesQueue); 779 780 Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader) 781 : new Resources(classLoader); 782 resources.setImpl(impl); 783 resources.setCallbacks(mUpdateCallbacks); 784 mResourceReferences.add(new WeakReference<>(resources, mResourcesReferencesQueue)); 785 if (DEBUG) { 786 Slog.d(TAG, "- creating new ref=" + resources); 787 Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl); 788 } 789 return resources; 790 } 791 792 /** 793 * Creates base resources for a binder token. Calls to 794 * 795 * {@link #getResources(IBinder, String, String[], String[], String[], String[], Integer, 796 * Configuration, CompatibilityInfo, ClassLoader, List)} with the same binder token will have 797 * their override configurations merged with the one specified here. 798 * 799 * @param token Represents an {@link Activity} or {@link WindowContext}. 800 * @param resDir The base resource path. Can be null (only framework resources will be loaded). 801 * @param splitResDirs An array of split resource paths. Can be null. 802 * @param legacyOverlayDirs An array of overlay APK paths. Can be null. 803 * @param overlayPaths An array of overlay APK and non-APK paths. Can be null. 804 * @param libDirs An array of resource library paths. Can be null. 805 * @param displayId The ID of the display for which to create the resources. 806 * @param overrideConfig The configuration to apply on top of the base configuration. Can be 807 * {@code null}. This provides the base override for this token. 808 * @param compatInfo The compatibility settings to use. Cannot be null. A default to use is 809 * {@link CompatibilityInfo#DEFAULT_COMPATIBILITY_INFO}. 810 * @param classLoader The class loader to use when inflating Resources. If null, the 811 * {@link ClassLoader#getSystemClassLoader()} is used. 812 * @return a Resources object from which to access resources. 813 */ createBaseTokenResources(@onNull IBinder token, @Nullable String resDir, @Nullable String[] splitResDirs, @Nullable String[] legacyOverlayDirs, @Nullable String[] overlayPaths, @Nullable String[] libDirs, int displayId, @Nullable Configuration overrideConfig, @NonNull CompatibilityInfo compatInfo, @Nullable ClassLoader classLoader, @Nullable List<ResourcesLoader> loaders)814 public @Nullable Resources createBaseTokenResources(@NonNull IBinder token, 815 @Nullable String resDir, 816 @Nullable String[] splitResDirs, 817 @Nullable String[] legacyOverlayDirs, 818 @Nullable String[] overlayPaths, 819 @Nullable String[] libDirs, 820 int displayId, 821 @Nullable Configuration overrideConfig, 822 @NonNull CompatibilityInfo compatInfo, 823 @Nullable ClassLoader classLoader, 824 @Nullable List<ResourcesLoader> loaders) { 825 try { 826 Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, 827 "ResourcesManager#createBaseActivityResources"); 828 final ResourcesKey key = new ResourcesKey( 829 resDir, 830 splitResDirs, 831 combinedOverlayPaths(legacyOverlayDirs, overlayPaths), 832 libDirs, 833 displayId, 834 overrideConfig, 835 compatInfo, 836 loaders == null ? null : loaders.toArray(new ResourcesLoader[0])); 837 classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader(); 838 839 if (DEBUG) { 840 Slog.d(TAG, "createBaseActivityResources activity=" + token 841 + " with key=" + key); 842 } 843 844 synchronized (mLock) { 845 // Force the creation of an ActivityResourcesStruct. 846 getOrCreateActivityResourcesStructLocked(token); 847 } 848 849 // Update any existing Activity Resources references. 850 updateResourcesForActivity(token, overrideConfig, displayId); 851 852 synchronized (mLock) { 853 Resources resources = findResourcesForActivityLocked(token, key, 854 classLoader); 855 if (resources != null) { 856 return resources; 857 } 858 } 859 860 // Now request an actual Resources object. 861 return createResourcesForActivity(token, key, 862 /* initialOverrideConfig */ Configuration.EMPTY, /* overrideDisplayId */ null, 863 classLoader, /* apkSupplier */ null); 864 } finally { 865 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 866 } 867 } 868 869 /** 870 * Rebases a key's override config on top of the Activity's base override. 871 * 872 * @param activityToken the token the supplied {@code key} is derived from. 873 * @param key the key to rebase 874 * @param overridesActivityDisplay whether this key is overriding the display from the token 875 */ rebaseKeyForActivity(IBinder activityToken, ResourcesKey key, boolean overridesActivityDisplay)876 private void rebaseKeyForActivity(IBinder activityToken, ResourcesKey key, 877 boolean overridesActivityDisplay) { 878 synchronized (mLock) { 879 final ActivityResources activityResources = 880 getOrCreateActivityResourcesStructLocked(activityToken); 881 882 if (key.mDisplayId == INVALID_DISPLAY) { 883 key.mDisplayId = activityResources.overrideDisplayId; 884 } 885 886 Configuration config; 887 if (key.hasOverrideConfiguration()) { 888 config = new Configuration(activityResources.overrideConfig); 889 config.updateFrom(key.mOverrideConfiguration); 890 } else { 891 config = activityResources.overrideConfig; 892 } 893 894 if (overridesActivityDisplay 895 && key.mOverrideConfiguration.windowConfiguration.getAppBounds() == null) { 896 if (!key.hasOverrideConfiguration()) { 897 // Make a copy to handle the case where the override config is set to defaults. 898 config = new Configuration(config); 899 } 900 901 // If this key is overriding the display from the token and the key's 902 // window config app bounds is null we need to explicitly override this to 903 // ensure the display adjustments are as expected. 904 config.windowConfiguration.setAppBounds(null); 905 } 906 907 key.mOverrideConfiguration.setTo(config); 908 } 909 } 910 911 /** 912 * Rebases a key's override config with display metrics of the {@code overrideDisplay} paired 913 * with the {code displayAdjustments}. 914 * 915 * @see #applyDisplayMetricsToConfiguration(DisplayMetrics, Configuration) 916 */ rebaseKeyForDisplay(ResourcesKey key, int overrideDisplay)917 private void rebaseKeyForDisplay(ResourcesKey key, int overrideDisplay) { 918 final Configuration temp = new Configuration(); 919 920 DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration); 921 daj.setCompatibilityInfo(key.mCompatInfo); 922 923 final DisplayMetrics dm = getDisplayMetrics(overrideDisplay, daj); 924 applyDisplayMetricsToConfiguration(dm, temp); 925 926 if (key.hasOverrideConfiguration()) { 927 temp.updateFrom(key.mOverrideConfiguration); 928 } 929 key.mOverrideConfiguration.setTo(temp); 930 } 931 932 /** 933 * Check WeakReferences and remove any dead references so they don't pile up. 934 */ cleanupReferences(ArrayList<WeakReference<T>> references, ReferenceQueue<T> referenceQueue)935 private static <T> void cleanupReferences(ArrayList<WeakReference<T>> references, 936 ReferenceQueue<T> referenceQueue) { 937 cleanupReferences(references, referenceQueue, Function.identity()); 938 } 939 940 /** 941 * Check WeakReferences and remove any dead references so they don't pile up. 942 */ cleanupReferences(ArrayList<C> referenceContainers, ReferenceQueue<T> referenceQueue, Function<C, WeakReference<T>> unwrappingFunction)943 private static <C, T> void cleanupReferences(ArrayList<C> referenceContainers, 944 ReferenceQueue<T> referenceQueue, Function<C, WeakReference<T>> unwrappingFunction) { 945 Reference<? extends T> enqueuedRef = referenceQueue.poll(); 946 if (enqueuedRef == null) { 947 return; 948 } 949 950 final HashSet<Reference<? extends T>> deadReferences = new HashSet<>(); 951 for (; enqueuedRef != null; enqueuedRef = referenceQueue.poll()) { 952 deadReferences.add(enqueuedRef); 953 } 954 955 ArrayUtils.unstableRemoveIf(referenceContainers, (refContainer) -> { 956 WeakReference<T> ref = unwrappingFunction.apply(refContainer); 957 return ref == null || deadReferences.contains(ref); 958 }); 959 } 960 961 /** 962 * Creates an {@link ApkAssetsSupplier} and loads all the ApkAssets required by the {@param key} 963 * into the supplier. This should be done while the lock is not held to prevent performing I/O 964 * while holding the lock. 965 */ createApkAssetsSupplierNotLocked(@onNull ResourcesKey key)966 private @NonNull ApkAssetsSupplier createApkAssetsSupplierNotLocked(@NonNull ResourcesKey key) { 967 Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, 968 "ResourcesManager#createApkAssetsSupplierNotLocked"); 969 try { 970 if (DEBUG && Thread.holdsLock(mLock)) { 971 Slog.w(TAG, "Calling thread " + Thread.currentThread().getName() 972 + " is holding mLock", new Throwable()); 973 } 974 975 final ApkAssetsSupplier supplier = new ApkAssetsSupplier(); 976 final ArrayList<ApkKey> apkKeys = extractApkKeys(key); 977 for (int i = 0, n = apkKeys.size(); i < n; i++) { 978 final ApkKey apkKey = apkKeys.get(i); 979 try { 980 supplier.load(apkKey); 981 } catch (IOException e) { 982 Log.w(TAG, String.format("failed to preload asset path '%s'", apkKey.path), e); 983 } 984 } 985 return supplier; 986 } finally { 987 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 988 } 989 } 990 991 /** 992 * Creates a Resources object set with a ResourcesImpl object matching the given key. 993 * 994 * @param key The key describing the parameters of the ResourcesImpl object. 995 * @param classLoader The classloader to use for the Resources object. 996 * If null, {@link ClassLoader#getSystemClassLoader()} is used. 997 * @return A Resources object that gets updated when 998 * {@link #applyConfigurationToResources(Configuration, CompatibilityInfo)} 999 * is called. 1000 */ 1001 @Nullable createResources(@onNull ResourcesKey key, @NonNull ClassLoader classLoader, @Nullable ApkAssetsSupplier apkSupplier)1002 private Resources createResources(@NonNull ResourcesKey key, @NonNull ClassLoader classLoader, 1003 @Nullable ApkAssetsSupplier apkSupplier) { 1004 synchronized (mLock) { 1005 if (DEBUG) { 1006 Throwable here = new Throwable(); 1007 here.fillInStackTrace(); 1008 Slog.w(TAG, "!! Create resources for key=" + key, here); 1009 } 1010 1011 ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key, apkSupplier); 1012 if (resourcesImpl == null) { 1013 return null; 1014 } 1015 1016 return createResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo); 1017 } 1018 } 1019 1020 @Nullable createResourcesForActivity(@onNull IBinder activityToken, @NonNull ResourcesKey key, @NonNull Configuration initialOverrideConfig, @Nullable Integer overrideDisplayId, @NonNull ClassLoader classLoader, @Nullable ApkAssetsSupplier apkSupplier)1021 private Resources createResourcesForActivity(@NonNull IBinder activityToken, 1022 @NonNull ResourcesKey key, @NonNull Configuration initialOverrideConfig, 1023 @Nullable Integer overrideDisplayId, @NonNull ClassLoader classLoader, 1024 @Nullable ApkAssetsSupplier apkSupplier) { 1025 synchronized (mLock) { 1026 if (DEBUG) { 1027 Throwable here = new Throwable(); 1028 here.fillInStackTrace(); 1029 Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here); 1030 } 1031 1032 ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key, apkSupplier); 1033 if (resourcesImpl == null) { 1034 return null; 1035 } 1036 1037 return createResourcesForActivityLocked(activityToken, initialOverrideConfig, 1038 overrideDisplayId, classLoader, resourcesImpl, key.mCompatInfo); 1039 } 1040 } 1041 1042 /** 1043 * Gets or creates a new Resources object associated with the IBinder token. References returned 1044 * by this method live as long as the Activity, meaning they can be cached and used by the 1045 * Activity even after a configuration change. If any other parameter is changed 1046 * (resDir, splitResDirs, overrideConfig) for a given Activity, the same Resources object 1047 * is updated and handed back to the caller. However, changing the class loader will result in a 1048 * new Resources object. 1049 * <p/> 1050 * If activityToken is null, a cached Resources object will be returned if it matches the 1051 * input parameters. Otherwise a new Resources object that satisfies these parameters is 1052 * returned. 1053 * 1054 * @param activityToken Represents an Activity. If null, global resources are assumed. 1055 * @param resDir The base resource path. Can be null (only framework resources will be loaded). 1056 * @param splitResDirs An array of split resource paths. Can be null. 1057 * @param legacyOverlayDirs An array of overlay APK paths. Can be null. 1058 * @param overlayPaths An array of overlay APK and non-APK paths. Can be null. 1059 * @param libDirs An array of resource library paths. Can be null. 1060 * @param overrideDisplayId The ID of the display for which the returned Resources should be 1061 * based. This will cause display-based configuration properties to override those of the base 1062 * Resources for the {@code activityToken}, or the global configuration if {@code activityToken} 1063 * is null. 1064 * @param overrideConfig The configuration to apply on top of the base configuration. Can be 1065 * null. Mostly used with Activities that are in multi-window which may override width and 1066 * height properties from the base config. 1067 * @param compatInfo The compatibility settings to use. Cannot be null. A default to use is 1068 * {@link CompatibilityInfo#DEFAULT_COMPATIBILITY_INFO}. 1069 * @param classLoader The class loader to use when inflating Resources. If null, the 1070 * {@link ClassLoader#getSystemClassLoader()} is used. 1071 * @return a Resources object from which to access resources. 1072 */ 1073 @Nullable getResources( @ullable IBinder activityToken, @Nullable String resDir, @Nullable String[] splitResDirs, @Nullable String[] legacyOverlayDirs, @Nullable String[] overlayPaths, @Nullable String[] libDirs, @Nullable Integer overrideDisplayId, @Nullable Configuration overrideConfig, @NonNull CompatibilityInfo compatInfo, @Nullable ClassLoader classLoader, @Nullable List<ResourcesLoader> loaders)1074 public Resources getResources( 1075 @Nullable IBinder activityToken, 1076 @Nullable String resDir, 1077 @Nullable String[] splitResDirs, 1078 @Nullable String[] legacyOverlayDirs, 1079 @Nullable String[] overlayPaths, 1080 @Nullable String[] libDirs, 1081 @Nullable Integer overrideDisplayId, 1082 @Nullable Configuration overrideConfig, 1083 @NonNull CompatibilityInfo compatInfo, 1084 @Nullable ClassLoader classLoader, 1085 @Nullable List<ResourcesLoader> loaders) { 1086 try { 1087 Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources"); 1088 final ResourcesKey key = new ResourcesKey( 1089 resDir, 1090 splitResDirs, 1091 combinedOverlayPaths(legacyOverlayDirs, overlayPaths), 1092 libDirs, 1093 overrideDisplayId != null ? overrideDisplayId : INVALID_DISPLAY, 1094 overrideConfig, 1095 compatInfo, 1096 loaders == null ? null : loaders.toArray(new ResourcesLoader[0])); 1097 classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader(); 1098 1099 // Preload the ApkAssets required by the key to prevent performing heavy I/O while the 1100 // ResourcesManager lock is held. 1101 final ApkAssetsSupplier assetsSupplier = createApkAssetsSupplierNotLocked(key); 1102 1103 if (overrideDisplayId != null) { 1104 rebaseKeyForDisplay(key, overrideDisplayId); 1105 } 1106 1107 Resources resources; 1108 if (activityToken != null) { 1109 Configuration initialOverrideConfig = new Configuration(key.mOverrideConfiguration); 1110 rebaseKeyForActivity(activityToken, key, overrideDisplayId != null); 1111 resources = createResourcesForActivity(activityToken, key, initialOverrideConfig, 1112 overrideDisplayId, classLoader, assetsSupplier); 1113 } else { 1114 resources = createResources(key, classLoader, assetsSupplier); 1115 } 1116 return resources; 1117 } finally { 1118 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 1119 } 1120 } 1121 1122 /** 1123 * Updates an Activity's Resources object with overrideConfig. The Resources object 1124 * that was previously returned by {@link #getResources(IBinder, String, String[], String[], 1125 * String[], String[], Integer, Configuration, CompatibilityInfo, ClassLoader, List)} is still 1126 * valid and will have the updated configuration. 1127 * 1128 * @param activityToken The Activity token. 1129 * @param overrideConfig The configuration override to update. 1130 * @param displayId Id of the display where activity currently resides. 1131 */ updateResourcesForActivity(@onNull IBinder activityToken, @Nullable Configuration overrideConfig, int displayId)1132 public void updateResourcesForActivity(@NonNull IBinder activityToken, 1133 @Nullable Configuration overrideConfig, int displayId) { 1134 try { 1135 Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, 1136 "ResourcesManager#updateResourcesForActivity"); 1137 if (displayId == INVALID_DISPLAY) { 1138 throw new IllegalArgumentException("displayId can not be INVALID_DISPLAY"); 1139 } 1140 synchronized (mLock) { 1141 final ActivityResources activityResources = 1142 getOrCreateActivityResourcesStructLocked(activityToken); 1143 1144 boolean movedToDifferentDisplay = activityResources.overrideDisplayId != displayId; 1145 if (Objects.equals(activityResources.overrideConfig, overrideConfig) 1146 && !movedToDifferentDisplay) { 1147 // They are the same and no change of display id, no work to do. 1148 return; 1149 } 1150 1151 // Grab a copy of the old configuration so we can create the delta's of each 1152 // Resources object associated with this Activity. 1153 final Configuration oldConfig = new Configuration(activityResources.overrideConfig); 1154 1155 // Update the Activity's base override. 1156 if (overrideConfig != null) { 1157 activityResources.overrideConfig.setTo(overrideConfig); 1158 } else { 1159 activityResources.overrideConfig.unset(); 1160 } 1161 1162 // Update the Activity's override display id. 1163 activityResources.overrideDisplayId = displayId; 1164 1165 // If a application info update was scheduled to occur in this process but has not 1166 // occurred yet, apply it now so the resources objects will have updated paths if 1167 // the assets sequence changed. 1168 applyAllPendingAppInfoUpdates(); 1169 1170 if (DEBUG) { 1171 Throwable here = new Throwable(); 1172 here.fillInStackTrace(); 1173 Slog.d(TAG, "updating resources override for activity=" + activityToken 1174 + " from oldConfig=" 1175 + Configuration.resourceQualifierString(oldConfig) 1176 + " to newConfig=" 1177 + Configuration.resourceQualifierString( 1178 activityResources.overrideConfig) + " displayId=" + displayId, 1179 here); 1180 } 1181 1182 1183 // Rebase each Resources associated with this Activity. 1184 final int refCount = activityResources.activityResources.size(); 1185 for (int i = 0; i < refCount; i++) { 1186 final ActivityResource activityResource = 1187 activityResources.activityResources.get(i); 1188 1189 final Resources resources = activityResource.resources.get(); 1190 if (resources == null) { 1191 continue; 1192 } 1193 1194 final ResourcesKey newKey = rebaseActivityOverrideConfig(activityResource, 1195 overrideConfig, displayId); 1196 if (newKey == null) { 1197 continue; 1198 } 1199 1200 // TODO(b/173090263): Improve the performance of AssetManager & ResourcesImpl 1201 // constructions. 1202 final ResourcesImpl resourcesImpl = 1203 findOrCreateResourcesImplForKeyLocked(newKey); 1204 if (resourcesImpl != null && resourcesImpl != resources.getImpl()) { 1205 // Set the ResourcesImpl, updating it for all users of this Resources 1206 // object. 1207 resources.setImpl(resourcesImpl); 1208 } 1209 } 1210 } 1211 } finally { 1212 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 1213 } 1214 } 1215 1216 /** 1217 * Rebases an updated override config over any old override config and returns the new one 1218 * that an Activity's Resources should be set to. 1219 */ 1220 @Nullable rebaseActivityOverrideConfig(@onNull ActivityResource activityResource, @Nullable Configuration newOverrideConfig, int displayId)1221 private ResourcesKey rebaseActivityOverrideConfig(@NonNull ActivityResource activityResource, 1222 @Nullable Configuration newOverrideConfig, int displayId) { 1223 final Resources resources = activityResource.resources.get(); 1224 if (resources == null) { 1225 return null; 1226 } 1227 1228 // Extract the ResourcesKey that was last used to create the Resources for this 1229 // activity. 1230 final ResourcesKey oldKey = findKeyForResourceImplLocked(resources.getImpl()); 1231 if (oldKey == null) { 1232 Slog.e(TAG, "can't find ResourcesKey for resources impl=" 1233 + resources.getImpl()); 1234 return null; 1235 } 1236 1237 // Build the new override configuration for this ResourcesKey. 1238 final Configuration rebasedOverrideConfig = new Configuration(); 1239 if (newOverrideConfig != null) { 1240 rebasedOverrideConfig.setTo(newOverrideConfig); 1241 } 1242 1243 final Integer overrideDisplayId = activityResource.overrideDisplayId; 1244 if (overrideDisplayId != null) { 1245 DisplayAdjustments displayAdjustments = new DisplayAdjustments(rebasedOverrideConfig); 1246 displayAdjustments.getConfiguration().setTo(activityResource.overrideConfig); 1247 displayAdjustments.setCompatibilityInfo(oldKey.mCompatInfo); 1248 1249 DisplayMetrics dm = getDisplayMetrics(overrideDisplayId, displayAdjustments); 1250 applyDisplayMetricsToConfiguration(dm, rebasedOverrideConfig); 1251 } 1252 1253 final boolean hasOverrideConfig = 1254 !activityResource.overrideConfig.equals(Configuration.EMPTY); 1255 if (hasOverrideConfig) { 1256 rebasedOverrideConfig.updateFrom(activityResource.overrideConfig); 1257 } 1258 1259 if (activityResource.overrideDisplayId != null 1260 && activityResource.overrideConfig.windowConfiguration.getAppBounds() == null) { 1261 // If this activity resource is overriding the display from the token and the key's 1262 // window config app bounds is null we need to explicitly override this to 1263 // ensure the display adjustments are as expected. 1264 rebasedOverrideConfig.windowConfiguration.setAppBounds(null); 1265 } 1266 1267 // Ensure the new key keeps the expected override display instead of the new token display. 1268 displayId = overrideDisplayId != null ? overrideDisplayId : displayId; 1269 1270 // Create the new ResourcesKey with the rebased override config. 1271 final ResourcesKey newKey = new ResourcesKey(oldKey.mResDir, 1272 oldKey.mSplitResDirs, oldKey.mOverlayPaths, oldKey.mLibDirs, 1273 displayId, rebasedOverrideConfig, oldKey.mCompatInfo, oldKey.mLoaders); 1274 1275 if (DEBUG) { 1276 Slog.d(TAG, "rebasing ref=" + resources + " from oldKey=" + oldKey 1277 + " to newKey=" + newKey + ", displayId=" + displayId); 1278 } 1279 1280 return newKey; 1281 } 1282 appendPendingAppInfoUpdate(@onNull String[] oldSourceDirs, @NonNull ApplicationInfo appInfo)1283 public void appendPendingAppInfoUpdate(@NonNull String[] oldSourceDirs, 1284 @NonNull ApplicationInfo appInfo) { 1285 synchronized (mLock) { 1286 if (mPendingAppInfoUpdates == null) { 1287 mPendingAppInfoUpdates = new ArrayList<>(); 1288 } 1289 // Clear previous app info changes for a package to prevent multiple ResourcesImpl 1290 // recreations when the recreation caused by this update completely overrides the 1291 // previous pending changes. 1292 for (int i = mPendingAppInfoUpdates.size() - 1; i >= 0; i--) { 1293 if (ArrayUtils.containsAll(oldSourceDirs, mPendingAppInfoUpdates.get(i).first)) { 1294 mPendingAppInfoUpdates.remove(i); 1295 } 1296 } 1297 mPendingAppInfoUpdates.add(new Pair<>(oldSourceDirs, appInfo)); 1298 } 1299 } 1300 applyAllPendingAppInfoUpdates()1301 public final void applyAllPendingAppInfoUpdates() { 1302 synchronized (mLock) { 1303 if (mPendingAppInfoUpdates != null) { 1304 for (int i = 0, n = mPendingAppInfoUpdates.size(); i < n; i++) { 1305 final Pair<String[], ApplicationInfo> appInfo = mPendingAppInfoUpdates.get(i); 1306 applyNewResourceDirsLocked(appInfo.first, appInfo.second); 1307 } 1308 mPendingAppInfoUpdates = null; 1309 } 1310 } 1311 } 1312 applyConfigurationToResources(@onNull Configuration config, @Nullable CompatibilityInfo compat)1313 public final boolean applyConfigurationToResources(@NonNull Configuration config, 1314 @Nullable CompatibilityInfo compat) { 1315 return applyConfigurationToResources(config, compat, null /* adjustments */); 1316 } 1317 1318 /** Applies the global configuration to the managed resources. */ applyConfigurationToResources(@onNull Configuration config, @Nullable CompatibilityInfo compat, @Nullable DisplayAdjustments adjustments)1319 public final boolean applyConfigurationToResources(@NonNull Configuration config, 1320 @Nullable CompatibilityInfo compat, @Nullable DisplayAdjustments adjustments) { 1321 synchronized (mLock) { 1322 try { 1323 Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, 1324 "ResourcesManager#applyConfigurationToResources"); 1325 1326 if (!mResConfiguration.isOtherSeqNewer(config) && compat == null) { 1327 if (DEBUG || DEBUG_CONFIGURATION) { 1328 Slog.v(TAG, "Skipping new config: curSeq=" 1329 + mResConfiguration.seq + ", newSeq=" + config.seq); 1330 } 1331 return false; 1332 } 1333 1334 int changes = mResConfiguration.updateFrom(config); 1335 if (compat != null && (mResCompatibilityInfo == null 1336 || !mResCompatibilityInfo.equals(compat))) { 1337 mResCompatibilityInfo = compat; 1338 changes |= ActivityInfo.CONFIG_SCREEN_LAYOUT 1339 | ActivityInfo.CONFIG_SCREEN_SIZE 1340 | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; 1341 } 1342 1343 // If a application info update was scheduled to occur in this process but has not 1344 // occurred yet, apply it now so the resources objects will have updated paths when 1345 // the assets sequence changes. 1346 if ((changes & ActivityInfo.CONFIG_ASSETS_PATHS) != 0) { 1347 applyAllPendingAppInfoUpdates(); 1348 } 1349 1350 DisplayMetrics displayMetrics = getDisplayMetrics(); 1351 if (adjustments != null) { 1352 // Currently the only case where the adjustment takes effect is to simulate 1353 // placing an app in a rotated display. 1354 adjustments.adjustGlobalAppMetrics(displayMetrics); 1355 } 1356 Resources.updateSystemConfiguration(config, displayMetrics, compat); 1357 1358 ApplicationPackageManager.configurationChanged(); 1359 1360 Configuration tmpConfig = new Configuration(); 1361 1362 for (int i = mResourceImpls.size() - 1; i >= 0; i--) { 1363 ResourcesKey key = mResourceImpls.keyAt(i); 1364 WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i); 1365 ResourcesImpl r = weakImplRef != null ? weakImplRef.get() : null; 1366 if (r != null) { 1367 applyConfigurationToResourcesLocked(config, compat, tmpConfig, key, r); 1368 } else { 1369 mResourceImpls.removeAt(i); 1370 } 1371 } 1372 1373 return changes != 0; 1374 } finally { 1375 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 1376 } 1377 } 1378 } 1379 applyConfigurationToResourcesLocked(@onNull Configuration config, @Nullable CompatibilityInfo compat, Configuration tmpConfig, ResourcesKey key, ResourcesImpl resourcesImpl)1380 private void applyConfigurationToResourcesLocked(@NonNull Configuration config, 1381 @Nullable CompatibilityInfo compat, Configuration tmpConfig, 1382 ResourcesKey key, ResourcesImpl resourcesImpl) { 1383 if (DEBUG || DEBUG_CONFIGURATION) { 1384 Slog.v(TAG, "Changing resources " 1385 + resourcesImpl + " config to: " + config); 1386 } 1387 1388 tmpConfig.setTo(config); 1389 if (key.hasOverrideConfiguration()) { 1390 tmpConfig.updateFrom(key.mOverrideConfiguration); 1391 } 1392 1393 // Get new DisplayMetrics based on the DisplayAdjustments given to the ResourcesImpl. Update 1394 // a copy if the CompatibilityInfo changed, because the ResourcesImpl object will handle the 1395 // update internally. 1396 DisplayAdjustments daj = resourcesImpl.getDisplayAdjustments(); 1397 if (compat != null) { 1398 daj = new DisplayAdjustments(daj); 1399 daj.setCompatibilityInfo(compat); 1400 } 1401 daj.setConfiguration(tmpConfig); 1402 DisplayMetrics dm = getDisplayMetrics(generateDisplayId(key), daj); 1403 1404 resourcesImpl.updateConfiguration(tmpConfig, dm, compat); 1405 } 1406 1407 /** 1408 * Appends the library asset path to any ResourcesImpl object that contains the main 1409 * assetPath. 1410 * @param assetPath The main asset path for which to add the library asset path. 1411 * @param libAsset The library asset path to add. 1412 */ 1413 @UnsupportedAppUsage appendLibAssetForMainAssetPath(String assetPath, String libAsset)1414 public void appendLibAssetForMainAssetPath(String assetPath, String libAsset) { 1415 appendLibAssetsForMainAssetPath(assetPath, new String[] { libAsset }); 1416 } 1417 1418 /** 1419 * Appends the library asset paths to any ResourcesImpl object that contains the main 1420 * assetPath. 1421 * @param assetPath The main asset path for which to add the library asset path. 1422 * @param libAssets The library asset paths to add. 1423 */ appendLibAssetsForMainAssetPath(String assetPath, String[] libAssets)1424 public void appendLibAssetsForMainAssetPath(String assetPath, String[] libAssets) { 1425 synchronized (mLock) { 1426 // Record which ResourcesImpl need updating 1427 // (and what ResourcesKey they should update to). 1428 final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys = new ArrayMap<>(); 1429 1430 final int implCount = mResourceImpls.size(); 1431 for (int i = 0; i < implCount; i++) { 1432 final ResourcesKey key = mResourceImpls.keyAt(i); 1433 final WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i); 1434 final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null; 1435 if (impl != null && Objects.equals(key.mResDir, assetPath)) { 1436 String[] newLibAssets = key.mLibDirs; 1437 for (String libAsset : libAssets) { 1438 newLibAssets = 1439 ArrayUtils.appendElement(String.class, newLibAssets, libAsset); 1440 } 1441 1442 if (!Arrays.equals(newLibAssets, key.mLibDirs)) { 1443 updatedResourceKeys.put(impl, new ResourcesKey( 1444 key.mResDir, 1445 key.mSplitResDirs, 1446 key.mOverlayPaths, 1447 newLibAssets, 1448 key.mDisplayId, 1449 key.mOverrideConfiguration, 1450 key.mCompatInfo, 1451 key.mLoaders)); 1452 } 1453 } 1454 } 1455 1456 redirectResourcesToNewImplLocked(updatedResourceKeys); 1457 } 1458 } 1459 applyNewResourceDirsLocked(@ullable final String[] oldSourceDirs, @NonNull final ApplicationInfo appInfo)1460 private void applyNewResourceDirsLocked(@Nullable final String[] oldSourceDirs, 1461 @NonNull final ApplicationInfo appInfo) { 1462 try { 1463 Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, 1464 "ResourcesManager#applyNewResourceDirsLocked"); 1465 1466 String baseCodePath = appInfo.getBaseCodePath(); 1467 1468 final int myUid = Process.myUid(); 1469 String[] newSplitDirs = appInfo.uid == myUid 1470 ? appInfo.splitSourceDirs 1471 : appInfo.splitPublicSourceDirs; 1472 1473 // ApplicationInfo is mutable, so clone the arrays to prevent outside modification 1474 String[] copiedSplitDirs = ArrayUtils.cloneOrNull(newSplitDirs); 1475 String[] copiedResourceDirs = combinedOverlayPaths(appInfo.resourceDirs, 1476 appInfo.overlayPaths); 1477 1478 if (appInfo.uid == myUid) { 1479 addApplicationPathsLocked(baseCodePath, copiedSplitDirs); 1480 } 1481 1482 final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys = new ArrayMap<>(); 1483 final int implCount = mResourceImpls.size(); 1484 for (int i = 0; i < implCount; i++) { 1485 final ResourcesKey key = mResourceImpls.keyAt(i); 1486 final WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i); 1487 final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null; 1488 1489 if (impl == null) { 1490 continue; 1491 } 1492 1493 if (key.mResDir == null 1494 || key.mResDir.equals(baseCodePath) 1495 || ArrayUtils.contains(oldSourceDirs, key.mResDir)) { 1496 updatedResourceKeys.put(impl, new ResourcesKey( 1497 baseCodePath, 1498 copiedSplitDirs, 1499 copiedResourceDirs, 1500 key.mLibDirs, 1501 key.mDisplayId, 1502 key.mOverrideConfiguration, 1503 key.mCompatInfo, 1504 key.mLoaders 1505 )); 1506 } 1507 } 1508 1509 redirectResourcesToNewImplLocked(updatedResourceKeys); 1510 } finally { 1511 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 1512 } 1513 } 1514 1515 /** 1516 * Creates an array with the contents of {@param overlayPaths} and the unique elements of 1517 * {@param resourceDirs}. 1518 * 1519 * {@link ApplicationInfo#resourceDirs} only contains paths of overlays APKs. 1520 * {@link ApplicationInfo#overlayPaths} was created to contain paths of overlay of varying file 1521 * formats. It also contains the contents of {@code resourceDirs} because the order of loaded 1522 * overlays matter. In case {@code resourceDirs} contains overlay APK paths that are not present 1523 * in overlayPaths (perhaps an app inserted an additional overlay path into a 1524 * {@code resourceDirs}), this method is used to combine the contents of {@code resourceDirs} 1525 * that do not exist in {@code overlayPaths}} and {@code overlayPaths}}. 1526 */ 1527 @Nullable combinedOverlayPaths(@ullable String[] resourceDirs, @Nullable String[] overlayPaths)1528 private static String[] combinedOverlayPaths(@Nullable String[] resourceDirs, 1529 @Nullable String[] overlayPaths) { 1530 if (resourceDirs == null) { 1531 return ArrayUtils.cloneOrNull(overlayPaths); 1532 } else if(overlayPaths == null) { 1533 return ArrayUtils.cloneOrNull(resourceDirs); 1534 } else { 1535 final ArrayList<String> paths = new ArrayList<>(); 1536 for (final String path : overlayPaths) { 1537 paths.add(path); 1538 } 1539 for (final String path : resourceDirs) { 1540 if (!paths.contains(path)) { 1541 paths.add(path); 1542 } 1543 } 1544 return paths.toArray(new String[0]); 1545 } 1546 } 1547 redirectResourcesToNewImplLocked( @onNull final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys)1548 private void redirectResourcesToNewImplLocked( 1549 @NonNull final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys) { 1550 // Bail early if there is no work to do. 1551 if (updatedResourceKeys.isEmpty()) { 1552 return; 1553 } 1554 1555 // Update any references to ResourcesImpl that require reloading. 1556 final int resourcesCount = mResourceReferences.size(); 1557 for (int i = 0; i < resourcesCount; i++) { 1558 final WeakReference<Resources> ref = mResourceReferences.get(i); 1559 final Resources r = ref != null ? ref.get() : null; 1560 if (r != null) { 1561 final ResourcesKey key = updatedResourceKeys.get(r.getImpl()); 1562 if (key != null) { 1563 final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key); 1564 if (impl == null) { 1565 throw new Resources.NotFoundException("failed to redirect ResourcesImpl"); 1566 } 1567 r.setImpl(impl); 1568 } 1569 } 1570 } 1571 1572 // Update any references to ResourcesImpl that require reloading for each Activity. 1573 for (ActivityResources activityResources : mActivityResourceReferences.values()) { 1574 final int resCount = activityResources.activityResources.size(); 1575 for (int i = 0; i < resCount; i++) { 1576 final ActivityResource activityResource = 1577 activityResources.activityResources.get(i); 1578 final Resources r = activityResource != null 1579 ? activityResource.resources.get() : null; 1580 if (r != null) { 1581 final ResourcesKey key = updatedResourceKeys.get(r.getImpl()); 1582 if (key != null) { 1583 final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key); 1584 if (impl == null) { 1585 throw new Resources.NotFoundException( 1586 "failed to redirect ResourcesImpl"); 1587 } 1588 r.setImpl(impl); 1589 } 1590 } 1591 } 1592 } 1593 } 1594 1595 /** 1596 * Overrides the display adjustments of all resources which are associated with the given token. 1597 * 1598 * @param token The token that owns the resources. 1599 * @param override The operation to override the existing display adjustments. If it is null, 1600 * the override adjustments will be cleared. 1601 * @return {@code true} if the override takes effect. 1602 */ overrideTokenDisplayAdjustments(IBinder token, @Nullable Consumer<DisplayAdjustments> override)1603 public boolean overrideTokenDisplayAdjustments(IBinder token, 1604 @Nullable Consumer<DisplayAdjustments> override) { 1605 boolean handled = false; 1606 synchronized (mLock) { 1607 final ActivityResources tokenResources = mActivityResourceReferences.get(token); 1608 if (tokenResources == null) { 1609 return false; 1610 } 1611 final ArrayList<ActivityResource> resourcesRefs = tokenResources.activityResources; 1612 for (int i = resourcesRefs.size() - 1; i >= 0; i--) { 1613 final ActivityResource activityResource = resourcesRefs.get(i); 1614 if (activityResource.overrideDisplayId != null) { 1615 // This resource overrides the display of the token so we should not be 1616 // modifying its display adjustments here. 1617 continue; 1618 } 1619 1620 final Resources res = activityResource.resources.get(); 1621 if (res != null) { 1622 res.overrideDisplayAdjustments(override); 1623 handled = true; 1624 } 1625 } 1626 } 1627 return handled; 1628 } 1629 1630 private class UpdateHandler implements Resources.UpdateCallbacks { 1631 1632 /** 1633 * Updates the list of {@link ResourcesLoader ResourcesLoader(s)} that the {@code resources} 1634 * instance uses. 1635 */ 1636 @Override onLoadersChanged(@onNull Resources resources, @NonNull List<ResourcesLoader> newLoader)1637 public void onLoadersChanged(@NonNull Resources resources, 1638 @NonNull List<ResourcesLoader> newLoader) { 1639 synchronized (mLock) { 1640 final ResourcesKey oldKey = findKeyForResourceImplLocked(resources.getImpl()); 1641 if (oldKey == null) { 1642 throw new IllegalArgumentException("Cannot modify resource loaders of" 1643 + " ResourcesImpl not registered with ResourcesManager"); 1644 } 1645 1646 final ResourcesKey newKey = new ResourcesKey( 1647 oldKey.mResDir, 1648 oldKey.mSplitResDirs, 1649 oldKey.mOverlayPaths, 1650 oldKey.mLibDirs, 1651 oldKey.mDisplayId, 1652 oldKey.mOverrideConfiguration, 1653 oldKey.mCompatInfo, 1654 newLoader.toArray(new ResourcesLoader[0])); 1655 1656 final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(newKey); 1657 resources.setImpl(impl); 1658 } 1659 } 1660 1661 /** 1662 * Refreshes the {@link AssetManager} of all {@link ResourcesImpl} that contain the 1663 * {@code loader} to apply any changes of the set of {@link ApkAssets}. 1664 **/ 1665 @Override onLoaderUpdated(@onNull ResourcesLoader loader)1666 public void onLoaderUpdated(@NonNull ResourcesLoader loader) { 1667 synchronized (mLock) { 1668 final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceImplKeys = 1669 new ArrayMap<>(); 1670 1671 for (int i = mResourceImpls.size() - 1; i >= 0; i--) { 1672 final ResourcesKey key = mResourceImpls.keyAt(i); 1673 final WeakReference<ResourcesImpl> impl = mResourceImpls.valueAt(i); 1674 if (impl == null || impl.get() == null 1675 || !ArrayUtils.contains(key.mLoaders, loader)) { 1676 continue; 1677 } 1678 1679 mResourceImpls.remove(key); 1680 updatedResourceImplKeys.put(impl.get(), key); 1681 } 1682 1683 redirectResourcesToNewImplLocked(updatedResourceImplKeys); 1684 } 1685 } 1686 } 1687 } 1688