Overriding resources at runtime
Solution 1:
While "dynamically overriding resources" might seem the straightforward solution to your problem, I believe a cleaner approach would be to use the official data binding implementation https://developer.android.com/tools/data-binding/guide.html since it doesn't imply hacking the android way.
You could pass your branding settings using a POJO. Instead of using static styles like @color/button_color
you could write @{brandingConfig.buttonColor}
and bind your views with the desired values. With a proper activity hierarchy, it shouldn't add too much boilerplate.
This also gives you the ability to change more complex elements on your layout, i.e.: including different layouts on other layout depending on the branding settings, making your UI highly configurable without too much effort.
Solution 2:
Having basically the same issue as Luke Sleeman, I had a look at how the LayoutInflater
is creating the views when parsing the XML layout files. I focused on checking why the string resources assigned to the text attribute of TextView
s inside the layout are not overwritten by my Resources
object returned by a custom ContextWrapper
. At the same time, the strings are overwritten as expected when setting the text or hint programatically through TextView.setText()
or TextView.setHint()
.
This is how the text is received as a CharSequence
inside the constructor of the TextView
(sdk v 23.0.1):
// android.widget.TextView.java, line 973
text = a.getText(attr);
where a
is a TypedArray
obtained earlier:
// android.widget.TextView.java, line 721
a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
The Theme.obtainStyledAttributes()
method calls a native method on the AssetManager
:
// android.content.res.Resources.java line 1593
public TypedArray obtainStyledAttributes(AttributeSet set,
@StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
...
AssetManager.applyStyle(mTheme, defStyleAttr, defStyleRes,
parser != null ? parser.mParseState : 0, attrs, array.mData, array.mIndices);
...
And this is the declaration of the AssetManager.applyStyle()
method:
// android.content.res.AssetManager.java, line 746
/*package*/ native static final boolean applyStyle(long theme,
int defStyleAttr, int defStyleRes, long xmlParser,
int[] inAttrs, int[] outValues, int[] outIndices);
In conclusion, even though the LayoutInflater
is using the correct extended context, when inflating the XML layouts and creating the views, the methods Resources.getText()
(on the resources returned by the custom ContextWrapper
) are never called to get the strings for the text attribute, because the constructor of the TextView
is using the AssetManager
directly to load the resources for the attributes. The same might be valid for other views and attributes.
Solution 3:
After searching for a quite long time I finally found an excellent solution.
protected void redefineStringResourceId(final String resourceName, final int newId) {
try {
final Field field = R.string.class.getDeclaredField(resourceName);
field.setAccessible(true);
field.set(null, newId);
} catch (Exception e) {
Log.e(getClass().getName(), "Couldn't redefine resource id", e);
}
}
For a sample test,
private Object initialStringValue() {
// TODO Auto-generated method stub
return getString(R.string.initial_value);
}
And inside the Main activity,
before.setText(getString(R.string.before, initialStringValue()));
final String resourceName = getResources().getResourceEntryName(R.string.initial_value);
redefineStringResourceId(resourceName, R.string.evil_value);
after.setText(getString(R.string.after, initialStringValue()));
This solution was originally posted by, Roman Zhilich
ResourceHackActivity