I have a large Android codebase and I am writing a custom lint rule that checks whether the values of certain attributes fall within a given range.
For example, I have this component:
<MyCustomComponent
my:animation_factor="0.7"
...>
</MyCustomComponent>
and I want to write a lint rule that alerts developers that values of my:animation_factor >= 1 should be used with caution.
I followed the instructions at http://tools.android.com/tips/lint-custom-rules and managed to retrieve the value of my:animation_factor using this code:
import com.android.tools.lint.detector.api.*;
public class XmlInterpolatorFactorTooHighDetector {
....
#Override
public Collection<String> getApplicableElements() {
return ImmutableList.of("MyCustomComponent");
}
#Override
public void visitElement(XmlContext context, Element element) {
String factor = element.getAttribute("my:animation_factor");
...
if (value.startsWith("#dimen/")) {
// How do I resolve #dimen/xyz to 1.85?
} else {
String value = Float.parseFloat(factor);
}
}
}
This code works fine when attributes such as my:animation_factor have literal values (e.g. 0.7).
However, when the attribute value is a resources (e.g. #dimen/standard_anim_factor) then element.getAttribute(...) returns the string value of the attribute instead of the actual resolved value.
For example, when I have a MyCustomComponent that looks like this:
<MyCustomComponent
my:animation_factor="#dimen/standard_anim_factory"
...>
</MyCustomComponent>
and #dimen/standard_anim_factor is defined elsewhere:
<dimen name="standard_anim_factor">1.85</dimen>
then the string factor becomes "#dimen/standard_anim_factor" instead of "1.85".
Is there a way to resolve "#dimen/standard_anim_factor" to the actual value of resource (i.e. "1.85") while processing the MyCustomComponent element?
The general problem with the resolution of values is, that they depend on the Android runtime context you are in. There might be several values folders with different concrete values for your key #dimen/standard_anim_factory, so just that you are aware of.
Nevertheless, AFAIK there exist two options:
Perform a two phase detection:
Phase 1: Scan your resources
Scan for your attribute and put it in a list (instead of evaluating it immediately)
Scan your dimension values and put them in a list as well
Phase 2:
override Detector.afterProjectCheck and resolve your attributes by iterating over the two lists filled within phase 1
usually the LintUtils class [1] is a perfect spot for that stuff but unfortunately there is no method which resolves dimensions values. However, there is a method called getStyleAttributes which demonstrates how to resolve resource values. So you could write your own convenient method to resolve dimension values:
private int resolveDimensionValue(String name, Context context){
LintClient client = context.getDriver().getClient();
LintProject project = context.getDriver().getProject();
AbstractResourceRepository resources = client.getProjectResources(project, true);
return Integer.valueOf(resources.getResourceItem(ResourceType.DIMEN, name).get(0).getResourceValue(false).getValue());
}
Note: I haven't tested the above code yet. So please see it as theoretical advice :-)
https://android.googlesource.com/platform/tools/base/+/master/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/LintUtils.java
Just one more slight advice for your custom Lint rule code, since you are only interested in the attribute:
Instead of doing something like this in visitElement:
String factor = element.getAttribute("my:animation_factor");
...you may want to do something like this:
#Override
public Collection<String> getApplicableAttributes() {
return ImmutableList.of("my:animation_factor");
}
#Override
void visitAttribute(#NonNull XmlContext context, #NonNull Attr attribute){
...
}
But it's just a matter of preference :-)
I believe you're looking looking for getResources().getDimension().
Source: http://developer.android.com/reference/android/content/res/Resources.html#getDimension%28int%29
Assuming xml node after parsing your data, try the following
Element element = null; //It is your root node.
NamedNodeMap attrib = (NamedNodeMap) element;
int numAttrs = attrib.getLength ();
for (int i = 0; i < numAttrs; i++) {
Attr attr = (Attr) attrib.item (i);
String attrName = attr.getNodeName ();
String attrValue = attr.getNodeValue ();
System.out.println ("Found attribute: " + attrName + " with value: " + attrValue);
}
Related
I am working on finishing up a project, and it's my first time using Proguard. I have a method to set background colors for the activity itself as well as some buttons. Before Proguard everything works fine. After Proguard the colors aren't set.
Before:
public void setBackgroundColor(String color, View background){
String id = "1";
try {
ColorId myObject = new ColorId();
Method method = ColorId.class.getMethod(color);
id = (String) method.invoke(myObject);
} catch (Exception e) {
e.printStackTrace();
id = "1";
}
int thisColor = Integer.valueOf(id);
switch(thisColor) {
case 0://black
background.setBackgroundColor(0xff000000);
break;
case 1://white
background.setBackgroundColor(0xffffffff);
break;
case 2://red
background.setBackgroundColor(0xffCC0000);
break;
...
default:
background.setBackgroundColor(0xff0099cc);
break;
}
}
After:
public void a(String paramString, View paramView){
try {
c localc = new c();
str = (String)c.class.getMethod(paramString, new Class[0]).invoke(localc, new Object[0]);
switch (Integer.valueOf(str).intValue()){
default:
paramView.setBackgroundColor(-16737844);
return;
}
}catch (Exception localException){
for (;;){
localException.printStackTrace();
String str = "1";
}
paramView.setBackgroundColor(-16777216);
return;
}
paramView.setBackgroundColor(-1);
return;
paramView.setBackgroundColor(-3407872);
return;
paramView.setBackgroundColor(-16737844);
return;
paramView.setBackgroundColor(-8355712);
return;
paramView.setBackgroundColor(-6697984);
return;
paramView.setBackgroundColor(-17613);
return;
paramView.setBackgroundColor(-5609780);
return;
paramView.setBackgroundColor(-35700);
}
Can anyone help explain what is happening here, and how I can make this method (and others in the future) work again after obfuscation? To me it looks like Proguard is rearranging things in regards to the switch.
Proguard shortens code by renaming classes and methods to have shorter names and by removing code that isn't referred to. Your code doesn't work because Proguard renamed or removed the ColorId methods black(), white(), and red(). To use reflection, you'd need to add Proguard keep directives to tell it to keep these methods and to keep their original names.
I don't have an explanation for why the "after" code's switch statement is messed up. Are you sure you decompiled it properly?
Why is the "before" code so convoluted? It uses reflection to look up a method by color name, then calls it to translate the color name to a String, parses the String to get an integer code, boxes the integer code into an Integer, unboxes it, uses a switch statement to pick a color value, then sets the background color, replicating the background.setBackgroundColor() call in each of the switch branches (breaking the DRY principle).
Reflection is an extreme tool to use in special cases like dynamically loaded code.
It'd be simpler, faster, and clearer to look up the color name in a HashMap:
static final int DEFAULT_COLOR = 0xff0099cc;
static final Map<String, Integer> colors = new HashMap<String, Integer>();
static {
colors.put("black", 0xff000000);
colors.put("white", 0xffffffff);
colors.put("red", 0xffCC0000);
}
public void setBackgroundColor(String color, View view) {
Integer colorInteger = colors.get(color);
int colorValue = colorInteger == null ? DEFAULT_COLOR : colorInteger.intValue();
view.setBackgroundColor(colorValue);
}
This HashMap is a good choice if the color has to be passed in as a string name. But if you can change the color argument, an enum would be more type safe, simpler, and faster:
public enum Color {
BLACK(0xff000000), WHITE(0xffffffff), RED(0xffCC0000), DEFAULT(0xff0099cc);
final int value;
Color(int value) { this.value = value; }
}
public void setBackgroundColor(Color color, View view) {
view.setBackgroundColor(color.value);
}
[It's better to define all your color values in an Android resource file (colors.xml). You can look them up by resource ID number.]
What is your proguard setup? Post your proguard-project.txt file (or proguard.cfg if you're using the old method). I'd suggest turning off obfuscation to see more clearly how the code is being changed. Use '-dontobfuscate'.
The 'after' code looks odd. Are you using the optimization common configuration file (proguard-android-optimize.txt)? If so, try to use without optimization to reduce how much your code is being modified.
I'm attempting to get the int resource id for a layout resource by name, using Resources.GetIdentifier() of the Android API, but it returns 0. I'm using c#/monodroid/Xamarin, but regular java Android knowledge would apply too I suspect. Here's my code:
int resId = Resources.GetIdentifier(typeName, "layout", _activity.PackageName);
Where typeName = "FrmMain", and in my project I have the file "Resources/Layout/FrmMain.axml". Any ideas?
This is old, but for everyone getting this problem, I think it is because the resource name should be in lower case, so:
int resId = Resources.GetIdentifier("FrmMain", "layout", _activity.PackageName);
does not work, but:
int resId = Resources.GetIdentifier("frmmain", nameof(Resource.Layout).ToLower(), _activity.PackageName);
should work
I don't know why that's failing, but wouldn't something like Resource.Layout.FrmMain achieve what you're after?
edit:
According to this answer, you can (and should) use reflection to achieve what you're after, so I think you would try something like this:
var resourceId = (int)typeof(Resource.Layout).GetField(typeName).GetValue(null);
which does seem to work on my app and should get what you're after.
In my case, this issue came up when I had to upgrade the target SDK due to google's new policy since November, 2018.
I had to display some strings according to the server response code (ex : api_res_001_suc), but it did not work on the upgraded version.
The overall version, about 22 as I recall, had to be changed to 27.
The cause of the issue seems to be the default translation stuff. When I put all the default translation for every string, it worked.
My code is,
getResources().getIdentifier(resName, "string", "packageName");
I've created a ResourceHelper class to handle this situation. Here is the code:
public static class ResourceHelper
{
public static int FindId(string resourceId)
{
var type = typeof(Resource.Id);
var field = type.GetField(resourceId);
return (int)field.GetRawConstantValue();
}
public static int FindLayout(string layoutName)
{
var type = typeof(Resource.Layout);
var field = type.GetField(layoutName);
return (int)field.GetRawConstantValue();
}
public static int FindMenu(string menuName)
{
var type = typeof(Resource.Menu);
var field = type.GetField(menuName);
return (int)field.GetRawConstantValue();
}
}
Actually I'm improving it because I need to use it from another Assembly and it's restricted to work in the same Assembly of the Droid App. I'm thinking about put a generic method (or an Extension one) to do this. Here is a draft of my idea:
public static int FindResource<T>(string resourceName)
{
var type = typeof(T);
var field = type.GetField(resourceName);
return (int)field.GetRawConstantValue();
}
Hope it can help you.
I am trying to add a float to my dimens.xml file.
I was reading the following SO answer. When I tried the solution, I got the exception described in the comments. I am trying to figure out why that exception is thrown.
For completeness here is the XML:
<item name="zoom_level" format="float" type="dimen">15.0</item>
Here is the code that blows up:
final float zoom = this.getResources().getDimension(R.dimen.zoom_level);
I jumped into the Android source, and here is the method definition for getDimension:
public float getDimension(int id) throws NotFoundException {
synchronized (mTmpValue) {
TypedValue value = mTmpValue;
getValue(id, value, true);
if (value.type == TypedValue.TYPE_DIMENSION) {
return TypedValue.complexToDimension(value.data, mMetrics);
}
throw new NotFoundException(
"Resource ID #0x" + Integer.toHexString(id) + " type #0x"
+ Integer.toHexString(value.type) + " is not valid");
}
}
So for whatever reason value.type != TypedValue.TYPE_DIMENSION. I do not have my Android source completely set up so I cannot easily add a Log.w("YARIAN", "value type is " + value.type)' statement in there.
I then jumped into getValue and the chain of calls seems to be:
Resources.getValue -> AssetManager.getResourceValue -> AssetManager.loadResourceValue
loadResourceValue is a native method and here is where my digging falls apart.
Anybody know what the best way to understand what's going is?
I also noticed that Resources has a TypedValue.TYPE_FLOAT and TypedValue.TYPE_DIMENSION. But in XML, I cannot write type="float".
The work around described in the comments is to use type=string and then use Float.parse to get the float. Is this necessary? Why or why not?
I know it's a late answer but you should use TypedValue#getFloat() instead of parsing the String to a float like you suggested.
XML:
<item name="float_resource" format="float" type="raw">5.0</item>
Java:
TypedValue out = new TypedValue();
context.getResources().getValue(R.raw.float_resource, out, true);
float floatResource = out.getFloat();
You can put fraction, raw or string as the type if you prefer, this only corresponds to the resource class in R.
There's now Resources.getFloat (from API 29) and ResourcesCompat.getFloat:
val zoomLevel: Float = ResourcesCompat.getFloat(resources, R.dimen.zoom_level)
You can leave your zoom_level XML as it is in the question.
I just ran into this problem too, and though the error message isn't too helpful, I realized my problem was that I was putting just a float value in my resource file and didn't specify a measurement. Switching 15.0 to 15.0dp for instance would avoid the problem and allow you to still use a regular dimension resource.
Kotlin extension function made from Rich answer:
fun Resources.getFloatValue(#DimenRes floatRes:Int):Float{
val out = TypedValue()
getValue(floatRes, out, true)
return out.float
}
Usage:
resources.getFloatValue(R.dimen.my_float)
I'm working with parsing custom attributes, and I've come across something weird. Let's say my parser looks something like this:
final TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.Item);
final int size = attributes.getIndexCount();
for(int i = 0; i < size; i++) {
final int attr = attributes.getIndex(i);
if(attr == R.styleable.Item_custom_attrib) {
final int resourceId = attributes.getResourceId(attr, -1);
if(resourceId == -1)
throw new Resources.NotFoundException("The resource specified was not found.");
...
}
attributes.recycle();
This works. Now, if I replace line #2 with final int size = attributes.length(); which means I get this:
final TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.Item);
final int size = attributes.length();
for(int i = 0; i < size; i++) {
final int attr = attributes.getIndex(i);
if(attr == R.styleable.Item_animation_src) {
final int resourceId = attributes.getResourceId(attr, -1);
if(resourceId == -1)
throw new Resources.NotFoundException("The resource specified was not found.");
...
}
attributes.recycle();
This crashes with the Resources.NotFoundException that I throw. In other words, attributes.getResourceId(attr, -1); returns the default -1 value.
Now, in this particular case, there is only one custom attribute. Both attributes.getIndexCount() and attributes.length() return 1 because there is indeed a value in my attribute. Which means getIndex(i) should return the same number, but it does not. It implies that getIndexCount() does more than simply return the number of indices in the array that have data. What exactly is the difference between the two methods where one allows me to get the attributes while the other does not?
I was just fiddling with this and I found the whole thing confusing, but I think I got it figured out:
So you have declared N attributes in attrs.xml, for your class.
Now the size of the obtained TypedArray is N. It always passes the same sized array.
But maybe you defined only X attributes in your layout xml, so getIndexCount() returns X.
In your widget's init, if you were to ask yourself how many and which attributes
were defined in xml, you would first find out how many, using getIndexCount().
Then you loop from 0 to getIndexCount()-1 and ask the TypedArray: "what's the index of
the i-th supplied attribute?".
So suppose you want to force an attribute to be set in xml, you can now check this
because one of the calls to getIndex() has to return the id you are looking for.
But you can always ignore that, and supply default values. Then you can just call a.getInteger( R.styleable.MyClass_MyAttribute, defaultValue );
You can check R.java to see that all attribute IDs start at 0, for every class. The generated comments are also helpful to understand.
you question is very simple indeed in common use,like this,first defines a class myview extends View{...},and then apply myview to layout.xml and define some attributes like 'textColor="#ffffffff"' etc,in the layout.xml,so your a.getIndexCount() return not null or not zero,that is what you expected.
now i change the question, you define a class myview extends Object,note not extends View class,and you will never use the layout.xml to your self-defined myview.the whole things like
the android defined class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Callback, AccessibilityEventSource{...},no layout.xml,no extends View convenience,the
only resources you can use is attrs.xml,styles.xml.
so what you able to do to let a.getIndexCount() return not null or not zero
like mytop: why TypedArray.getIndexCount() always returns 0
i have a multilingual android app, where i have put the different translations in the strings.xml in the respective directory.
now i also have a custom xml file, where i would like to reference texts like this:
<?xml version="1.0" encoding="UTF-8"?>
<rooms>
<room title="#+string/localizedtext" />
</rooms>
now when i read the title attribute in my code, i obviously get the unresolved string "#+string/localizedtext" like it is.
is it possible to somehow resolve this link to the localized text automatically?
thanks!
Almost a year later:
public static String getStringResource(Context context, String thingie) {
try {
String[] split = thingie.split("/");
String pack = split[0].replace("#", "");
String name = split[1];
int id = context.getResources().getIdentifier(name, pack, context.getPackageName());
return context.getResources().getString(id);
} catch (Exception e) {
return thingie;
}
}
That'll do it.
This might seem like a broad answer but I believe it'll clarify a lot of things for people who spent hours looking for it (I'm one of them).
The short answer is yes, you can use references in custom XML, not just for strings, but that's the example I use, for ease of understanding.
Considering the context:
res/values/strings.xml
(Default strings, usually en-US for convenience but that's up to the developer)
<resources>
<string name="sample_string">This is a sample string.</string>
</resources>
res/values-fr/strings.xml
(Localized french strings)
<resources>
<string name="sample_string">Ceci est un exemple de chaƮne</string>
</resources>
res/xml/test.xml
(Custom XML file)
<!-- #string/sample_string identifies both
the default and french localized strings,
the system settings determine which is used at runtime.
-->
<test>
<sample name="sampleName" text="#string/sample_string"/>
</test>
src/com/example/app/TestXmlParser.java
//Omitted imports for clarity.
public class testXmlParser {
public static final String ns = null;
public int parse(XmlResourceParser parser) throws XmlPullParserException,
IOException{
while(parser.next() != XmlPullParser.END_DOCUMENT){
if(parser.getEventType() == XmlPullParser.START_TAG){
if(parser.getName().equalsIgnoreCase("sample")){
// This is what matters, we're getting a
// resource identifier and returning it.
return parser.getAttributeResourceValue(ns, "text", -1);
}
}
}
return -1;
}
Use String getText(int id) to obtain the string corresponding to id (localized, if available).
Using the example above it would amount to replace :
//Return the resource id
return parser.getAttributeResourceValue(ns, "text", -1);
with :
//Return the localized string corresponding to the id.
int id = parser.getAttributeResourceValue(ns, "text", -1);
return getString(id);
The way you tried is not possible.
You might get similar functionality with <string-array> resource:
<resources>
<string-array name="room">
<item>#string/localizedText</item>
<item>#string/otherLocalizedText</item>
</string-array>
</resources>
then you would use it like this :
String[] room = getResources().getStringArray(R.array.room);
String localizedText = room[0];
String otherLocalizedText = room[1];
Localization in Android is done with resource identifiers. Check out this Android tutorial.
http://developer.android.com/resources/tutorials/localization/index.html
See discussion below.
Great answer kyis, shame I still don't have enough brownie points to rate it. To answer Nick's question, just change the last bit of code to:
int id = parser.getAttributeResourceValue(ns, "text", 0);
return (id != 0) ? getString(id) : parser.getAttributeValue(ns, "text");
Note that I used 0 for the default value of the resource as this is guaranteed never to be a real resource value. -1 would have done also.