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
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index d8bc119..060ac5e 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -1793,24 +1793,9 @@
* 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 @@
}
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 @@
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 @@
}
}
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 @@
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 @@
// 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 @@
}
}
- 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 @@
mCompatConfiguration = new Configuration();
}
mCompatConfiguration.setTo(mConfiguration);
- if (mResourcesManager.applyCompatConfiguration(displayDensity, mCompatConfiguration)) {
+ if (mResourcesManager.applyCompatConfigurationLocked(displayDensity,
+ mCompatConfiguration)) {
config = mCompatConfiguration;
}
return config;
@@ -4661,7 +4684,8 @@
if (callbacks != null) {
final int N = callbacks.size();
for (int i=0; i<N; i++) {
- performConfigurationChanged(callbacks.get(i), config, REPORT_TO_ACTIVITY);
+ performConfigurationChanged(callbacks.get(i), null, config, null,
+ REPORT_TO_ACTIVITY);
}
}
}
@@ -4687,15 +4711,8 @@
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle activity config changed: "
+ r.activityInfo.name + ", with callback=" + reportToActivity);
- r.tmpConfig.setTo(mCompatConfiguration);
- if (data.overrideConfig != null) {
- r.overrideConfig = data.overrideConfig;
- r.tmpConfig.updateFrom(data.overrideConfig);
- }
- performConfigurationChanged(r.activity, r.tmpConfig, reportToActivity);
-
- freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(mCompatConfiguration));
-
+ r.overrideConfig = data.overrideConfig;
+ performConfigurationChangedForActivity(r, mCompatConfiguration, reportToActivity);
mSomeActivitiesChanged = true;
}
@@ -5032,21 +5049,25 @@
*/
TimeZone.setDefault(null);
- /*
- * Initialize the default locales in this process for the reasons we set the time zone.
- *
- * We do this through ResourcesManager, since we need to do locale negotiation.
- */
- mResourcesManager.setDefaultLocalesLocked(data.config.getLocales());
+ synchronized (mResourcesManager) {
+ /*
+ * Initialize the default locales in this process for the reasons we set the time zone.
+ *
+ * We do this through ResourcesManager, since we need to do locale negotiation.
+ */
+ mResourcesManager.setDefaultLocalesLocked(data.config.getLocales());
- /*
- * Update the system configuration since its preloaded and might not
- * reflect configuration changes. The configuration object passed
- * in AppBindData can be safely assumed to be up to date
- */
- mResourcesManager.applyConfigurationToResourcesLocked(data.config, data.compatInfo);
- mCurDefaultDisplayDpi = data.config.densityDpi;
- applyCompatConfiguration(mCurDefaultDisplayDpi);
+ /*
+ * Update the system configuration since its preloaded and might not
+ * reflect configuration changes. The configuration object passed
+ * in AppBindData can be safely assumed to be up to date
+ */
+ mResourcesManager.applyConfigurationToResourcesLocked(data.config, data.compatInfo);
+ mCurDefaultDisplayDpi = data.config.densityDpi;
+
+ // This calls mResourcesManager so keep it within the synchronized block.
+ applyCompatConfiguration(mCurDefaultDisplayDpi);
+ }
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 0b44925..ca05091 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -1182,7 +1182,7 @@
sameUid ? app.sourceDir : app.publicSourceDir,
sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs,
app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
- null, mContext.mPackageInfo);
+ mContext.mPackageInfo);
if (r != null) {
return r;
}
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index e76f991..32ace14 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1994,9 +1994,10 @@
}
static ContextImpl createActivityContext(ActivityThread mainThread,
- LoadedApk packageInfo, int displayId, Configuration overrideConfiguration) {
+ LoadedApk packageInfo, IBinder activityToken, int displayId,
+ Configuration overrideConfiguration) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
- return new ContextImpl(null, mainThread, packageInfo, null, null, 0,
+ return new ContextImpl(null, mainThread, packageInfo, activityToken, null, 0,
null, overrideConfiguration, displayId);
}
@@ -2054,10 +2055,16 @@
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
- resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
- packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
- packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
- overrideConfiguration, compatInfo, packageInfo.getClassLoader());
+ resources = mResourcesManager.getResources(
+ activityToken,
+ packageInfo.getResDir(),
+ packageInfo.getSplitResDirs(),
+ packageInfo.getOverlayDirs(),
+ packageInfo.getApplicationInfo().sharedLibraryFiles,
+ displayId,
+ overrideConfiguration,
+ compatInfo,
+ packageInfo.getClassLoader());
}
}
mResources = resources;
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index cd17078..b8fc323 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -283,9 +283,9 @@
synchronized (this) {
mClassLoader = createOrUpdateClassLoaderLocked(addedPaths);
if (mResources != null) {
- mResources = mActivityThread.getNewTopLevelResources(mResDir, mSplitResDirs,
+ mResources = mActivityThread.getTopLevelResources(mResDir, mSplitResDirs,
mOverlayDirs, mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
- null /*overrideConfiguration*/, this);
+ this);
}
}
}
@@ -668,7 +668,7 @@
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
- mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
+ mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
}
return mResources;
}
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index bc44553d..54d813d 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -18,6 +18,8 @@
import static android.app.ActivityThread.DEBUG_CONFIGURATION;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.pm.ActivityInfo;
import android.content.res.AssetManager;
import android.content.res.CompatibilityInfo;
@@ -26,6 +28,7 @@
import android.content.res.ResourcesImpl;
import android.content.res.ResourcesKey;
import android.hardware.display.DisplayManagerGlobal;
+import android.os.IBinder;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.util.LocaleList;
@@ -34,11 +37,16 @@
import android.util.Slog;
import android.view.Display;
import android.view.DisplayAdjustments;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
import java.lang.ref.WeakReference;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
-import java.util.List;
+import java.util.Objects;
+import java.util.WeakHashMap;
+import java.util.function.Predicate;
/** @hide */
public class ResourcesManager {
@@ -46,18 +54,56 @@
private static final boolean DEBUG = false;
private static ResourcesManager sResourcesManager;
- private final ArrayMap<ResourcesKey, WeakReference<Resources>> mActiveResources =
- new ArrayMap<>();
- private final ArrayMap<Pair<Integer, DisplayAdjustments>, WeakReference<Display>> mDisplays =
- new ArrayMap<>();
+
+ /**
+ * Predicate that returns true if a WeakReference is gc'ed.
+ */
+ private static final Predicate<WeakReference<Resources>> sEmptyReferencePredicate =
+ new Predicate<WeakReference<Resources>>() {
+ @Override
+ public boolean test(WeakReference<Resources> weakRef) {
+ return weakRef == null || weakRef.get() == null;
+ }
+ };
private String[] mSystemLocales = {};
- private final HashSet<String> mNonSystemLocales = new HashSet<String>();
+ private final HashSet<String> 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<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls =
+ new ArrayMap<>();
+
+ /**
+ * A list of Resource references that can be reused.
+ */
+ private final ArrayList<WeakReference<Resources>> mResourceReferences = new ArrayList<>();
+
+ /**
+ * Each Activity may have only one Resources object.
+ */
+ private final WeakHashMap<IBinder, WeakReference<Resources>> mActivityResourceReferences =
+ new WeakHashMap<>();
+
+ /**
+ * A cache of DisplayId to DisplayAdjustments.
+ */
+ private final ArrayMap<Pair<Integer, DisplayAdjustments>, WeakReference<Display>> mDisplays =
+ new ArrayMap<>();
public static ResourcesManager getInstance() {
synchronized (ResourcesManager.class) {
@@ -76,7 +122,11 @@
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 @@
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 @@
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 @@
* @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<Integer, DisplayAdjustments> key =
@@ -153,76 +204,42 @@
}
/**
- * 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<Resources> 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 @@
}
}
}
+ 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 @@
} 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<ResourcesImpl> 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<ResourcesImpl> 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<Resources> 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<Resources> 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.
+ * <p/>
+ * 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 @@
LocaleList.isPseudoLocalesOnly(nonSystemLocales);
synchronized (this) {
- WeakReference<Resources> 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<Resources> 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 @@
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 @@
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 @@
tmpConfig.setTo(localeAdjustedConfig);
if (!isDefaultDisplay) {
dm = getDisplayMetricsLocked(displayId);
- applyNonDefaultDisplayMetricsToConfigurationLocked(dm, tmpConfig);
+ applyNonDefaultDisplayMetricsToConfiguration(dm, tmpConfig);
}
if (hasOverrideConfiguration) {
tmpConfig.updateFrom(key.mOverrideConfiguration);
@@ -398,11 +643,10 @@
// + " " + 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 f337fe6..fb706fc 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -54,6 +54,7 @@
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 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
@@ -113,6 +116,13 @@
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<WeakReference<Theme>> mThemeRefs = new ArrayList<>();
+
+ /**
* Returns the most appropriate default theme for the specified target SDK version.
* <ul>
* <li>Below API 11: Gingerbread
@@ -231,10 +241,42 @@
}
/**
+ * 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<Theme> 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 final Theme newTheme() {
Theme theme = new Theme();
theme.setImpl(mResourcesImpl.newThemeImpl());
+ mThemeRefs.add(new WeakReference<>(theme));
return theme;
}
@@ -1785,6 +1828,7 @@
* This is just for testing.
* @hide
*/
+ @VisibleForTesting
public void setCompatibilityInfo(CompatibilityInfo ci) {
if (ci != null) {
mResourcesImpl.updateConfiguration(null, null, ci);
@@ -2069,6 +2113,15 @@
}
/**
+ * 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 2ffd372..0858cb8 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -48,6 +48,13 @@
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 @@
* @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 @@
}
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 @@
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 @@
}
/**
- * 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 @@
@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 @@
}
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 @@
}
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 @@
* 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 @@
* 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<ComplexColor> cache = mComplexColorCache;
ComplexColor complexColor = cache.getInstance(key, wrapper, theme);
@@ -714,7 +721,7 @@
@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 @@
@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 @@
*/
@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 @@
* @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 @@
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 @@
@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 @@
@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 2620571..e894492 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 @@
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 @@
@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 a84a061..ee73b90 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -27,6 +27,7 @@
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 @@
}
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 <T> int unstableRemoveIf(@Nullable ArrayList<T> collection,
+ @NonNull java.util.function.Predicate<T> 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 5d46489..47554a6 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<ResourceCacheActivity> {
ConfigurationBoundResourceCache<Float> mCache;
- Method mCalcConfigChanges;
-
public ConfigurationBoundResourceCacheTest() {
super(ResourceCacheActivity.class);
}
@@ -41,33 +39,42 @@
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 @@
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 @@
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 @@
Configuration.ORIENTATION_PORTRAIT
: Configuration.ORIENTATION_LANDSCAPE;
int changes = calcConfigChanges(res, newCnf);
- assertEquals(staticDim, mCache.get(R.dimen.resource_cache_test_generic,
+ assertEquals(staticDim, mCache.getInstance(R.dimen.resource_cache_test_generic, res,
getActivity().getTheme()));
- assertEquals(changingDim, mCache.get(R.dimen.resource_cache_test_orientation_dependent,
- 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 @@
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<Float> {
+ static class DummyFloatConstantState extends ConstantState<Float> {
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 0000000..3cadbf6
--- /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 0000000..b3897ce
--- /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<Object> isNull = new java.util.function.Predicate<Object>() {
+ @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<Object> 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());
+ }
+}