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
This commit is contained in:
@@ -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<N; i++) {
|
||||
performConfigurationChanged(callbacks.get(i), config, REPORT_TO_ACTIVITY);
|
||||
performConfigurationChanged(callbacks.get(i), null, config, null,
|
||||
REPORT_TO_ACTIVITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4687,15 +4711,8 @@ public final class ActivityThread {
|
||||
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 @@ public final class ActivityThread {
|
||||
*/
|
||||
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);
|
||||
|
||||
|
||||
@@ -1182,7 +1182,7 @@ public class ApplicationPackageManager extends PackageManager {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1994,9 +1994,10 @@ class ContextImpl extends Context {
|
||||
}
|
||||
|
||||
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 @@ class ContextImpl extends Context {
|
||||
|| 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;
|
||||
|
||||
@@ -283,9 +283,9 @@ public final class LoadedApk {
|
||||
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 final class LoadedApk {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ package android.app;
|
||||
|
||||
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.Resources;
|
||||
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.Pair;
|
||||
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 @@ public class ResourcesManager {
|
||||
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 @@ 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<Integer, DisplayAdjustments> 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<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 @@ 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<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 @@ public class ResourcesManager {
|
||||
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 @@ 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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<WeakReference<Theme>> mThemeRefs = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Returns the most appropriate default theme for the specified target SDK version.
|
||||
* <ul>
|
||||
@@ -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<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 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.
|
||||
|
||||
@@ -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<ComplexColor> 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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@ 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<Float> {
|
||||
static class DummyFloatConstantState extends ConstantState<Float> {
|
||||
|
||||
final Float mObj;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user