LayoutLib: Support getting resource arrays. [DO NOT MERGE]

- Don't crash when Resources.get<Type>Array() is called.
- If the IDE supports it, actually return the value.
- Add tests for getArray.
- Update test app to latest gradle plugin version.
- Switch to using AppTheme for tests, since the tests depend on some
  custom theme attributes. The AppTheme now inherits from
  Material.Light.DarkActionBar, so other tests should be unaffected.

Depends on a newer version of sdk-common, which fixes the parsing of
array resource in value files.

Bug: 12372031
Change-Id: I313b61511e98ac1402d75056ebfdeeb005ebb96d
(cherry picked from commit 642cff50f8)
This commit is contained in:
Deepanshu Gupta
2015-05-22 15:47:16 -07:00
parent a5ffed0b69
commit ba5a02c5aa
22 changed files with 244 additions and 14 deletions

View File

@@ -16,6 +16,8 @@
package android.content.res;
import com.android.SdkConstants;
import com.android.ide.common.rendering.api.ArrayResourceValue;
import com.android.ide.common.rendering.api.LayoutLog;
import com.android.ide.common.rendering.api.LayoutlibCallback;
import com.android.ide.common.rendering.api.ResourceValue;
@@ -32,6 +34,8 @@ import com.android.util.Pair;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
@@ -42,6 +46,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Iterator;
/**
*
@@ -241,6 +246,145 @@ public final class BridgeResources extends Resources {
return null;
}
@Override
public CharSequence[] getTextArray(int id) throws NotFoundException {
ResourceValue resValue = getArrayResourceValue(id);
if (resValue == null) {
// Error already logged by getArrayResourceValue.
return new CharSequence[0];
} else if (!(resValue instanceof ArrayResourceValue)) {
return new CharSequence[]{
resolveReference(resValue.getValue(), resValue.isFramework())};
}
ArrayResourceValue arv = ((ArrayResourceValue) resValue);
return fillValues(arv, new CharSequence[arv.getElementCount()]);
}
@Override
public String[] getStringArray(int id) throws NotFoundException {
ResourceValue resValue = getArrayResourceValue(id);
if (resValue == null) {
// Error already logged by getArrayResourceValue.
return new String[0];
} else if (!(resValue instanceof ArrayResourceValue)) {
return new String[]{
resolveReference(resValue.getValue(), resValue.isFramework())};
}
ArrayResourceValue arv = ((ArrayResourceValue) resValue);
return fillValues(arv, new String[arv.getElementCount()]);
}
/**
* Resolve each element in resValue and copy them to {@code values}. The values copied are
* always Strings. The ideal signature for the method should be &lt;T super String&gt;, but java
* generics don't support it.
*/
private <T extends CharSequence> T[] fillValues(ArrayResourceValue resValue, T[] values) {
int i = 0;
for (Iterator<String> iterator = resValue.iterator(); iterator.hasNext(); i++) {
@SuppressWarnings("unchecked")
T s = (T) resolveReference(iterator.next(), resValue.isFramework());
values[i] = s;
}
return values;
}
@Override
public int[] getIntArray(int id) throws NotFoundException {
ResourceValue rv = getArrayResourceValue(id);
if (rv == null) {
// Error already logged by getArrayResourceValue.
return new int[0];
} else if (!(rv instanceof ArrayResourceValue)) {
// This is an older IDE that can only give us the first element of the array.
String firstValue = resolveReference(rv.getValue(), rv.isFramework());
try {
return new int[]{getInt(firstValue)};
} catch (NumberFormatException e) {
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
"Integer resource array contains non-integer value: " +
firstValue, null);
return new int[1];
}
}
ArrayResourceValue resValue = ((ArrayResourceValue) rv);
int[] values = new int[resValue.getElementCount()];
int i = 0;
for (Iterator<String> iterator = resValue.iterator(); iterator.hasNext(); i++) {
String element = resolveReference(iterator.next(), resValue.isFramework());
try {
values[i] = getInt(element);
} catch (NumberFormatException e) {
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
"Integer resource array contains non-integer value: " + element, null);
}
}
return values;
}
/**
* Try to find the ArrayResourceValue for the given id.
* <p/>
* If the ResourceValue found is not of type {@link ResourceType#ARRAY}, the method logs an
* error and return null. However, if the ResourceValue found has type {@code
* ResourceType.ARRAY}, but the value is not an instance of {@link ArrayResourceValue}, the
* method returns the ResourceValue. This happens on older versions of the IDE, which did not
* parse the array resources properly.
* <p/>
* @throws NotFoundException if no resource if found
*/
@Nullable
private ResourceValue getArrayResourceValue(int id) throws NotFoundException {
Pair<String, ResourceValue> v = getResourceValue(id, mPlatformResourceFlag);
if (v != null) {
ResourceValue resValue = v.getSecond();
assert resValue != null;
if (resValue != null) {
final ResourceType type = resValue.getResourceType();
if (type != ResourceType.ARRAY) {
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE,
String.format(
"Resource with id 0x%1$X is not an array resource, but %2$s",
id, type == null ? "null" : type.getDisplayName()),
null);
return null;
}
if (!(resValue instanceof ArrayResourceValue)) {
Bridge.getLog().warning(LayoutLog.TAG_UNSUPPORTED,
"Obtaining resource arrays via getTextArray, getStringArray or getIntArray is not fully supported in this version of the IDE.",
null);
}
return resValue;
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(id);
// this is not used since the method above always throws
return null;
}
@NonNull
private String resolveReference(@NonNull String ref, boolean forceFrameworkOnly) {
if (ref.startsWith(SdkConstants.PREFIX_RESOURCE_REF) || ref.startsWith
(SdkConstants.PREFIX_THEME_REF)) {
ResourceValue rv =
mContext.getRenderResources().findResValue(ref, forceFrameworkOnly);
rv = mContext.getRenderResources().resolveResValue(rv);
if (rv != null) {
return rv.getValue();
} else {
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE,
"Unable to resolve resource " + ref, null);
}
}
// Not a reference.
return ref;
}
@Override
public XmlResourceParser getLayout(int id) throws NotFoundException {
Pair<String, ResourceValue> v = getResourceValue(id, mPlatformResourceFlag);
@@ -431,13 +575,8 @@ public final class BridgeResources extends Resources {
if (resValue != null) {
String v = resValue.getValue();
if (v != null) {
int radix = 10;
if (v.startsWith("0x")) {
v = v.substring(2);
radix = 16;
}
try {
return Integer.parseInt(v, radix);
return getInt(v);
} catch (NumberFormatException e) {
// return exception below
}
@@ -610,7 +749,6 @@ public final class BridgeResources extends Resources {
}
}
@Override
public InputStream openRawResource(int id) throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag);
@@ -691,7 +829,7 @@ public final class BridgeResources extends Resources {
resourceInfo = mLayoutlibCallback.resolveResourceId(id);
}
String message = null;
String message;
if (resourceInfo != null) {
message = String.format(
"Could not find %1$s resource matching value 0x%2$X (resolved name: %3$s) in current configuration.",
@@ -703,4 +841,15 @@ public final class BridgeResources extends Resources {
throw new NotFoundException(message);
}
private int getInt(String v) throws NumberFormatException {
int radix = 10;
if (v.startsWith("0x")) {
v = v.substring(2);
radix = 16;
} else if (v.startsWith("0")) {
radix = 8;
}
return Integer.parseInt(v, radix);
}
}

View File

@@ -3,7 +3,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.1.3'
classpath 'com.android.tools.build:gradle:1.2.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -19,12 +19,12 @@ allprojects {
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
compileSdkVersion 22
buildToolsVersion '21.1.2'
defaultConfig {
applicationId 'com.android.layoutlib.test.myapplication'
minSdkVersion 19
targetSdkVersion 21
minSdkVersion 21
targetSdkVersion 22
versionCode 1
versionName '1.0'
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -0,0 +1,41 @@
package com.android.layoutlib.test.myapplication;
import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* A widget to test obtaining arrays from resources.
*/
public class ArraysCheckWidget extends LinearLayout {
public ArraysCheckWidget(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ArraysCheckWidget(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public ArraysCheckWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
Resources resources = context.getResources();
for (CharSequence chars : resources.getTextArray(R.array.array)) {
addTextView(context, chars);
}
for (int i : resources.getIntArray(R.array.int_array)) {
addTextView(context, String.valueOf(i));
}
for (String string : resources.getStringArray(R.array.string_array)) {
addTextView(context, string);
}
}
private void addTextView(Context context, CharSequence string) {
TextView textView = new TextView(context);
textView.setText(string);
textView.setTextSize(30);
addView(textView);
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<com.android.layoutlib.test.myapplication.ArraysCheckWidget xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
</com.android.layoutlib.test.myapplication.ArraysCheckWidget>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="array">
<item>first</item>
<item>second</item>
</array>
<integer-array name="int_array">
<item>1</item>
<item>0xaB</item> <!-- hex entry (decimal 171) -->
<item>010</item> <!-- octal entry -->
<item>@integer/ten</item> <!-- value reference -->
<item>?attr/myattr</item> <!-- theme attr reference -->
</integer-array>
<string-array name="string_array">
<item>mystring</item>
<item>@string/hello_world</item> <!-- string ref in appNs -->
<!-- theme ref in android NS. value = @string/candidates_style = <u>candidates</u> -->
<item>?android:attr/candidatesTextStyleSpans</item>
<item>@android:string/unknownName</item> <!-- value = Unknown -->
</string-array>
<!-- resources that the above array can refer to -->
<integer name="ten">10</integer>
<integer name="twelve">12</integer>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="myattr" format="integer" />
</resources>

View File

@@ -1,7 +1,8 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
<style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
<item name="myattr">@integer/ten</item>
<!-- Customize your theme here. -->
</style>

View File

@@ -296,6 +296,11 @@ public class Main {
renderAndVerify("allwidgets.xml", "allwidgets.png");
}
@Test
public void testArrayCheck() throws ClassNotFoundException {
renderAndVerify("array_check.xml", "array_check.png");
}
/** Test expand_layout.xml */
@Test
public void testExpand() throws ClassNotFoundException {
@@ -388,7 +393,6 @@ public class Main {
ResourceResolver.create(mProjectResources.getConfiguredResources(config),
mFrameworkRepo.getConfiguredResources(config),
themeName, false);
return new SessionParams(
layoutParser,
renderingMode,