From 082614c6a57a115ee0c5975e3579bf34a178c0f8 Mon Sep 17 00:00:00 2001 From: Adam Lesinski Date: Fri, 4 Mar 2016 14:33:47 -0800 Subject: [PATCH] Cache per-Activity Resources objects Each Activity has a Resources object whose underlying state changes when a configuration change occurs, both global and for that activity only (multi-window). Views and other clients of the Resources object can safely cache it knowing that they always have a reference to the most up-to-date resources. This applies to Resources.Theme objects as well. Bug:26854894 Change-Id: Ifd3da5a1086e17f747dfc265a447fe92a3a5c692 --- core/java/android/app/ActivityThread.java | 153 +++--- .../app/ApplicationPackageManager.java | 2 +- core/java/android/app/ContextImpl.java | 19 +- core/java/android/app/LoadedApk.java | 6 +- core/java/android/app/ResourcesManager.java | 474 +++++++++++++----- core/java/android/content/res/Resources.java | 53 ++ .../android/content/res/ResourcesImpl.java | 63 ++- .../android/content/res/ResourcesKey.java | 92 +++- .../com/android/internal/util/ArrayUtils.java | 45 ++ .../ConfigurationBoundResourceCacheTest.java | 81 +-- .../content/res/ResourcesManagerTest.java | 222 ++++++++ .../android/internal/util/ArrayUtilsTest.java | 119 +++++ 12 files changed, 1064 insertions(+), 265 deletions(-) create mode 100644 core/tests/coretests/src/android/content/res/ResourcesManagerTest.java create mode 100644 core/tests/coretests/src/com/android/internal/util/ArrayUtilsTest.java diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index d8bc1195c52b0..060ac5e578e4e 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1793,24 +1793,9 @@ public final class ActivityThread { * Resources if one has already been created. */ Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, - String[] libDirs, int displayId, Configuration overrideConfiguration, - LoadedApk pkgInfo) { - return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs, - displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), - pkgInfo.getClassLoader()); - } - - /** - * Creates a new top level resources for the given package. Will always create a new - * Resources, regardless if one has already been created. - */ - Resources getNewTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, - String[] libDirs, int displayId, Configuration overrideConfiguration, - LoadedApk pkgInfo) { - mResourcesManager.removeTopLevelResources( - resDir, displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo()); - return getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs, - displayId, overrideConfiguration, pkgInfo); + String[] libDirs, int displayId, LoadedApk pkgInfo) { + return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs, + displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader()); } final Handler getHandler() { @@ -2624,7 +2609,7 @@ public final class ActivityThread { } ContextImpl appContext = ContextImpl.createActivityContext( - this, r.packageInfo, displayId, r.overrideConfig); + this, r.packageInfo, r.token, displayId, r.overrideConfig); appContext.setOuterContext(activity); Context baseContext = appContext; @@ -3481,14 +3466,9 @@ public final class ActivityThread { if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { - r.tmpConfig.setTo(r.newConfig); - if (r.overrideConfig != null) { - r.tmpConfig.updateFrom(r.overrideConfig); - } + performConfigurationChangedForActivity(r, r.newConfig, REPORT_TO_ACTIVITY); if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity " - + r.activityInfo.name + " with newConfig " + r.tmpConfig); - performConfigurationChanged(r.activity, r.tmpConfig, REPORT_TO_ACTIVITY); - freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig)); + + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig); r.newConfig = null; } if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" @@ -3833,14 +3813,10 @@ public final class ActivityThread { } } if (r.newConfig != null) { - r.tmpConfig.setTo(r.newConfig); - if (r.overrideConfig != null) { - r.tmpConfig.updateFrom(r.overrideConfig); - } + performConfigurationChangedForActivity(r, r.newConfig, REPORT_TO_ACTIVITY); if (DEBUG_CONFIGURATION) Slog.v(TAG, "Updating activity vis " - + r.activityInfo.name + " with new config " + r.tmpConfig); - performConfigurationChanged(r.activity, r.tmpConfig, REPORT_TO_ACTIVITY); - freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig)); + + r.activityInfo.name + " with new config " + + r.activity.mCurrentConfig); r.newConfig = null; } } else { @@ -4545,8 +4521,44 @@ public final class ActivityThread { return callbacks; } - private static void performConfigurationChanged(ComponentCallbacks2 cb, Configuration config, - boolean reportToActivity) { + /** + * Updates the configuration for an Activity. The ActivityClientRecord's + * {@link ActivityClientRecord#overrideConfig} is used to compute the final Configuration for + * that Activity. {@link ActivityClientRecord#tmpConfig} is used as a temporary for delivering + * the updated Configuration. + * @param r ActivityClientRecord representing the Activity. + * @param newBaseConfig The new configuration to use. This may be augmented with + * {@link ActivityClientRecord#overrideConfig}. + * @param reportToActivity true if the change should be reported to the Activity's callback. + */ + private void performConfigurationChangedForActivity(ActivityClientRecord r, + Configuration newBaseConfig, + boolean reportToActivity) { + r.tmpConfig.setTo(newBaseConfig); + if (r.overrideConfig != null) { + r.tmpConfig.updateFrom(r.overrideConfig); + } + performConfigurationChanged(r.activity, r.token, r.tmpConfig, r.overrideConfig, + reportToActivity); + freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig)); + } + + /** + * Decides whether to update an Activity's configuration and whether to tell the + * Activity/Component about it. + * @param cb The component callback to notify of configuration change. + * @param activityToken The Activity binder token for which this configuration change happened. + * If the change is global, this is null. + * @param newConfig The new configuration. + * @param overrideConfig The override config that differentiates the Activity's configuration + * from the base global configuration. + * @param reportToActivity Notify the Activity of the change. + */ + private void performConfigurationChanged(ComponentCallbacks2 cb, + IBinder activityToken, + Configuration newConfig, + Configuration overrideConfig, + boolean reportToActivity) { // Only for Activity objects, check that they actually call up to their // superclass implementation. ComponentCallbacks2 is an interface, so // we check the runtime type and act accordingly. @@ -4563,7 +4575,7 @@ public final class ActivityThread { // If the new config is the same as the config this Activity // is already running with then don't bother calling // onConfigurationChanged - int diff = activity.mCurrentConfig.diff(config); + int diff = activity.mCurrentConfig.diff(newConfig); if (diff != 0) { // If this activity doesn't handle any of the config changes then don't bother // calling onConfigurationChanged as we're going to destroy it. @@ -4578,21 +4590,31 @@ public final class ActivityThread { } } - if (DEBUG_CONFIGURATION) Slog.v(TAG, "Config callback " + cb - + ": shouldChangeConfig=" + shouldChangeConfig); + if (DEBUG_CONFIGURATION) { + Slog.v(TAG, "Config callback " + cb + ": shouldChangeConfig=" + shouldChangeConfig); + } + if (shouldChangeConfig) { + if (activityToken != null) { + // We only update an Activity's configuration if this is not a global + // configuration change. This must also be done before the callback, + // or else we violate the contract that the new resources are available + // in {@link ComponentCallbacks2#onConfigurationChanged(Configuration)}. + mResourcesManager.updateResourcesForActivity(activityToken, overrideConfig); + } + if (reportToActivity) { - cb.onConfigurationChanged(config); + cb.onConfigurationChanged(newConfig); } if (activity != null) { if (reportToActivity && !activity.mCalled) { throw new SuperNotCalledException( "Activity " + activity.getLocalClassName() + - " did not call through to super.onConfigurationChanged()"); + " did not call through to super.onConfigurationChanged()"); } activity.mConfigChangeFlags = 0; - activity.mCurrentConfig = new Configuration(config); + activity.mCurrentConfig = new Configuration(newConfig); } } } @@ -4609,7 +4631,8 @@ public final class ActivityThread { mCompatConfiguration = new Configuration(); } mCompatConfiguration.setTo(mConfiguration); - if (mResourcesManager.applyCompatConfiguration(displayDensity, mCompatConfiguration)) { + if (mResourcesManager.applyCompatConfigurationLocked(displayDensity, + mCompatConfiguration)) { config = mCompatConfiguration; } return config; @@ -4661,7 +4684,8 @@ public final class ActivityThread { if (callbacks != null) { final int N = callbacks.size(); for (int i=0; i> mActiveResources = - new ArrayMap<>(); - private final ArrayMap, WeakReference> mDisplays = - new ArrayMap<>(); + + /** + * Predicate that returns true if a WeakReference is gc'ed. + */ + private static final Predicate> sEmptyReferencePredicate = + new Predicate>() { + @Override + public boolean test(WeakReference weakRef) { + return weakRef == null || weakRef.get() == null; + } + }; private String[] mSystemLocales = {}; - private final HashSet mNonSystemLocales = new HashSet(); + private final HashSet mNonSystemLocales = new HashSet<>(); private boolean mHasNonSystemLocales = false; - CompatibilityInfo mResCompatibilityInfo; + /** + * The global compatibility settings. + */ + private CompatibilityInfo mResCompatibilityInfo; - Configuration mResConfiguration; + /** + * The global configuration upon which all Resources are based. Multi-window Resources + * apply their overrides to this configuration. + */ + private final Configuration mResConfiguration = new Configuration(); + + /** + * A mapping of ResourceImpls and their configurations. These are heavy weight objects + * which should be reused as much as possible. + */ + private final ArrayMap> mResourceImpls = + new ArrayMap<>(); + + /** + * A list of Resource references that can be reused. + */ + private final ArrayList> mResourceReferences = new ArrayList<>(); + + /** + * Each Activity may have only one Resources object. + */ + private final WeakHashMap> mActivityResourceReferences = + new WeakHashMap<>(); + + /** + * A cache of DisplayId to DisplayAdjustments. + */ + private final ArrayMap, WeakReference> mDisplays = + new ArrayMap<>(); public static ResourcesManager getInstance() { synchronized (ResourcesManager.class) { @@ -76,7 +122,11 @@ public class ResourcesManager { return getDisplayMetricsLocked(Display.DEFAULT_DISPLAY); } - DisplayMetrics getDisplayMetricsLocked(int displayId) { + /** + * Protected so that tests can override and returns something a fixed value. + */ + @VisibleForTesting + protected DisplayMetrics getDisplayMetricsLocked(int displayId) { DisplayMetrics dm = new DisplayMetrics(); final Display display = getAdjustedDisplay(displayId, DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS); @@ -88,12 +138,12 @@ public class ResourcesManager { return dm; } - final void applyNonDefaultDisplayMetricsToConfigurationLocked( - DisplayMetrics dm, Configuration config) { + private static void applyNonDefaultDisplayMetricsToConfiguration( + @NonNull DisplayMetrics dm, @NonNull Configuration config) { config.touchscreen = Configuration.TOUCHSCREEN_NOTOUCH; config.densityDpi = dm.densityDpi; - config.screenWidthDp = (int)(dm.widthPixels / dm.density); - config.screenHeightDp = (int)(dm.heightPixels / dm.density); + config.screenWidthDp = (int) (dm.widthPixels / dm.density); + config.screenHeightDp = (int) (dm.heightPixels / dm.density); int sl = Configuration.resetScreenLayout(config.screenLayout); if (dm.widthPixels > dm.heightPixels) { config.orientation = Configuration.ORIENTATION_LANDSCAPE; @@ -110,8 +160,8 @@ public class ResourcesManager { config.compatSmallestScreenWidthDp = config.smallestScreenWidthDp; } - public boolean applyCompatConfiguration(int displayDensity, - Configuration compatConfiguration) { + public boolean applyCompatConfigurationLocked(int displayDensity, + @NonNull Configuration compatConfiguration) { if (mResCompatibilityInfo != null && !mResCompatibilityInfo.supportsScreen()) { mResCompatibilityInfo.applyToConfiguration(displayDensity, compatConfiguration); return true; @@ -126,7 +176,8 @@ public class ResourcesManager { * @param displayId display Id. * @param displayAdjustments display adjustments. */ - public Display getAdjustedDisplay(final int displayId, DisplayAdjustments displayAdjustments) { + public Display getAdjustedDisplay(final int displayId, + @Nullable DisplayAdjustments displayAdjustments) { final DisplayAdjustments displayAdjustmentsCopy = (displayAdjustments != null) ? new DisplayAdjustments(displayAdjustments) : new DisplayAdjustments(); final Pair key = @@ -153,76 +204,42 @@ public class ResourcesManager { } /** - * Creates the top level Resources for applications with the given compatibility info. + * Creates an AssetManager from the paths within the ResourcesKey. * - * @param resDir the resource directory. - * @param splitResDirs split resource directories. - * @param overlayDirs the resource overlay directories. - * @param libDirs the shared library resource dirs this app references. - * @param displayId display Id. - * @param overrideConfiguration override configurations. - * @param compatInfo the compatibility info. Must not be null. - * @param classLoader the class loader for the resource package - */ - Resources getTopLevelResources(String resDir, String[] splitResDirs, - String[] overlayDirs, String[] libDirs, int displayId, - Configuration overrideConfiguration, CompatibilityInfo compatInfo, - ClassLoader classLoader) { - final float scale = compatInfo.applicationScale; - Configuration overrideConfigCopy = (overrideConfiguration != null) - ? new Configuration(overrideConfiguration) : null; - ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale); - Resources r; - final boolean findSystemLocales; - final boolean hasNonSystemLocales; - synchronized (this) { - // Resources is app scale dependent. - if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale); - - WeakReference wr = mActiveResources.get(key); - r = wr != null ? wr.get() : null; - //if (r != null) Log.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate()); - if (r != null && r.getAssets().isUpToDate()) { - if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir - + ": appScale=" + r.getCompatibilityInfo().applicationScale - + " key=" + key + " overrideConfig=" + overrideConfiguration); - return r; - } - findSystemLocales = (mSystemLocales.length == 0); - hasNonSystemLocales = mHasNonSystemLocales; - } - - //if (r != null) { - // Log.w(TAG, "Throwing away out-of-date resources!!!! " - // + r + " " + resDir); - //} - + * This can be overridden in tests so as to avoid creating a real AssetManager with + * real APK paths. + * @param key The key containing the resource paths to add to the AssetManager. + * @return a new AssetManager. + */ + @VisibleForTesting + protected AssetManager createAssetManager(@NonNull final ResourcesKey key) { AssetManager assets = new AssetManager(); + // resDir can be null if the 'android' package is creating a new Resources object. // This is fine, since each AssetManager automatically loads the 'android' package // already. - if (resDir != null) { - if (assets.addAssetPath(resDir) == 0) { + if (key.mResDir != null) { + if (assets.addAssetPath(key.mResDir) == 0) { return null; } } - if (splitResDirs != null) { - for (String splitResDir : splitResDirs) { + if (key.mSplitResDirs != null) { + for (final String splitResDir : key.mSplitResDirs) { if (assets.addAssetPath(splitResDir) == 0) { return null; } } } - if (overlayDirs != null) { - for (String idmapPath : overlayDirs) { + if (key.mOverlayDirs != null) { + for (final String idmapPath : key.mOverlayDirs) { assets.addOverlayPath(idmapPath); } } - if (libDirs != null) { - for (String libDir : libDirs) { + if (key.mLibDirs != null) { + for (final String libDir : key.mLibDirs) { if (libDir.endsWith(".apk")) { // Avoid opening files we know do not have resources, // like code-only .jar files. @@ -233,16 +250,17 @@ public class ResourcesManager { } } } + return assets; + } - //Log.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics); - DisplayMetrics dm = getDisplayMetricsLocked(displayId); + private Configuration generateConfig(@NonNull ResourcesKey key, @NonNull DisplayMetrics dm) { Configuration config; - final boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); + final boolean isDefaultDisplay = (key.mDisplayId == Display.DEFAULT_DISPLAY); final boolean hasOverrideConfig = key.hasOverrideConfiguration(); if (!isDefaultDisplay || hasOverrideConfig) { config = new Configuration(getConfiguration()); if (!isDefaultDisplay) { - applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config); + applyNonDefaultDisplayMetricsToConfiguration(dm, config); } if (hasOverrideConfig) { config.updateFrom(key.mOverrideConfiguration); @@ -251,16 +269,212 @@ public class ResourcesManager { } else { config = getConfiguration(); } - r = new Resources(classLoader); - r.setImpl(new ResourcesImpl(assets, dm, config, compatInfo)); - if (DEBUG) Slog.i(TAG, "Created app resources " + resDir + " " + r + ": " - + r.getConfiguration() + " appScale=" + r.getCompatibilityInfo().applicationScale); + return config; + } - final String[] systemLocales = ( - findSystemLocales ? - AssetManager.getSystem().getLocales() : - null); - final String[] nonSystemLocales = assets.getNonSystemLocales(); + + private ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) { + AssetManager assets = createAssetManager(key); + DisplayMetrics dm = getDisplayMetricsLocked(key.mDisplayId); + Configuration config = generateConfig(key, dm); + ResourcesImpl impl = new ResourcesImpl(assets, dm, config, key.mCompatInfo); + if (DEBUG) { + Slog.d(TAG, "- creating impl=" + impl + " with key: " + key); + } + return impl; + } + + /** + * Finds a cached ResourcesImpl object that matches the given ResourcesKey. + * + * @param key The key to match. + * @return a ResourcesImpl if the key matches a cache entry, null otherwise. + */ + private ResourcesImpl findResourcesImplForKey(@NonNull ResourcesKey key) { + WeakReference weakImplRef = mResourceImpls.get(key); + ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null; + if (impl != null && impl.getAssets().isUpToDate()) { + return impl; + } + return null; + } + + /** + * Find the ResourcesKey that this ResourcesImpl object is associated with. + * @return the ResourcesKey or null if none was found. + */ + private ResourcesKey findKeyForResourceImpl(@NonNull ResourcesImpl resourceImpl) { + final int refCount = mResourceImpls.size(); + for (int i = 0; i < refCount; i++) { + WeakReference weakImplRef = mResourceImpls.valueAt(i); + ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null; + if (impl != null && resourceImpl == impl) { + return mResourceImpls.keyAt(i); + } + } + return null; + } + + /** + * Gets an existing Resources object tied to this Activity, or creates one if it doesn't exist + * or the class loader is different. + */ + private Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken, + @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl) { + // This is a request tied to an Activity, meaning we will need to update all + // Activity related Resources to match this configuration. + WeakReference weakResourceRef = mActivityResourceReferences.get(activityToken); + Resources resources = weakResourceRef != null ? weakResourceRef.get() : null; + if (resources == null || !Objects.equals(resources.getClassLoader(), classLoader)) { + resources = new Resources(classLoader); + mActivityResourceReferences.put(activityToken, new WeakReference<>(resources)); + if (DEBUG) { + Slog.d(TAG, "- creating new ref=" + resources); + } + } else { + if (DEBUG) { + Slog.d(TAG, "- using existing ref=" + resources); + } + } + + if (resources.getImpl() != impl) { + if (DEBUG) { + Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl); + } + + // Setting an impl is expensive because we update all ThemeImpl references. + // too. + resources.setImpl(impl); + } + return resources; + } + + /** + * Gets an existing Resources object if the class loader and ResourcesImpl are the same, + * otherwise creates a new Resources object. + */ + private Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader, + @NonNull ResourcesImpl impl) { + // Find an existing Resources that has this ResourcesImpl set. + final int refCount = mResourceReferences.size(); + for (int i = 0; i < refCount; i++) { + WeakReference weakResourceRef = mResourceReferences.get(i); + Resources resources = weakResourceRef != null ? weakResourceRef.get() : null; + if (resources != null && + Objects.equals(resources.getClassLoader(), classLoader) && + resources.getImpl() == impl) { + if (DEBUG) { + Slog.d(TAG, "- using existing ref=" + resources); + } + return resources; + } + } + + // Create a new Resources reference and use the existing ResourcesImpl object. + Resources resources = new Resources(classLoader); + resources.setImpl(impl); + mResourceReferences.add(new WeakReference<>(resources)); + if (DEBUG) { + Slog.d(TAG, "- creating new ref=" + resources); + Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl); + } + return resources; + } + + /** + * Gets or creates a new Resources object associated with the IBinder token. References returned + * by this method live as long as the Activity, meaning they can be cached and used by the + * Activity even after a configuration change. If any other parameter is changed + * (resDir, splitResDirs, overrideConfig) for a given Activity, the same Resources object + * is updated and handed back to the caller. However, changing the class loader will result in a + * new Resources object. + *

+ * If activityToken is null, a cached Resources object will be returned if it matches the + * input parameters. Otherwise a new Resources object that satisfies these parameters is + * returned. + * + * @param activityToken Represents an Activity. If null, global resources are assumed. + * @param resDir The base resource path. Can be null (only framework resources will be loaded). + * @param splitResDirs An array of split resource paths. Can be null. + * @param overlayDirs An array of overlay paths. Can be null. + * @param libDirs An array of resource library paths. Can be null. + * @param displayId The ID of the display for which to create the resources. + * @param overrideConfig The configuration to apply on top of the base configuration. Can be + * null. Mostly used with Activities that are in multi-window which may override width and + * height properties from the base config. + * @param compatInfo The compatibility settings to use. Cannot be null. A default to use is + * {@link CompatibilityInfo#DEFAULT_COMPATIBILITY_INFO}. + * @param classLoader The class loader to use when inflating Resources. If null, the + * {@link ClassLoader#getSystemClassLoader()} is used. + * @return a Resources object from which to access resources. + */ + public Resources getResources(@Nullable IBinder activityToken, + @Nullable String resDir, + @Nullable String[] splitResDirs, + @Nullable String[] overlayDirs, + @Nullable String[] libDirs, + int displayId, + @Nullable Configuration overrideConfig, + @NonNull CompatibilityInfo compatInfo, + @Nullable ClassLoader classLoader) { + final ResourcesKey key = new ResourcesKey( + resDir, + splitResDirs, + overlayDirs, + libDirs, + displayId, + overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy + compatInfo); + + classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader(); + + final boolean findSystemLocales; + final boolean hasNonSystemLocales; + synchronized (this) { + findSystemLocales = (mSystemLocales.length == 0); + hasNonSystemLocales = mHasNonSystemLocales; + + if (DEBUG) { + Throwable here = new Throwable(); + here.fillInStackTrace(); + Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here); + } + + if (activityToken != null) { + ResourcesImpl resourcesImpl = findResourcesImplForKey(key); + if (resourcesImpl != null) { + if (DEBUG) { + Slog.d(TAG, "- using existing impl=" + resourcesImpl); + } + return getOrCreateResourcesForActivityLocked(activityToken, classLoader, + resourcesImpl); + } + + // We will create the ResourcesImpl object outside of holding this lock. + + } else { + // Clean up any dead references so they don't pile up. + ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate); + + // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl + ResourcesImpl resourcesImpl = findResourcesImplForKey(key); + if (resourcesImpl != null) { + if (DEBUG) { + Slog.d(TAG, "- using existing impl=" + resourcesImpl); + } + return getOrCreateResourcesLocked(classLoader, resourcesImpl); + } + + // We will create the ResourcesImpl object outside of holding this lock. + } + } + + // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now. + ResourcesImpl resourcesImpl = createResourcesImpl(key); + + final String[] systemLocales = findSystemLocales + ? AssetManager.getSystem().getLocales() : null; + final String[] nonSystemLocales = resourcesImpl.getAssets().getNonSystemLocales(); // Avoid checking for non-pseudo-locales if we already know there were some from a previous // Resources. The default value (for when hasNonSystemLocales is true) doesn't matter, // since mHasNonSystemLocales will also be true, and thus isPseudoLocalesOnly would not be @@ -269,41 +483,75 @@ public class ResourcesManager { LocaleList.isPseudoLocalesOnly(nonSystemLocales); synchronized (this) { - WeakReference wr = mActiveResources.get(key); - Resources existing = wr != null ? wr.get() : null; - if (existing != null && existing.getAssets().isUpToDate()) { - // Someone else already created the resources while we were - // unlocked; go ahead and use theirs. - r.getAssets().close(); - return existing; - } - - // XXX need to remove entries when weak references go away - mActiveResources.put(key, new WeakReference<>(r)); if (mSystemLocales.length == 0) { mSystemLocales = systemLocales; } mNonSystemLocales.addAll(Arrays.asList(nonSystemLocales)); mHasNonSystemLocales = mHasNonSystemLocales || !isPseudoLocalesOnly; - if (DEBUG) Slog.v(TAG, "mActiveResources.size()=" + mActiveResources.size()); - return r; + + ResourcesImpl existingResourcesImpl = findResourcesImplForKey(key); + if (existingResourcesImpl != null) { + if (DEBUG) { + Slog.d(TAG, "- got beat! existing impl=" + existingResourcesImpl + + " new impl=" + resourcesImpl); + } + resourcesImpl.getAssets().close(); + resourcesImpl = existingResourcesImpl; + } else { + // Add this ResourcesImpl to the cache. + mResourceImpls.put(key, new WeakReference<>(resourcesImpl)); + } + + final Resources resources; + if (activityToken != null) { + resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader, + resourcesImpl); + } else { + resources = getOrCreateResourcesLocked(classLoader, resourcesImpl); + } + return resources; } } /** - * Removes the top level Resources for applications with the given compatibility info. - * @see #getTopLevelResources(String, String[], String[], String[], int, Configuration, CompatibilityInfo, ClassLoader) + * Updates an Activity's Resources object with overrideConfig. The Resources object + * that was previously returned by + * {@link #getResources(IBinder, String, String[], String[], String[], int, Configuration, + * CompatibilityInfo, ClassLoader)} is + * still valid and will have the updated configuration. + * @param activityToken The Activity token. + * @param overrideConfig The configuration override to update. */ - void removeTopLevelResources(String resDir, int displayId, Configuration overrideConfiguration, - CompatibilityInfo compatInfo) { - final float scale = compatInfo.applicationScale; - final Configuration overrideConfigCopy = (overrideConfiguration != null) - ? new Configuration(overrideConfiguration) : null; - final ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale); - mActiveResources.remove(key); + public void updateResourcesForActivity(@NonNull IBinder activityToken, + @Nullable Configuration overrideConfig) { + final ClassLoader classLoader; + final ResourcesKey oldKey; + synchronized (this) { + // Extract the ResourcesKey that was last used to create the Resources for this + // activity. + WeakReference weakResRef = mActivityResourceReferences.get(activityToken); + final Resources resources = weakResRef != null ? weakResRef.get() : null; + if (resources == null) { + Slog.e(TAG, "can't update resources for uncached activity " + activityToken); + return; + } + + classLoader = resources.getClassLoader(); + oldKey = findKeyForResourceImpl(resources.getImpl()); + if (oldKey == null) { + Slog.e(TAG, "can't find ResourcesKey for resources impl=" + resources.getImpl()); + return; + } + } + + // Update the Resources object with the new override config and all of the existing + // settings. + getResources(activityToken, oldKey.mResDir, oldKey.mSplitResDirs, oldKey.mOverlayDirs, + oldKey.mLibDirs, oldKey.mDisplayId, overrideConfig, oldKey.mCompatInfo, + classLoader); } - /* package */ void setDefaultLocalesLocked(LocaleList locales) { + /* package */ void setDefaultLocalesLocked(@NonNull LocaleList locales) { final int bestLocale; if (mHasNonSystemLocales) { bestLocale = locales.getFirstMatchIndexWithEnglishSupported(mNonSystemLocales); @@ -317,11 +565,8 @@ public class ResourcesManager { LocaleList.setDefault(locales, bestLocale); } - final boolean applyConfigurationToResourcesLocked(Configuration config, - CompatibilityInfo compat) { - if (mResConfiguration == null) { - mResConfiguration = new Configuration(); - } + public final boolean applyConfigurationToResourcesLocked(@NonNull Configuration config, + @Nullable CompatibilityInfo compat) { if (!mResConfiguration.isOtherSeqNewer(config) && compat == null) { if (DEBUG || DEBUG_CONFIGURATION) Slog.v(TAG, "Skipping new config: curSeq=" + mResConfiguration.seq + ", newSeq=" + config.seq); @@ -368,9 +613,9 @@ public class ResourcesManager { Configuration tmpConfig = null; - for (int i = mActiveResources.size() - 1; i >= 0; i--) { - ResourcesKey key = mActiveResources.keyAt(i); - Resources r = mActiveResources.valueAt(i).get(); + for (int i = mResourceImpls.size() - 1; i >= 0; i--) { + ResourcesKey key = mResourceImpls.keyAt(i); + ResourcesImpl r = mResourceImpls.valueAt(i).get(); if (r != null) { if (DEBUG || DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources " + r + " config to: " + localeAdjustedConfig); @@ -385,7 +630,7 @@ public class ResourcesManager { tmpConfig.setTo(localeAdjustedConfig); if (!isDefaultDisplay) { dm = getDisplayMetricsLocked(displayId); - applyNonDefaultDisplayMetricsToConfigurationLocked(dm, tmpConfig); + applyNonDefaultDisplayMetricsToConfiguration(dm, tmpConfig); } if (hasOverrideConfiguration) { tmpConfig.updateFrom(key.mOverrideConfiguration); @@ -398,11 +643,10 @@ public class ResourcesManager { // + " " + r + ": " + r.getConfiguration()); } else { //Slog.i(TAG, "Removing old resources " + v.getKey()); - mActiveResources.removeAt(i); + mResourceImpls.removeAt(i); } } return changes != 0; } - -} +} \ No newline at end of file diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index f337fe6fe4b1a..fb706fcb3c94b 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -54,6 +54,7 @@ import android.util.TypedValue; import android.view.ViewDebug; import android.view.ViewHierarchyEncoder; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.XmlUtils; @@ -62,6 +63,8 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.util.ArrayList; /** * Class for accessing an application's resources. This sits on top of the @@ -112,6 +115,13 @@ public class Resources { final ClassLoader mClassLoader; + /** + * WeakReferences to Themes that were constructed from this Resources object. + * We keep track of these in case our underlying implementation is changed, in which case + * the Themes must also get updated ThemeImpls. + */ + private final ArrayList> mThemeRefs = new ArrayList<>(); + /** * Returns the most appropriate default theme for the specified target SDK version. *

    @@ -231,10 +241,42 @@ public class Resources { } /** + * Set the underlying implementation (containing all the resources and caches) + * and updates all Theme references to new implementations as well. * @hide */ public void setImpl(ResourcesImpl impl) { + if (impl == mResourcesImpl) { + return; + } + mResourcesImpl = impl; + + // Create new ThemeImpls that are identical to the ones we have. + synchronized (mThemeRefs) { + final int count = mThemeRefs.size(); + for (int i = 0; i < count; i++) { + WeakReference weakThemeRef = mThemeRefs.get(i); + Theme theme = weakThemeRef != null ? weakThemeRef.get() : null; + if (theme != null) { + theme.setImpl(mResourcesImpl.newThemeImpl(theme.getKey())); + } + } + } + } + + /** + * @hide + */ + public ResourcesImpl getImpl() { + return mResourcesImpl; + } + + /** + * @hide + */ + public ClassLoader getClassLoader() { + return mClassLoader; } /** @@ -1683,6 +1725,7 @@ public class Resources { public final Theme newTheme() { Theme theme = new Theme(); theme.setImpl(mResourcesImpl.newThemeImpl()); + mThemeRefs.add(new WeakReference<>(theme)); return theme; } @@ -1785,6 +1828,7 @@ public class Resources { * This is just for testing. * @hide */ + @VisibleForTesting public void setCompatibilityInfo(CompatibilityInfo ci) { if (ci != null) { mResourcesImpl.updateConfiguration(null, null, ci); @@ -2068,6 +2112,15 @@ public class Resources { return mResourcesImpl.loadXmlResourceParser(file, id, assetCookie, type); } + /** + * Called by ConfigurationBoundResourceCacheTest. + * @hide + */ + @VisibleForTesting + public int calcConfigChanges(Configuration config) { + return mResourcesImpl.calcConfigChanges(config); + } + /** * Obtains styled attributes from the theme, if available, or unstyled * resources if the theme is null. diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index 2ffd3722b5a3a..0858cb8b9305d 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -48,6 +48,13 @@ import java.util.Arrays; import java.util.Locale; /** + * The implementation of Resource access. This class contains the AssetManager and all caches + * associated with it. + * + * {@link Resources} is just a thing wrapper around this class. When a configuration change + * occurs, clients can retain the same {@link Resources} reference because the underlying + * {@link ResourcesImpl} object will be updated or re-created. + * * @hide */ public class ResourcesImpl { @@ -126,14 +133,14 @@ public class ResourcesImpl { * @param compatInfo this resource's compatibility info. Must not be null. */ public ResourcesImpl(AssetManager assets, DisplayMetrics metrics, Configuration config, - CompatibilityInfo compatInfo) { + CompatibilityInfo compatInfo) { mAssets = assets; mMetrics.setToDefaults(); updateConfiguration(config, metrics, compatInfo); mAssets.ensureStringBlocks(); } - AssetManager getAssets() { + public AssetManager getAssets() { return mAssets; } @@ -174,7 +181,7 @@ public class ResourcesImpl { } void getValueForDensity(@AnyRes int id, int density, TypedValue outValue, - boolean resolveRefs) throws NotFoundException { + boolean resolveRefs) throws NotFoundException { boolean found = mAssets.getResourceValue(id, density, outValue, resolveRefs); if (found) { return; @@ -298,8 +305,8 @@ public class ResourcesImpl { return mStateListAnimatorCache; } - void updateConfiguration(Configuration config, DisplayMetrics metrics, - CompatibilityInfo compat) { + public void updateConfiguration(Configuration config, DisplayMetrics metrics, + CompatibilityInfo compat) { synchronized (mAccessLock) { if (false) { Slog.i(TAG, "**** Updating config of " + this + ": old config is " @@ -388,9 +395,9 @@ public class ResourcesImpl { } /** - * Called by ConfigurationBoundResourceCacheTest via reflection. + * Called by ConfigurationBoundResourceCacheTest. */ - private int calcConfigChanges(Configuration config) { + public int calcConfigChanges(Configuration config) { int configChanges = 0xfffffff; if (config != null) { mTmpConfig.setTo(config); @@ -460,7 +467,7 @@ public class ResourcesImpl { @Nullable Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme, - boolean useCache) throws NotFoundException { + boolean useCache) throws NotFoundException { try { if (TRACE_FOR_PRELOAD) { // Log only framework resources @@ -553,7 +560,7 @@ public class ResourcesImpl { } private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches, - Resources.Theme theme, boolean usesTheme, long key, Drawable dr) { + Resources.Theme theme, boolean usesTheme, long key, Drawable dr) { final Drawable.ConstantState cs = dr.getConstantState(); if (cs == null) { return; @@ -587,7 +594,7 @@ public class ResourcesImpl { } private boolean verifyPreloadConfig(int changingConfigurations, int allowVarying, - int resourceId, String name) { + int resourceId, String name) { // We allow preloading of resources even if they vary by font scale (which // doesn't impact resource selection) or density (which we handle specially by // simply turning off all preloading), as well as any other configs specified @@ -625,7 +632,7 @@ public class ResourcesImpl { * Loads a drawable from XML or resources stream. */ private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id, - Resources.Theme theme) { + Resources.Theme theme) { if (value.string == null) { throw new NotFoundException("Resource \"" + getResourceName(id) + "\" (" + Integer.toHexString(id) + ") is not a Drawable (color or path): " + value); @@ -681,7 +688,7 @@ public class ResourcesImpl { * Last, parse the XML and generate the CSL. */ private ComplexColor loadComplexColorFromName(Resources wrapper, Resources.Theme theme, - TypedValue value, int id) { + TypedValue value, int id) { final long key = (((long) value.assetCookie) << 32) | value.data; final ConfigurationBoundResourceCache cache = mComplexColorCache; ComplexColor complexColor = cache.getInstance(key, wrapper, theme); @@ -714,7 +721,7 @@ public class ResourcesImpl { @Nullable ComplexColor loadComplexColor(Resources wrapper, @NonNull TypedValue value, int id, - Resources.Theme theme) { + Resources.Theme theme) { if (TRACE_FOR_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { @@ -755,7 +762,7 @@ public class ResourcesImpl { @Nullable ColorStateList loadColorStateList(Resources wrapper, TypedValue value, int id, - Resources.Theme theme) + Resources.Theme theme) throws NotFoundException { if (TRACE_FOR_PRELOAD) { // Log only framework resources @@ -815,7 +822,7 @@ public class ResourcesImpl { */ @Nullable private ComplexColor loadComplexColorForCookie(Resources wrapper, TypedValue value, int id, - Resources.Theme theme) { + Resources.Theme theme) { if (value.string == null) { throw new UnsupportedOperationException( "Can't convert to ComplexColor: type=0x" + value.type); @@ -893,8 +900,8 @@ public class ResourcesImpl { * @throws NotFoundException if the file could not be loaded */ @NonNull - XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, - int assetCookie, @NonNull String type) + XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie, + @NonNull String type) throws NotFoundException { if (id != 0) { try { @@ -975,6 +982,16 @@ public class ResourcesImpl { return new ThemeImpl(); } + /** + * Creates a new ThemeImpl which is already set to the given Resources.ThemeKey. + */ + ThemeImpl newThemeImpl(Resources.ThemeKey key) { + ThemeImpl impl = new ThemeImpl(); + impl.mKey.setTo(key); + impl.rebase(); + return impl; + } + public class ThemeImpl { /** * Unique key for the series of styles applied to this theme. @@ -1035,10 +1052,10 @@ public class ResourcesImpl { @NonNull TypedArray obtainStyledAttributes(@NonNull Resources.Theme wrapper, - AttributeSet set, - @StyleableRes int[] attrs, - @AttrRes int defStyleAttr, - @StyleRes int defStyleRes) { + AttributeSet set, + @StyleableRes int[] attrs, + @AttrRes int defStyleAttr, + @StyleRes int defStyleRes) { synchronized (mKey) { final int len = attrs.length; final TypedArray array = TypedArray.obtain(wrapper.getResources(), len); @@ -1060,8 +1077,8 @@ public class ResourcesImpl { @NonNull TypedArray resolveAttributes(@NonNull Resources.Theme wrapper, - @NonNull int[] values, - @NonNull int[] attrs) { + @NonNull int[] values, + @NonNull int[] attrs) { synchronized (mKey) { final int len = attrs.length; if (values == null || len != values.length) { diff --git a/core/java/android/content/res/ResourcesKey.java b/core/java/android/content/res/ResourcesKey.java index 26205718c44d9..e89449283c5e2 100644 --- a/core/java/android/content/res/ResourcesKey.java +++ b/core/java/android/content/res/ResourcesKey.java @@ -17,32 +17,59 @@ package android.content.res; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; +import java.util.Arrays; import java.util.Objects; /** @hide */ public final class ResourcesKey { - private final String mResDir; - private final float mScale; - private final int mHash; + @Nullable + public final String mResDir; + + @Nullable + public final String[] mSplitResDirs; + + @Nullable + public final String[] mOverlayDirs; + + @Nullable + public final String[] mLibDirs; public final int mDisplayId; + @NonNull public final Configuration mOverrideConfiguration; - public ResourcesKey(String resDir, int displayId, Configuration overrideConfiguration, - float scale) { + @NonNull + public final CompatibilityInfo mCompatInfo; + + private final int mHash; + + public ResourcesKey(@Nullable String resDir, + @Nullable String[] splitResDirs, + @Nullable String[] overlayDirs, + @Nullable String[] libDirs, + int displayId, + @Nullable Configuration overrideConfig, + @Nullable CompatibilityInfo compatInfo) { mResDir = resDir; + mSplitResDirs = splitResDirs; + mOverlayDirs = overlayDirs; + mLibDirs = libDirs; mDisplayId = displayId; - mOverrideConfiguration = overrideConfiguration != null - ? overrideConfiguration : Configuration.EMPTY; - mScale = scale; + mOverrideConfiguration = overrideConfig != null ? overrideConfig : Configuration.EMPTY; + mCompatInfo = compatInfo != null ? compatInfo : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; int hash = 17; - hash = 31 * hash + (mResDir == null ? 0 : mResDir.hashCode()); + hash = 31 * hash + Objects.hashCode(mResDir); + hash = 31 * hash + Arrays.hashCode(mSplitResDirs); + hash = 31 * hash + Arrays.hashCode(mOverlayDirs); + hash = 31 * hash + Arrays.hashCode(mLibDirs); hash = 31 * hash + mDisplayId; - hash = 31 * hash + mOverrideConfiguration.hashCode(); - hash = 31 * hash + Float.floatToIntBits(mScale); + hash = 31 * hash + Objects.hashCode(mOverrideConfiguration); + hash = 31 * hash + Objects.hashCode(mCompatInfo); mHash = hash; } @@ -60,18 +87,32 @@ public final class ResourcesKey { if (!(obj instanceof ResourcesKey)) { return false; } + ResourcesKey peer = (ResourcesKey) obj; + if (mHash != peer.mHash) { + // If the hashes don't match, the objects can't match. + return false; + } if (!Objects.equals(mResDir, peer.mResDir)) { return false; } + if (!Arrays.equals(mSplitResDirs, peer.mSplitResDirs)) { + return false; + } + if (!Arrays.equals(mOverlayDirs, peer.mOverlayDirs)) { + return false; + } + if (!Arrays.equals(mLibDirs, peer.mLibDirs)) { + return false; + } if (mDisplayId != peer.mDisplayId) { return false; } - if (!mOverrideConfiguration.equals(peer.mOverrideConfiguration)) { + if (!Objects.equals(mOverrideConfiguration, peer.mOverrideConfiguration)) { return false; } - if (mScale != peer.mScale) { + if (!Objects.equals(mCompatInfo, peer.mCompatInfo)) { return false; } return true; @@ -79,6 +120,29 @@ public final class ResourcesKey { @Override public String toString() { - return Integer.toHexString(mHash); + StringBuilder builder = new StringBuilder().append("ResourcesKey{"); + builder.append(" mHash=").append(Integer.toHexString(mHash)); + builder.append(" mResDir=").append(mResDir); + builder.append(" mSplitDirs=["); + if (mSplitResDirs != null) { + builder.append(TextUtils.join(",", mSplitResDirs)); + } + builder.append("]"); + builder.append(" mOverlayDirs=["); + if (mOverlayDirs != null) { + builder.append(TextUtils.join(",", mOverlayDirs)); + } + builder.append("]"); + builder.append(" mLibDirs=["); + if (mLibDirs != null) { + builder.append(TextUtils.join(",", mLibDirs)); + } + builder.append("]"); + builder.append(" mDisplayId=").append(mDisplayId); + builder.append(" mOverrideConfig=").append(Configuration.resourceQualifierString( + mOverrideConfiguration)); + builder.append(" mCompatInfo=").append(mCompatInfo); + builder.append("}"); + return builder.toString(); } } diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index a84a061077bf2..ee73b90097af3 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -27,6 +27,7 @@ import libcore.util.EmptyArray; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -477,4 +478,48 @@ public class ArrayUtils { } return !diff; } + + /** + * Removes elements that match the predicate in an efficient way that alters the order of + * elements in the collection. This should only be used if order is not important. + * @param collection The ArrayList from which to remove elements. + * @param predicate The predicate that each element is tested against. + * @return the number of elements removed. + */ + public static int unstableRemoveIf(@Nullable ArrayList collection, + @NonNull java.util.function.Predicate predicate) { + if (collection == null) { + return 0; + } + + final int size = collection.size(); + int leftIdx = 0; + int rightIdx = size - 1; + while (leftIdx <= rightIdx) { + // Find the next element to remove moving left to right. + while (leftIdx < size && !predicate.test(collection.get(leftIdx))) { + leftIdx++; + } + + // Find the next element to keep moving right to left. + while (rightIdx > leftIdx && predicate.test(collection.get(rightIdx))) { + rightIdx--; + } + + if (leftIdx >= rightIdx) { + // Done. + break; + } + + Collections.swap(collection, leftIdx, rightIdx); + leftIdx++; + rightIdx--; + } + + // leftIdx is now at the end. + for (int i = size - 1; i >= leftIdx; i--) { + collection.remove(i); + } + return size - leftIdx; + } } diff --git a/core/tests/coretests/src/android/content/res/ConfigurationBoundResourceCacheTest.java b/core/tests/coretests/src/android/content/res/ConfigurationBoundResourceCacheTest.java index 5d46489878119..47554a68645aa 100644 --- a/core/tests/coretests/src/android/content/res/ConfigurationBoundResourceCacheTest.java +++ b/core/tests/coretests/src/android/content/res/ConfigurationBoundResourceCacheTest.java @@ -17,20 +17,18 @@ package android.content.res; import android.test.ActivityInstrumentationTestCase2; +import android.test.suitebuilder.annotation.SmallTest; import android.util.TypedValue; import com.android.frameworks.coretests.R; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; public class ConfigurationBoundResourceCacheTest extends ActivityInstrumentationTestCase2 { ConfigurationBoundResourceCache mCache; - Method mCalcConfigChanges; - public ConfigurationBoundResourceCacheTest() { super(ResourceCacheActivity.class); } @@ -41,33 +39,42 @@ public class ConfigurationBoundResourceCacheTest mCache = new ConfigurationBoundResourceCache<>(); } + @SmallTest public void testGetEmpty() { - assertNull(mCache.get(-1, null)); + final Resources res = getActivity().getResources(); + assertNull(mCache.getInstance(-1, res, null)); } + @SmallTest public void testSetGet() { mCache.put(1, null, new DummyFloatConstantState(5f)); - assertEquals(5f, mCache.get(1, null)); - assertNotSame(5f, mCache.get(1, null)); - assertEquals(null, mCache.get(1, getActivity().getTheme())); + final Resources res = getActivity().getResources(); + assertEquals(5f, mCache.getInstance(1, res, null)); + assertNotSame(5f, mCache.getInstance(1, res, null)); + assertEquals(null, mCache.getInstance(1, res, getActivity().getTheme())); } + @SmallTest public void testSetGetThemed() { mCache.put(1, getActivity().getTheme(), new DummyFloatConstantState(5f)); - assertEquals(null, mCache.get(1, null)); - assertEquals(5f, mCache.get(1, getActivity().getTheme())); - assertNotSame(5f, mCache.get(1, getActivity().getTheme())); + final Resources res = getActivity().getResources(); + assertEquals(null, mCache.getInstance(1, res, null)); + assertEquals(5f, mCache.getInstance(1, res, getActivity().getTheme())); + assertNotSame(5f, mCache.getInstance(1, res, getActivity().getTheme())); } + @SmallTest public void testMultiThreadPutGet() { mCache.put(1, getActivity().getTheme(), new DummyFloatConstantState(5f)); mCache.put(1, null, new DummyFloatConstantState(10f)); - assertEquals(10f, mCache.get(1, null)); - assertNotSame(10f, mCache.get(1, null)); - assertEquals(5f, mCache.get(1, getActivity().getTheme())); - assertNotSame(5f, mCache.get(1, getActivity().getTheme())); + final Resources res = getActivity().getResources(); + assertEquals(10f, mCache.getInstance(1, res, null)); + assertNotSame(10f, mCache.getInstance(1, res, null)); + assertEquals(5f, mCache.getInstance(1, res, getActivity().getTheme())); + assertNotSame(5f, mCache.getInstance(1, res, getActivity().getTheme())); } + @SmallTest public void testVoidConfigChange() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { TypedValue staticValue = new TypedValue(); @@ -83,11 +90,12 @@ public class ConfigurationBoundResourceCacheTest Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE; int changes = calcConfigChanges(res, newCnf); - assertEquals(staticDim, mCache.get(key, getActivity().getTheme())); + assertEquals(staticDim, mCache.getInstance(key, res, getActivity().getTheme())); mCache.onConfigurationChange(changes); - assertEquals(staticDim, mCache.get(key, getActivity().getTheme())); + assertEquals(staticDim, mCache.getInstance(key, res, getActivity().getTheme())); } + @SmallTest public void testEffectiveConfigChange() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { TypedValue changingValue = new TypedValue(); @@ -105,11 +113,12 @@ public class ConfigurationBoundResourceCacheTest Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE; int changes = calcConfigChanges(res, newCnf); - assertEquals(changingDim, mCache.get(key, getActivity().getTheme())); + assertEquals(changingDim, mCache.getInstance(key, res, getActivity().getTheme())); mCache.onConfigurationChange(changes); assertNull(mCache.get(key, getActivity().getTheme())); } + @SmallTest public void testConfigChangeMultipleResources() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { TypedValue staticValue = new TypedValue(); @@ -130,17 +139,19 @@ public class ConfigurationBoundResourceCacheTest Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE; int changes = calcConfigChanges(res, newCnf); - assertEquals(staticDim, mCache.get(R.dimen.resource_cache_test_generic, - getActivity().getTheme())); - assertEquals(changingDim, mCache.get(R.dimen.resource_cache_test_orientation_dependent, + assertEquals(staticDim, mCache.getInstance(R.dimen.resource_cache_test_generic, res, getActivity().getTheme())); + assertEquals(changingDim, + mCache.getInstance(R.dimen.resource_cache_test_orientation_dependent, res, + getActivity().getTheme())); mCache.onConfigurationChange(changes); - assertEquals(staticDim, mCache.get(R.dimen.resource_cache_test_generic, + assertEquals(staticDim, mCache.getInstance(R.dimen.resource_cache_test_generic, res, getActivity().getTheme())); - assertNull(mCache.get(R.dimen.resource_cache_test_orientation_dependent, + assertNull(mCache.getInstance(R.dimen.resource_cache_test_orientation_dependent, res, getActivity().getTheme())); } + @SmallTest public void testConfigChangeMultipleThemes() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { TypedValue[] staticValues = new TypedValue[]{new TypedValue(), new TypedValue()}; @@ -172,31 +183,27 @@ public class ConfigurationBoundResourceCacheTest int changes = calcConfigChanges(res, newCnf); for (int i = 0; i < 2; i++) { final Resources.Theme theme = i == 0 ? getActivity().getTheme() : null; - assertEquals(staticDim, mCache.get(R.dimen.resource_cache_test_generic, theme)); + assertEquals(staticDim, + mCache.getInstance(R.dimen.resource_cache_test_generic, res, theme)); assertEquals(changingDim, - mCache.get(R.dimen.resource_cache_test_orientation_dependent, theme)); + mCache.getInstance(R.dimen.resource_cache_test_orientation_dependent, res, + theme)); } mCache.onConfigurationChange(changes); for (int i = 0; i < 2; i++) { final Resources.Theme theme = i == 0 ? getActivity().getTheme() : null; - assertEquals(staticDim, mCache.get(R.dimen.resource_cache_test_generic, theme)); - assertNull(mCache.get(R.dimen.resource_cache_test_orientation_dependent, theme)); + assertEquals(staticDim, + mCache.getInstance(R.dimen.resource_cache_test_generic, res, theme)); + assertNull(mCache.getInstance(R.dimen.resource_cache_test_orientation_dependent, res, + theme)); } } - private int calcConfigChanges(Resources resources, Configuration configuration) - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - if (mCalcConfigChanges == null) { - mCalcConfigChanges = Resources.class.getDeclaredMethod("calcConfigChanges", - Configuration.class); - mCalcConfigChanges.setAccessible(true); - } - return (Integer) mCalcConfigChanges.invoke(resources, configuration); - + private static int calcConfigChanges(Resources resources, Configuration configuration) { + return resources.calcConfigChanges(configuration); } - static class DummyFloatConstantState extends - ConstantState { + static class DummyFloatConstantState extends ConstantState { final Float mObj; diff --git a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java new file mode 100644 index 0000000000000..3cadbf64c7a37 --- /dev/null +++ b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.content.res; + +import android.annotation.NonNull; +import android.app.ResourcesManager; +import android.os.Binder; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.DisplayMetrics; +import android.util.LocaleList; +import android.util.TypedValue; +import android.view.Display; +import junit.framework.TestCase; + +public class ResourcesManagerTest extends TestCase { + private static final String APP_ONE_RES_DIR = "app_one.apk"; + private static final String APP_ONE_RES_SPLIT_DIR = "app_one_split.apk"; + private static final String APP_TWO_RES_DIR = "app_two.apk"; + private static final String LIB_RES_DIR = "lib.apk"; + + private ResourcesManager mResourcesManager; + private DisplayMetrics mDisplayMetrics; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mDisplayMetrics = new DisplayMetrics(); + mDisplayMetrics.setToDefaults(); + + // Override defaults (which take device specific properties). + mDisplayMetrics.density = 1.0f; + mDisplayMetrics.densityDpi = DisplayMetrics.DENSITY_DEFAULT; + mDisplayMetrics.xdpi = DisplayMetrics.DENSITY_DEFAULT; + mDisplayMetrics.ydpi = DisplayMetrics.DENSITY_DEFAULT; + mDisplayMetrics.noncompatDensity = mDisplayMetrics.density; + mDisplayMetrics.noncompatDensityDpi = mDisplayMetrics.densityDpi; + mDisplayMetrics.noncompatXdpi = DisplayMetrics.DENSITY_DEFAULT; + mDisplayMetrics.noncompatYdpi = DisplayMetrics.DENSITY_DEFAULT; + + mResourcesManager = new ResourcesManager() { + @Override + protected AssetManager createAssetManager(@NonNull ResourcesKey key) { + return new AssetManager(); + } + + @Override + protected DisplayMetrics getDisplayMetricsLocked(int displayId) { + return mDisplayMetrics; + } + }; + } + + @SmallTest + public void testMultipleCallsWithIdenticalParametersCacheReference() { + Resources resources = mResourcesManager.getResources( + null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(resources); + + Resources newResources = mResourcesManager.getResources( + null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(newResources); + assertSame(resources, newResources); + } + + @SmallTest + public void testMultipleCallsWithDifferentParametersReturnDifferentReferences() { + Resources resources = mResourcesManager.getResources( + null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(resources); + + Configuration overrideConfig = new Configuration(); + overrideConfig.smallestScreenWidthDp = 200; + Resources newResources = mResourcesManager.getResources( + null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, overrideConfig, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(newResources); + assertNotSame(resources, newResources); + } + + @SmallTest + public void testAddingASplitCreatesANewImpl() { + Resources resources1 = mResourcesManager.getResources( + null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(resources1); + + Resources resources2 = mResourcesManager.getResources( + null, APP_ONE_RES_DIR, new String[] { APP_ONE_RES_SPLIT_DIR }, null, null, + Display.DEFAULT_DISPLAY, null, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(resources2); + + assertNotSame(resources1, resources2); + assertNotSame(resources1.getImpl(), resources2.getImpl()); + } + + @SmallTest + public void testUpdateConfigurationUpdatesAllAssetManagers() { + Resources resources1 = mResourcesManager.getResources( + null, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(resources1); + + Resources resources2 = mResourcesManager.getResources( + null, APP_TWO_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(resources2); + + Binder activity = new Binder(); + final Configuration overrideConfig = new Configuration(); + overrideConfig.orientation = Configuration.ORIENTATION_LANDSCAPE; + Resources resources3 = mResourcesManager.getResources( + activity, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, + overrideConfig, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(resources3); + + // No Resources object should be the same. + assertNotSame(resources1, resources2); + assertNotSame(resources1, resources3); + assertNotSame(resources2, resources3); + + // Each ResourcesImpl should be different. + assertNotSame(resources1.getImpl(), resources2.getImpl()); + assertNotSame(resources1.getImpl(), resources3.getImpl()); + assertNotSame(resources2.getImpl(), resources3.getImpl()); + + Configuration newConfig = new Configuration(); + newConfig.orientation = Configuration.ORIENTATION_LANDSCAPE; + mResourcesManager.applyConfigurationToResourcesLocked(newConfig, null); + + final Configuration expectedConfig = new Configuration(); + expectedConfig.setLocales(LocaleList.getAdjustedDefault()); + expectedConfig.densityDpi = mDisplayMetrics.densityDpi; + expectedConfig.orientation = Configuration.ORIENTATION_LANDSCAPE; + + assertEquals(expectedConfig, resources1.getConfiguration()); + assertEquals(expectedConfig, resources2.getConfiguration()); + assertEquals(expectedConfig, resources3.getConfiguration()); + } + + @SmallTest + public void testTwoActivitiesWithIdenticalParametersShareImpl() { + Binder activity1 = new Binder(); + Resources resources1 = mResourcesManager.getResources( + activity1, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(resources1); + + Binder activity2 = new Binder(); + Resources resources2 = mResourcesManager.getResources( + activity2, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(resources1); + + // The references themselves should be unique. + assertNotSame(resources1, resources2); + + // The implementations should be the same. + assertSame(resources1.getImpl(), resources2.getImpl()); + + final Configuration overrideConfig = new Configuration(); + overrideConfig.orientation = Configuration.ORIENTATION_LANDSCAPE; + Resources resources3 = mResourcesManager.getResources( + activity2, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, + overrideConfig, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + + // Since we requested new resources for activity2, the resource should be the same + // as the one returned before for activity2. + assertSame(resources2, resources3); + + // But the implementation has changed. + assertNotSame(resources1.getImpl(), resources2.getImpl()); + } + + @SmallTest + public void testThemesGetUpdatedWithNewImpl() { + Binder activity1 = new Binder(); + Resources resources1 = mResourcesManager.getResources( + activity1, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(resources1); + + Resources.Theme theme = resources1.newTheme(); + assertSame(resources1, theme.getResources()); + theme.applyStyle(android.R.style.Theme_NoTitleBar, false); + + TypedValue value = new TypedValue(); + assertTrue(theme.resolveAttribute(android.R.attr.windowNoTitle, value, true)); + assertEquals(TypedValue.TYPE_INT_BOOLEAN, value.type); + assertTrue(value.data != 0); + + final Configuration overrideConfig = new Configuration(); + overrideConfig.orientation = Configuration.ORIENTATION_LANDSCAPE; + Resources resources2 = mResourcesManager.getResources( + activity1, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, + overrideConfig, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); + assertNotNull(resources2); + assertSame(resources1, resources2); + assertSame(resources2, theme.getResources()); + + // Make sure we can still access the data. + assertTrue(theme.resolveAttribute(android.R.attr.windowNoTitle, value, true)); + assertEquals(TypedValue.TYPE_INT_BOOLEAN, value.type); + assertTrue(value.data != 0); + } +} diff --git a/core/tests/coretests/src/com/android/internal/util/ArrayUtilsTest.java b/core/tests/coretests/src/com/android/internal/util/ArrayUtilsTest.java new file mode 100644 index 0000000000000..b3897ceb6e794 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/util/ArrayUtilsTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.internal.util; + +import android.test.suitebuilder.annotation.SmallTest; +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +public class ArrayUtilsTest extends TestCase { + + @SmallTest + public void testUnstableRemoveIf() throws Exception { + java.util.function.Predicate isNull = new java.util.function.Predicate() { + @Override + public boolean test(Object o) { + return o == null; + } + }; + + final Object a = new Object(); + final Object b = new Object(); + final Object c = new Object(); + + ArrayList collection = null; + assertEquals(0, ArrayUtils.unstableRemoveIf(collection, isNull)); + + collection = new ArrayList<>(); + assertEquals(0, ArrayUtils.unstableRemoveIf(collection, isNull)); + + collection = new ArrayList<>(Collections.singletonList(a)); + assertEquals(0, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(1, collection.size()); + assertTrue(collection.contains(a)); + + collection = new ArrayList<>(Collections.singletonList(null)); + assertEquals(1, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(0, collection.size()); + + collection = new ArrayList<>(Arrays.asList(a, b)); + assertEquals(0, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(2, collection.size()); + assertTrue(collection.contains(a)); + assertTrue(collection.contains(b)); + + collection = new ArrayList<>(Arrays.asList(a, null)); + assertEquals(1, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(1, collection.size()); + assertTrue(collection.contains(a)); + + collection = new ArrayList<>(Arrays.asList(null, a)); + assertEquals(1, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(1, collection.size()); + assertTrue(collection.contains(a)); + + collection = new ArrayList<>(Arrays.asList(null, null)); + assertEquals(2, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(0, collection.size()); + + collection = new ArrayList<>(Arrays.asList(a, b, c)); + assertEquals(0, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(3, collection.size()); + assertTrue(collection.contains(a)); + assertTrue(collection.contains(b)); + assertTrue(collection.contains(c)); + + collection = new ArrayList<>(Arrays.asList(a, b, null)); + assertEquals(1, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(2, collection.size()); + assertTrue(collection.contains(a)); + assertTrue(collection.contains(b)); + + collection = new ArrayList<>(Arrays.asList(a, null, b)); + assertEquals(1, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(2, collection.size()); + assertTrue(collection.contains(a)); + assertTrue(collection.contains(b)); + + collection = new ArrayList<>(Arrays.asList(null, a, b)); + assertEquals(1, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(2, collection.size()); + assertTrue(collection.contains(a)); + assertTrue(collection.contains(b)); + + collection = new ArrayList<>(Arrays.asList(a, null, null)); + assertEquals(2, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(1, collection.size()); + assertTrue(collection.contains(a)); + + collection = new ArrayList<>(Arrays.asList(null, null, a)); + assertEquals(2, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(1, collection.size()); + assertTrue(collection.contains(a)); + + collection = new ArrayList<>(Arrays.asList(null, a, null)); + assertEquals(2, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(1, collection.size()); + assertTrue(collection.contains(a)); + + collection = new ArrayList<>(Arrays.asList(null, null, null)); + assertEquals(3, ArrayUtils.unstableRemoveIf(collection, isNull)); + assertEquals(0, collection.size()); + } +}