From 756220bd1912535840388a6743830d2e59ad4964 Mon Sep 17 00:00:00 2001 From: Dianne Hackborn Date: Tue, 14 Aug 2012 16:45:30 -0700 Subject: [PATCH] Add API to create new contexts with custom configurations. This allows you to, say, make a Context whose configuration is set to a different density than the actual density of the device. The main API is Context.createConfigurationContext(). There is also a new API on ContextThemeWrapper that allows you to apply an override context before its resources are retrieved, which addresses some feature requests from developers to be able to customize the context their app is running in. Change-Id: I88364986660088521e24b567e2fda22fb7042819 --- api/current.txt | 4 + core/java/android/app/ActivityThread.java | 81 +++++++++++++++---- .../app/ApplicationPackageManager.java | 2 +- core/java/android/app/ContextImpl.java | 22 +++-- core/java/android/app/LoadedApk.java | 2 +- core/java/android/content/Context.java | 18 +++++ core/java/android/content/ContextWrapper.java | 6 ++ .../android/content/res/Configuration.java | 3 + core/java/android/content/res/Resources.java | 11 ++- .../android/view/ContextThemeWrapper.java | 38 +++++++++ .../src/android/test/mock/MockContext.java | 6 ++ .../test/activity/ActivityTestMain.java | 33 ++++++++ .../bridge/android/BridgeContext.java | 6 ++ 13 files changed, 203 insertions(+), 29 deletions(-) diff --git a/api/current.txt b/api/current.txt index da3dd3edc8b97..8dbf9fbbdfd75 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5262,6 +5262,7 @@ package android.content { method public abstract int checkUriPermission(android.net.Uri, int, int, int); method public abstract int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int); method public abstract deprecated void clearWallpaper() throws java.io.IOException; + method public abstract android.content.Context createConfigurationContext(android.content.res.Configuration); method public abstract android.content.Context createPackageContext(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; method public abstract java.lang.String[] databaseList(); method public abstract boolean deleteDatabase(java.lang.String); @@ -5406,6 +5407,7 @@ package android.content { method public int checkUriPermission(android.net.Uri, int, int, int); method public int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int); method public void clearWallpaper() throws java.io.IOException; + method public android.content.Context createConfigurationContext(android.content.res.Configuration); method public android.content.Context createPackageContext(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; method public java.lang.String[] databaseList(); method public boolean deleteDatabase(java.lang.String); @@ -21147,6 +21149,7 @@ package android.test.mock { method public int checkUriPermission(android.net.Uri, int, int, int); method public int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int); method public void clearWallpaper(); + method public android.content.Context createConfigurationContext(android.content.res.Configuration); method public android.content.Context createPackageContext(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; method public java.lang.String[] databaseList(); method public boolean deleteDatabase(java.lang.String); @@ -23351,6 +23354,7 @@ package android.view { public class ContextThemeWrapper extends android.content.ContextWrapper { ctor public ContextThemeWrapper(); ctor public ContextThemeWrapper(android.content.Context, int); + method public void applyOverrideConfiguration(android.content.res.Configuration); method protected void onApplyThemeResource(android.content.res.Resources.Theme, int, boolean); } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index bb35ddd33d511..0789c60ffa3d8 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1471,13 +1471,25 @@ public final class ActivityThread { private static class ResourcesKey { final private String mResDir; + final private Configuration mOverrideConfiguration; final private float mScale; final private int mHash; - ResourcesKey(String resDir, float scale) { + ResourcesKey(String resDir, Configuration overrideConfiguration, float scale) { mResDir = resDir; + if (overrideConfiguration != null) { + if (Configuration.EMPTY.equals(overrideConfiguration)) { + overrideConfiguration = null; + } + } + mOverrideConfiguration = overrideConfiguration; mScale = scale; - mHash = mResDir.hashCode() << 2 + (int) (mScale * 2); + int hash = 17; + hash = 31 * hash + mResDir.hashCode(); + hash = 31 * hash + (mOverrideConfiguration != null + ? mOverrideConfiguration.hashCode() : 0); + hash = 31 * hash + Float.floatToIntBits(mScale); + mHash = hash; } @Override @@ -1491,7 +1503,21 @@ public final class ActivityThread { return false; } ResourcesKey peer = (ResourcesKey) obj; - return mResDir.equals(peer.mResDir) && mScale == peer.mScale; + if (!mResDir.equals(peer.mResDir)) { + return false; + } + if (mOverrideConfiguration != peer.mOverrideConfiguration) { + if (mOverrideConfiguration == null || peer.mOverrideConfiguration == null) { + return false; + } + if (!mOverrideConfiguration.equals(peer.mOverrideConfiguration)) { + return false; + } + } + if (mScale != peer.mScale) { + return false; + } + return true; } } @@ -1562,8 +1588,10 @@ public final class ActivityThread { * @param compInfo the compability info. It will use the default compatibility info when it's * null. */ - Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) { - ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale); + Resources getTopLevelResources(String resDir, Configuration overrideConfiguration, + CompatibilityInfo compInfo) { + ResourcesKey key = new ResourcesKey(resDir, overrideConfiguration, + compInfo.applicationScale); Resources r; synchronized (mPackages) { // Resources is app scale dependent. @@ -1595,13 +1623,20 @@ public final class ActivityThread { //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics); DisplayMetrics metrics = getDisplayMetricsLocked(null, false); - r = new Resources(assets, metrics, getConfiguration(), compInfo); + Configuration config; + if (key.mOverrideConfiguration != null) { + config = new Configuration(getConfiguration()); + config.updateFrom(key.mOverrideConfiguration); + } else { + config = getConfiguration(); + } + r = new Resources(assets, metrics, config, compInfo); if (false) { Slog.i(TAG, "Created app resources " + resDir + " " + r + ": " + r.getConfiguration() + " appScale=" + r.getCompatibilityInfo().applicationScale); } - + synchronized (mPackages) { WeakReference wr = mActiveResources.get(key); Resources existing = wr != null ? wr.get() : null; @@ -1621,8 +1656,10 @@ public final class ActivityThread { /** * Creates the top level resources for the given package. */ - Resources getTopLevelResources(String resDir, LoadedApk pkgInfo) { - return getTopLevelResources(resDir, pkgInfo.mCompatibilityInfo.get()); + Resources getTopLevelResources(String resDir, Configuration overrideConfiguration, + LoadedApk pkgInfo) { + return getTopLevelResources(resDir, overrideConfiguration, + pkgInfo.mCompatibilityInfo.get()); } final Handler getHandler() { @@ -3675,18 +3712,28 @@ public final class ActivityThread { ApplicationPackageManager.configurationChanged(); //Slog.i(TAG, "Configuration changed in " + currentPackageName()); - - Iterator> it = - mActiveResources.values().iterator(); - //Iterator>> it = - // mActiveResources.entrySet().iterator(); + + Configuration tmpConfig = null; + + Iterator>> it = + mActiveResources.entrySet().iterator(); while (it.hasNext()) { - WeakReference v = it.next(); - Resources r = v.get(); + Map.Entry> entry = it.next(); + Resources r = entry.getValue().get(); if (r != null) { if (DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources " + r + " config to: " + config); - r.updateConfiguration(config, dm, compat); + Configuration override = entry.getKey().mOverrideConfiguration; + if (override != null) { + if (tmpConfig == null) { + tmpConfig = new Configuration(); + } + tmpConfig.setTo(config); + tmpConfig.updateFrom(override); + r.updateConfiguration(tmpConfig, dm, compat); + } else { + r.updateConfiguration(config, dm, compat); + } //Slog.i(TAG, "Updated app resources " + v.getKey() // + " " + r + ": " + r.getConfiguration()); } else { diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 2face4c22c2c3..9b59e2ce1017f 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -713,7 +713,7 @@ final class ApplicationPackageManager extends PackageManager { } Resources r = mContext.mMainThread.getTopLevelResources( app.uid == Process.myUid() ? app.sourceDir - : app.publicSourceDir, mContext.mPackageInfo); + : app.publicSourceDir, null, mContext.mPackageInfo); if (r != null) { return r; } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 4496ce81484a5..1ef14ebaec2ba 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -37,6 +37,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.CompatibilityInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; @@ -525,7 +526,7 @@ class ContextImpl extends Context { @Override public AssetManager getAssets() { - return mResources.getAssets(); + return getResources().getAssets(); } @Override @@ -1590,6 +1591,16 @@ class ContextImpl extends Context { "Application package " + packageName + " not found"); } + @Override + public Context createConfigurationContext(Configuration overrideConfiguration) { + ContextImpl c = new ContextImpl(); + c.init(mPackageInfo, null, mMainThread); + c.mResources = mMainThread.getTopLevelResources( + mPackageInfo.getResDir(), overrideConfiguration, + mResources.getCompatibilityInfo()); + return c; + } + @Override public boolean isRestricted() { return mRestricted; @@ -1659,12 +1670,11 @@ class ContextImpl extends Context { " compatiblity info:" + container.getDisplayMetrics()); } mResources = mainThread.getTopLevelResources( - mPackageInfo.getResDir(), container.getCompatibilityInfo()); + mPackageInfo.getResDir(), null, container.getCompatibilityInfo()); } mMainThread = mainThread; mContentResolver = new ApplicationContentResolver(this, mainThread); - - setActivityToken(activityToken); + mActivityToken = activityToken; } final void init(Resources resources, ActivityThread mainThread) { @@ -1691,10 +1701,6 @@ class ContextImpl extends Context { return mReceiverRestrictedContext = new ReceiverRestrictedContext(getOuterContext()); } - final void setActivityToken(IBinder token) { - mActivityToken = token; - } - final void setOuterContext(Context context) { mOuterContext = context; } diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index be4b2844c0b1d..f4195d68794a3 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -471,7 +471,7 @@ public final class LoadedApk { public Resources getResources(ActivityThread mainThread) { if (mResources == null) { - mResources = mainThread.getTopLevelResources(mResDir, this); + mResources = mainThread.getTopLevelResources(mResDir, null, this); } return mResources; } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index bf60a96ea0713..a90142ae2a483 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -19,6 +19,7 @@ package android.content; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.database.DatabaseErrorHandler; @@ -2444,6 +2445,23 @@ public abstract class Context { public abstract Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException; + /** + * Return a new Context object for the current Context but whose resources + * are adjusted to match the given Configuration. Each call to this method + * returns a new instance of a Contex object; Context objects are not + * shared, however common state (ClassLoader, other Resources for the + * same configuration) may be so the Context itself can be fairly lightweight. + * + * @param overrideConfiguration A {@link Configuration} specifying what + * values to modify in the base Configuration of the original Context's + * resources. If the base configuration changes (such as due to an + * orientation change), the resources of this context will also change except + * for those that have been explicitly overridden with a value here. + * + * @return A Context for the application. + */ + public abstract Context createConfigurationContext(Configuration overrideConfiguration); + /** * Indicates whether this Context is restricted. * diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index ff4c9a1001230..fdf60ab0517f1 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -19,6 +19,7 @@ package android.content; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; +import android.content.res.Configuration; import android.content.res.Resources; import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; @@ -532,6 +533,11 @@ public class ContextWrapper extends Context { return mBase.createPackageContext(packageName, flags); } + @Override + public Context createConfigurationContext(Configuration overrideConfiguration) { + return mBase.createConfigurationContext(overrideConfiguration); + } + @Override public boolean isRestricted() { return mBase.isRestricted(); diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index ea13a2a141abe..52b6498f80e5f 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -35,6 +35,9 @@ import java.util.Locale; *
Configuration config = getResources().getConfiguration();
*/ public final class Configuration implements Parcelable, Comparable { + /** @hide */ + public static final Configuration EMPTY = new Configuration(); + /** * Current user preference for the scaling factor for fonts, relative * to the base density scaling. diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index d2af3e90f33b7..26512e2f1b259 100755 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -1435,9 +1435,12 @@ public class Resources { int configChanges = 0xfffffff; if (config != null) { mTmpConfig.setTo(config); + int density = config.densityDpi; + if (density == Configuration.DENSITY_DPI_UNDEFINED) { + density = mMetrics.noncompatDensityDpi; + } if (mCompatibilityInfo != null) { - mCompatibilityInfo.applyToConfiguration(mMetrics.noncompatDensityDpi, - mTmpConfig); + mCompatibilityInfo.applyToConfiguration(density, mTmpConfig); } if (mTmpConfig.locale == null) { mTmpConfig.locale = Locale.getDefault(); @@ -1448,6 +1451,10 @@ public class Resources { if (mConfiguration.locale == null) { mConfiguration.locale = Locale.getDefault(); } + if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) { + mMetrics.densityDpi = mConfiguration.densityDpi; + mMetrics.density = mConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; + } mMetrics.scaledDensity = mMetrics.density * mConfiguration.fontScale; String locale = null; diff --git a/core/java/android/view/ContextThemeWrapper.java b/core/java/android/view/ContextThemeWrapper.java index 626f385551f76..6c733f9e11a88 100644 --- a/core/java/android/view/ContextThemeWrapper.java +++ b/core/java/android/view/ContextThemeWrapper.java @@ -18,6 +18,7 @@ package android.view; import android.content.Context; import android.content.ContextWrapper; +import android.content.res.Configuration; import android.content.res.Resources; import android.os.Build; @@ -30,6 +31,8 @@ public class ContextThemeWrapper extends ContextWrapper { private int mThemeResource; private Resources.Theme mTheme; private LayoutInflater mInflater; + private Configuration mOverrideConfiguration; + private Resources mResources; public ContextThemeWrapper() { super(null); @@ -45,6 +48,41 @@ public class ContextThemeWrapper extends ContextWrapper { super.attachBaseContext(newBase); mBase = newBase; } + + /** + * Call to set an "override configuration" on this context -- this is + * a configuration that replies one or more values of the standard + * configuration that is applied to the context. See + * {@link Context#createConfigurationContext(Configuration)} for more + * information. + * + *

This method can only be called once, and must be called before any + * calls to {@link #getResources()} are made. + */ + public void applyOverrideConfiguration(Configuration overrideConfiguration) { + if (mResources != null) { + throw new IllegalStateException("getResources() has already been called"); + } + if (mOverrideConfiguration != null) { + throw new IllegalStateException("Override configuration has already been set"); + } + mOverrideConfiguration = new Configuration(overrideConfiguration); + } + + @Override + public Resources getResources() { + if (mResources != null) { + return mResources; + } + if (mOverrideConfiguration == null) { + mResources = super.getResources(); + return mResources; + } else { + Context resc = createConfigurationContext(mOverrideConfiguration); + mResources = resc.getResources(); + return mResources; + } + } @Override public void setTheme(int resid) { mThemeResource = resid; diff --git a/test-runner/src/android/test/mock/MockContext.java b/test-runner/src/android/test/mock/MockContext.java index 9acffa30ba999..36f2c14924bdf 100644 --- a/test-runner/src/android/test/mock/MockContext.java +++ b/test-runner/src/android/test/mock/MockContext.java @@ -28,6 +28,7 @@ import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; +import android.content.res.Configuration; import android.content.res.Resources; import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; @@ -476,6 +477,11 @@ public class MockContext extends Context { throw new UnsupportedOperationException(); } + @Override + public Context createConfigurationContext(Configuration overrideConfiguration) { + throw new UnsupportedOperationException(); + } + @Override public boolean isRestricted() { throw new UnsupportedOperationException(); diff --git a/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java b/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java index 0ec1f137c6f81..0577dbba901e0 100644 --- a/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java +++ b/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java @@ -40,12 +40,16 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.content.Context; +import android.content.res.Configuration; import android.util.Log; public class ActivityTestMain extends Activity { static final String TAG = "ActivityTest"; + static final String KEY_CONFIGURATION = "configuration"; + ActivityManager mAm; + Configuration mOverrideConfig; class BroadcastResultReceiver extends BroadcastReceiver { @Override @@ -111,6 +115,12 @@ public class ActivityTestMain extends Activity { super.onCreate(savedInstanceState); mAm = (ActivityManager)getSystemService(ACTIVITY_SERVICE); + if (savedInstanceState != null) { + mOverrideConfig = savedInstanceState.getParcelable(KEY_CONFIGURATION); + if (mOverrideConfig != null) { + applyOverrideConfiguration(mOverrideConfig); + } + } } @Override @@ -182,6 +192,21 @@ public class ActivityTestMain extends Activity { return true; } }); + menu.add("Density!").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override public boolean onMenuItemClick(MenuItem item) { + if (mOverrideConfig == null) { + mOverrideConfig = new Configuration(); + } + if (mOverrideConfig.densityDpi == Configuration.DENSITY_DPI_UNDEFINED) { + mOverrideConfig.densityDpi = (getApplicationContext().getResources() + .getConfiguration().densityDpi*2)/3; + } else { + mOverrideConfig.densityDpi = Configuration.DENSITY_DPI_UNDEFINED; + } + recreate(); + return true; + } + }); return true; } @@ -191,6 +216,14 @@ public class ActivityTestMain extends Activity { buildUi(); } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mOverrideConfig != null) { + outState.putParcelable(KEY_CONFIGURATION, mOverrideConfig); + } + } + private View scrollWrap(View view) { ScrollView scroller = new ScrollView(this); scroller.addView(view, new ScrollView.LayoutParams(ScrollView.LayoutParams.MATCH_PARENT, diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java index c4a69067aec0d..0a1191ba96d08 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -916,6 +916,12 @@ public final class BridgeContext extends Context { return null; } + @Override + public Context createConfigurationContext(Configuration overrideConfiguration) { + // pass + return null; + } + @Override public String[] databaseList() { // pass