1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.content.res;
18
19import com.android.ide.common.rendering.api.AttrResourceValue;
20import com.android.ide.common.rendering.api.LayoutLog;
21import com.android.ide.common.rendering.api.RenderResources;
22import com.android.ide.common.rendering.api.ResourceValue;
23import com.android.ide.common.rendering.api.StyleResourceValue;
24import com.android.internal.util.XmlUtils;
25import com.android.layoutlib.bridge.Bridge;
26import com.android.layoutlib.bridge.android.BridgeContext;
27import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
28import com.android.layoutlib.bridge.impl.ParserFactory;
29import com.android.layoutlib.bridge.impl.ResourceHelper;
30import com.android.resources.ResourceType;
31
32import org.xmlpull.v1.XmlPullParser;
33import org.xmlpull.v1.XmlPullParserException;
34
35import android.annotation.Nullable;
36import android.content.res.Resources.Theme;
37import android.graphics.drawable.Drawable;
38import android.util.DisplayMetrics;
39import android.util.TypedValue;
40import android.view.LayoutInflater_Delegate;
41import android.view.ViewGroup.LayoutParams;
42
43import java.io.File;
44import java.util.ArrayList;
45import java.util.Arrays;
46import java.util.Map;
47
48import static android.util.TypedValue.TYPE_ATTRIBUTE;
49import static android.util.TypedValue.TYPE_DIMENSION;
50import static android.util.TypedValue.TYPE_FLOAT;
51import static android.util.TypedValue.TYPE_INT_BOOLEAN;
52import static android.util.TypedValue.TYPE_INT_COLOR_ARGB4;
53import static android.util.TypedValue.TYPE_INT_COLOR_ARGB8;
54import static android.util.TypedValue.TYPE_INT_COLOR_RGB4;
55import static android.util.TypedValue.TYPE_INT_COLOR_RGB8;
56import static android.util.TypedValue.TYPE_INT_DEC;
57import static android.util.TypedValue.TYPE_INT_HEX;
58import static android.util.TypedValue.TYPE_NULL;
59import static android.util.TypedValue.TYPE_REFERENCE;
60import static android.util.TypedValue.TYPE_STRING;
61import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
62import static com.android.SdkConstants.PREFIX_THEME_REF;
63import static com.android.ide.common.rendering.api.RenderResources.REFERENCE_EMPTY;
64import static com.android.ide.common.rendering.api.RenderResources.REFERENCE_NULL;
65import static com.android.ide.common.rendering.api.RenderResources.REFERENCE_UNDEFINED;
66
67/**
68 * Custom implementation of TypedArray to handle non compiled resources.
69 */
70public final class BridgeTypedArray extends TypedArray {
71
72    private final BridgeResources mBridgeResources;
73    private final BridgeContext mContext;
74    private final boolean mPlatformFile;
75
76    private final ResourceValue[] mResourceData;
77    private final String[] mNames;
78    private final boolean[] mIsFramework;
79
80    // Contains ids that are @empty. We still store null in mResourceData for that index, since we
81    // want to save on the check against empty, each time a resource value is requested.
82    @Nullable
83    private int[] mEmptyIds;
84
85    public BridgeTypedArray(BridgeResources resources, BridgeContext context, int len,
86            boolean platformFile) {
87        super(resources, null, null, 0);
88        mBridgeResources = resources;
89        mContext = context;
90        mPlatformFile = platformFile;
91        mResourceData = new ResourceValue[len];
92        mNames = new String[len];
93        mIsFramework = new boolean[len];
94    }
95
96    /**
97     * A bridge-specific method that sets a value in the type array
98     * @param index the index of the value in the TypedArray
99     * @param name the name of the attribute
100     * @param isFramework whether the attribute is in the android namespace.
101     * @param value the value of the attribute
102     */
103    public void bridgeSetValue(int index, String name, boolean isFramework, ResourceValue value) {
104        mResourceData[index] = value;
105        mNames[index] = name;
106        mIsFramework[index] = isFramework;
107    }
108
109    /**
110     * Seals the array after all calls to
111     * {@link #bridgeSetValue(int, String, boolean, ResourceValue)} have been done.
112     * <p/>This allows to compute the list of non default values, permitting
113     * {@link #getIndexCount()} to return the proper value.
114     */
115    public void sealArray() {
116        // fills TypedArray.mIndices which is used to implement getIndexCount/getIndexAt
117        // first count the array size
118        int count = 0;
119        ArrayList<Integer> emptyIds = null;
120        for (int i = 0; i < mResourceData.length; i++) {
121            ResourceValue data = mResourceData[i];
122            if (data != null) {
123                String dataValue = data.getValue();
124                if (REFERENCE_NULL.equals(dataValue) || REFERENCE_UNDEFINED.equals(dataValue)) {
125                    mResourceData[i] = null;
126                } else if (REFERENCE_EMPTY.equals(dataValue)) {
127                    mResourceData[i] = null;
128                    if (emptyIds == null) {
129                        emptyIds = new ArrayList<Integer>(4);
130                    }
131                    emptyIds.add(i);
132                } else {
133                    count++;
134                }
135            }
136        }
137
138        if (emptyIds != null) {
139            mEmptyIds = new int[emptyIds.size()];
140            for (int i = 0; i < emptyIds.size(); i++) {
141                mEmptyIds[i] = emptyIds.get(i);
142            }
143        }
144
145        // allocate the table with an extra to store the size
146        mIndices = new int[count+1];
147        mIndices[0] = count;
148
149        // fill the array with the indices.
150        int index = 1;
151        for (int i = 0 ; i < mResourceData.length ; i++) {
152            if (mResourceData[i] != null) {
153                mIndices[index++] = i;
154            }
155        }
156    }
157
158    /**
159     * Set the theme to be used for inflating drawables.
160     */
161    public void setTheme(Theme theme) {
162        mTheme = theme;
163    }
164
165    /**
166     * Return the number of values in this array.
167     */
168    @Override
169    public int length() {
170        return mResourceData.length;
171    }
172
173    /**
174     * Return the Resources object this array was loaded from.
175     */
176    @Override
177    public Resources getResources() {
178        return mBridgeResources;
179    }
180
181    /**
182     * Retrieve the styled string value for the attribute at <var>index</var>.
183     *
184     * @param index Index of attribute to retrieve.
185     *
186     * @return CharSequence holding string data.  May be styled.  Returns
187     *         null if the attribute is not defined.
188     */
189    @Override
190    public CharSequence getText(int index) {
191        // FIXME: handle styled strings!
192        return getString(index);
193    }
194
195    /**
196     * Retrieve the string value for the attribute at <var>index</var>.
197     *
198     * @param index Index of attribute to retrieve.
199     *
200     * @return String holding string data.  Any styling information is
201     * removed.  Returns null if the attribute is not defined.
202     */
203    @Override
204    public String getString(int index) {
205        if (!hasValue(index)) {
206            return null;
207        }
208        // As unfortunate as it is, it's possible to use enums with all attribute formats,
209        // not just integers/enums. So, we need to search the enums always. In case
210        // enums are used, the returned value is an integer.
211        Integer v = resolveEnumAttribute(index);
212        return v == null ? mResourceData[index].getValue() : String.valueOf((int) v);
213    }
214
215    /**
216     * Retrieve the boolean value for the attribute at <var>index</var>.
217     *
218     * @param index Index of attribute to retrieve.
219     * @param defValue Value to return if the attribute is not defined.
220     *
221     * @return Attribute boolean value, or defValue if not defined.
222     */
223    @Override
224    public boolean getBoolean(int index, boolean defValue) {
225        String s = getString(index);
226        return s == null ? defValue : XmlUtils.convertValueToBoolean(s, defValue);
227
228    }
229
230    /**
231     * Retrieve the integer value for the attribute at <var>index</var>.
232     *
233     * @param index Index of attribute to retrieve.
234     * @param defValue Value to return if the attribute is not defined.
235     *
236     * @return Attribute int value, or defValue if not defined.
237     */
238    @Override
239    public int getInt(int index, int defValue) {
240        String s = getString(index);
241        try {
242            return convertValueToInt(s, defValue);
243        } catch (NumberFormatException e) {
244            Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT,
245                    String.format("\"%1$s\" in attribute \"%2$s\" is not a valid integer",
246                            s, mNames[index]),
247                    null);
248        }
249        return defValue;
250    }
251
252    /**
253     * Retrieve the float value for the attribute at <var>index</var>.
254     *
255     * @param index Index of attribute to retrieve.
256     *
257     * @return Attribute float value, or defValue if not defined..
258     */
259    @Override
260    public float getFloat(int index, float defValue) {
261        String s = getString(index);
262        try {
263            if (s != null) {
264                    return Float.parseFloat(s);
265            }
266        } catch (NumberFormatException e) {
267            Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT,
268                    String.format("\"%1$s\" in attribute \"%2$s\" cannot be converted to float.",
269                            s, mNames[index]),
270                    null);
271        }
272        return defValue;
273    }
274
275    /**
276     * Retrieve the color value for the attribute at <var>index</var>.  If
277     * the attribute references a color resource holding a complex
278     * {@link android.content.res.ColorStateList}, then the default color from
279     * the set is returned.
280     *
281     * @param index Index of attribute to retrieve.
282     * @param defValue Value to return if the attribute is not defined or
283     *                 not a resource.
284     *
285     * @return Attribute color value, or defValue if not defined.
286     */
287    @Override
288    public int getColor(int index, int defValue) {
289        if (index < 0 || index >= mResourceData.length) {
290            return defValue;
291        }
292
293        if (mResourceData[index] == null) {
294            return defValue;
295        }
296
297        ColorStateList colorStateList = ResourceHelper.getColorStateList(
298                mResourceData[index], mContext);
299        if (colorStateList != null) {
300            return colorStateList.getDefaultColor();
301        }
302
303        return defValue;
304    }
305
306    /**
307     * Retrieve the ColorStateList for the attribute at <var>index</var>.
308     * The value may be either a single solid color or a reference to
309     * a color or complex {@link android.content.res.ColorStateList} description.
310     *
311     * @param index Index of attribute to retrieve.
312     *
313     * @return ColorStateList for the attribute, or null if not defined.
314     */
315    @Override
316    public ColorStateList getColorStateList(int index) {
317        if (!hasValue(index)) {
318            return null;
319        }
320
321        ResourceValue resValue = mResourceData[index];
322        String value = resValue.getValue();
323
324        if (value == null) {
325            return null;
326        }
327
328        // let the framework inflate the ColorStateList from the XML file.
329        File f = new File(value);
330        if (f.isFile()) {
331            try {
332                XmlPullParser parser = ParserFactory.create(f);
333
334                BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(
335                        parser, mContext, resValue.isFramework());
336                try {
337                    return ColorStateList.createFromXml(mContext.getResources(), blockParser,
338                            mContext.getTheme());
339                } finally {
340                    blockParser.ensurePopped();
341                }
342            } catch (XmlPullParserException e) {
343                Bridge.getLog().error(LayoutLog.TAG_BROKEN,
344                        "Failed to configure parser for " + value, e, null);
345                return null;
346            } catch (Exception e) {
347                // this is an error and not warning since the file existence is checked before
348                // attempting to parse it.
349                Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
350                        "Failed to parse file " + value, e, null);
351
352                return null;
353            }
354        }
355
356        try {
357            int color = ResourceHelper.getColor(value);
358            return ColorStateList.valueOf(color);
359        } catch (NumberFormatException e) {
360            Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, e.getMessage(), e, null);
361        }
362
363        return null;
364    }
365
366    /**
367     * Retrieve the integer value for the attribute at <var>index</var>.
368     *
369     * @param index Index of attribute to retrieve.
370     * @param defValue Value to return if the attribute is not defined or
371     *                 not a resource.
372     *
373     * @return Attribute integer value, or defValue if not defined.
374     */
375    @Override
376    public int getInteger(int index, int defValue) {
377        return getInt(index, defValue);
378    }
379
380    /**
381     * Retrieve a dimensional unit attribute at <var>index</var>.  Unit
382     * conversions are based on the current {@link DisplayMetrics}
383     * associated with the resources this {@link TypedArray} object
384     * came from.
385     *
386     * @param index Index of attribute to retrieve.
387     * @param defValue Value to return if the attribute is not defined or
388     *                 not a resource.
389     *
390     * @return Attribute dimension value multiplied by the appropriate
391     * metric, or defValue if not defined.
392     *
393     * @see #getDimensionPixelOffset
394     * @see #getDimensionPixelSize
395     */
396    @Override
397    public float getDimension(int index, float defValue) {
398        String s = getString(index);
399        if (s == null) {
400            return defValue;
401        }
402        // Check if the value is a magic constant that doesn't require a unit.
403        try {
404            int i = Integer.parseInt(s);
405            if (i == LayoutParams.MATCH_PARENT || i == LayoutParams.WRAP_CONTENT) {
406                return i;
407            }
408        } catch (NumberFormatException ignored) {
409            // pass
410        }
411
412        if (ResourceHelper.parseFloatAttribute(mNames[index], s, mValue, true)) {
413            return mValue.getDimension(mBridgeResources.getDisplayMetrics());
414        }
415
416        return defValue;
417    }
418
419    /**
420     * Retrieve a dimensional unit attribute at <var>index</var> for use
421     * as an offset in raw pixels.  This is the same as
422     * {@link #getDimension}, except the returned value is converted to
423     * integer pixels for you.  An offset conversion involves simply
424     * truncating the base value to an integer.
425     *
426     * @param index Index of attribute to retrieve.
427     * @param defValue Value to return if the attribute is not defined or
428     *                 not a resource.
429     *
430     * @return Attribute dimension value multiplied by the appropriate
431     * metric and truncated to integer pixels, or defValue if not defined.
432     *
433     * @see #getDimension
434     * @see #getDimensionPixelSize
435     */
436    @Override
437    public int getDimensionPixelOffset(int index, int defValue) {
438        return (int) getDimension(index, defValue);
439    }
440
441    /**
442     * Retrieve a dimensional unit attribute at <var>index</var> for use
443     * as a size in raw pixels.  This is the same as
444     * {@link #getDimension}, except the returned value is converted to
445     * integer pixels for use as a size.  A size conversion involves
446     * rounding the base value, and ensuring that a non-zero base value
447     * is at least one pixel in size.
448     *
449     * @param index Index of attribute to retrieve.
450     * @param defValue Value to return if the attribute is not defined or
451     *                 not a resource.
452     *
453     * @return Attribute dimension value multiplied by the appropriate
454     * metric and truncated to integer pixels, or defValue if not defined.
455     *
456     * @see #getDimension
457     * @see #getDimensionPixelOffset
458     */
459    @Override
460    public int getDimensionPixelSize(int index, int defValue) {
461        try {
462            return getDimension(index, null);
463        } catch (RuntimeException e) {
464            String s = getString(index);
465
466            if (s != null) {
467                // looks like we were unable to resolve the dimension value
468                Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT,
469                        String.format("\"%1$s\" in attribute \"%2$s\" is not a valid format.",
470                                s, mNames[index]), null);
471            }
472
473            return defValue;
474        }
475    }
476
477    /**
478     * Special version of {@link #getDimensionPixelSize} for retrieving
479     * {@link android.view.ViewGroup}'s layout_width and layout_height
480     * attributes.  This is only here for performance reasons; applications
481     * should use {@link #getDimensionPixelSize}.
482     *
483     * @param index Index of the attribute to retrieve.
484     * @param name Textual name of attribute for error reporting.
485     *
486     * @return Attribute dimension value multiplied by the appropriate
487     * metric and truncated to integer pixels.
488     */
489    @Override
490    public int getLayoutDimension(int index, String name) {
491        try {
492            // this will throw an exception if not found.
493            return getDimension(index, name);
494        } catch (RuntimeException e) {
495
496            if (LayoutInflater_Delegate.sIsInInclude) {
497                throw new RuntimeException("Layout Dimension '" + name + "' not found.");
498            }
499
500            Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT,
501                    "You must supply a " + name + " attribute.", null);
502
503            return 0;
504        }
505    }
506
507    @Override
508    public int getLayoutDimension(int index, int defValue) {
509        return getDimensionPixelSize(index, defValue);
510    }
511
512    /** @param name attribute name, used for error reporting. */
513    private int getDimension(int index, @Nullable String name) {
514        String s = getString(index);
515        if (s == null) {
516            if (name != null) {
517                throw new RuntimeException("Attribute '" + name + "' not found");
518            }
519            throw new RuntimeException();
520        }
521        // Check if the value is a magic constant that doesn't require a unit.
522        try {
523            int i = Integer.parseInt(s);
524            if (i == LayoutParams.MATCH_PARENT || i == LayoutParams.WRAP_CONTENT) {
525                return i;
526            }
527        } catch (NumberFormatException ignored) {
528            // pass
529        }
530        if (ResourceHelper.parseFloatAttribute(mNames[index], s, mValue, true)) {
531            float f = mValue.getDimension(mBridgeResources.getDisplayMetrics());
532
533            final int res = (int)(f+0.5f);
534            if (res != 0) return res;
535            if (f == 0) return 0;
536            if (f > 0) return 1;
537        }
538
539        throw new RuntimeException();
540    }
541
542    /**
543     * Retrieve a fractional unit attribute at <var>index</var>.
544     *
545     * @param index Index of attribute to retrieve.
546     * @param base The base value of this fraction.  In other words, a
547     *             standard fraction is multiplied by this value.
548     * @param pbase The parent base value of this fraction.  In other
549     *             words, a parent fraction (nn%p) is multiplied by this
550     *             value.
551     * @param defValue Value to return if the attribute is not defined or
552     *                 not a resource.
553     *
554     * @return Attribute fractional value multiplied by the appropriate
555     * base value, or defValue if not defined.
556     */
557    @Override
558    public float getFraction(int index, int base, int pbase, float defValue) {
559        String value = getString(index);
560        if (value == null) {
561            return defValue;
562        }
563
564        if (ResourceHelper.parseFloatAttribute(mNames[index], value, mValue, false)) {
565            return mValue.getFraction(base, pbase);
566        }
567
568        // looks like we were unable to resolve the fraction value
569        Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT,
570                String.format(
571                        "\"%1$s\" in attribute \"%2$s\" cannot be converted to a fraction.",
572                        value, mNames[index]), null);
573
574        return defValue;
575    }
576
577    /**
578     * Retrieve the resource identifier for the attribute at
579     * <var>index</var>.  Note that attribute resource as resolved when
580     * the overall {@link TypedArray} object is retrieved.  As a
581     * result, this function will return the resource identifier of the
582     * final resource value that was found, <em>not</em> necessarily the
583     * original resource that was specified by the attribute.
584     *
585     * @param index Index of attribute to retrieve.
586     * @param defValue Value to return if the attribute is not defined or
587     *                 not a resource.
588     *
589     * @return Attribute resource identifier, or defValue if not defined.
590     */
591    @Override
592    public int getResourceId(int index, int defValue) {
593        if (index < 0 || index >= mResourceData.length) {
594            return defValue;
595        }
596
597        // get the Resource for this index
598        ResourceValue resValue = mResourceData[index];
599
600        // no data, return the default value.
601        if (resValue == null) {
602            return defValue;
603        }
604
605        // check if this is a style resource
606        if (resValue instanceof StyleResourceValue) {
607            // get the id that will represent this style.
608            return mContext.getDynamicIdByStyle((StyleResourceValue)resValue);
609        }
610
611        // if the attribute was a reference to a resource, and not a declaration of an id (@+id),
612        // then the xml attribute value was "resolved" which leads us to a ResourceValue with a
613        // valid getType() and getName() returning a resource name.
614        // (and getValue() returning null!). We need to handle this!
615        if (resValue.getResourceType() != null) {
616            // if this is a framework id
617            if (mPlatformFile || resValue.isFramework()) {
618                // look for idName in the android R classes
619                return mContext.getFrameworkResourceValue(
620                        resValue.getResourceType(), resValue.getName(), defValue);
621            }
622
623            // look for idName in the project R class.
624            return mContext.getProjectResourceValue(
625                    resValue.getResourceType(), resValue.getName(), defValue);
626        }
627
628        // else, try to get the value, and resolve it somehow.
629        String value = resValue.getValue();
630        if (value == null) {
631            return defValue;
632        }
633
634        // if the value is just an integer, return it.
635        try {
636            int i = Integer.parseInt(value);
637            if (Integer.toString(i).equals(value)) {
638                return i;
639            }
640        } catch (NumberFormatException e) {
641            // pass
642        }
643
644        // Handle the @id/<name>, @+id/<name> and @android:id/<name>
645        // We need to return the exact value that was compiled (from the various R classes),
646        // as these values can be reused internally with calls to findViewById().
647        // There's a trick with platform layouts that not use "android:" but their IDs are in
648        // fact in the android.R and com.android.internal.R classes.
649        // The field mPlatformFile will indicate that all IDs are to be looked up in the android R
650        // classes exclusively.
651
652        // if this is a reference to an id, find it.
653        if (value.startsWith("@id/") || value.startsWith("@+") ||
654                value.startsWith("@android:id/")) {
655
656            int pos = value.indexOf('/');
657            String idName = value.substring(pos + 1);
658            boolean create = value.startsWith("@+");
659            boolean isFrameworkId =
660                    mPlatformFile || value.startsWith("@android") || value.startsWith("@+android");
661
662            // Look for the idName in project or android R class depending on isPlatform.
663            if (create) {
664                Integer idValue;
665                if (isFrameworkId) {
666                    idValue = Bridge.getResourceId(ResourceType.ID, idName);
667                } else {
668                    idValue = mContext.getLayoutlibCallback().getResourceId(ResourceType.ID, idName);
669                }
670                return idValue == null ? defValue : idValue;
671            }
672            // This calls the same method as in if(create), but doesn't create a dynamic id, if
673            // one is not found.
674            if (isFrameworkId) {
675                return mContext.getFrameworkResourceValue(ResourceType.ID, idName, defValue);
676            } else {
677                return mContext.getProjectResourceValue(ResourceType.ID, idName, defValue);
678            }
679        }
680
681        // not a direct id valid reference? resolve it
682        Integer idValue;
683
684        if (resValue.isFramework()) {
685            idValue = Bridge.getResourceId(resValue.getResourceType(),
686                    resValue.getName());
687        } else {
688            idValue = mContext.getLayoutlibCallback().getResourceId(
689                    resValue.getResourceType(), resValue.getName());
690        }
691
692        if (idValue != null) {
693            return idValue;
694        }
695
696        Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_RESOLVE,
697                String.format(
698                    "Unable to resolve id \"%1$s\" for attribute \"%2$s\"", value, mNames[index]),
699                    resValue);
700
701        return defValue;
702    }
703
704    @Override
705    public int getThemeAttributeId(int index, int defValue) {
706        // TODO: Get the right Theme Attribute ID to enable caching of the drawables.
707        return defValue;
708    }
709
710    /**
711     * Retrieve the Drawable for the attribute at <var>index</var>.  This
712     * gets the resource ID of the selected attribute, and uses
713     * {@link Resources#getDrawable Resources.getDrawable} of the owning
714     * Resources object to retrieve its Drawable.
715     *
716     * @param index Index of attribute to retrieve.
717     *
718     * @return Drawable for the attribute, or null if not defined.
719     */
720    @Override
721    public Drawable getDrawable(int index) {
722        if (!hasValue(index)) {
723            return null;
724        }
725
726        ResourceValue value = mResourceData[index];
727        return ResourceHelper.getDrawable(value, mContext, mTheme);
728    }
729
730
731    /**
732     * Retrieve the CharSequence[] for the attribute at <var>index</var>.
733     * This gets the resource ID of the selected attribute, and uses
734     * {@link Resources#getTextArray Resources.getTextArray} of the owning
735     * Resources object to retrieve its String[].
736     *
737     * @param index Index of attribute to retrieve.
738     *
739     * @return CharSequence[] for the attribute, or null if not defined.
740     */
741    @Override
742    public CharSequence[] getTextArray(int index) {
743        String value = getString(index);
744        if (value != null) {
745            return new CharSequence[] { value };
746        }
747
748        return null;
749    }
750
751    @Override
752    public int[] extractThemeAttrs() {
753        // The drawables are always inflated with a Theme and we don't care about caching. So,
754        // just return.
755        return null;
756    }
757
758    @Override
759    public int getChangingConfigurations() {
760        // We don't care about caching. Any change in configuration is a fresh render. So,
761        // just return.
762        return 0;
763    }
764
765    /**
766     * Retrieve the raw TypedValue for the attribute at <var>index</var>.
767     *
768     * @param index Index of attribute to retrieve.
769     * @param outValue TypedValue object in which to place the attribute's
770     *                 data.
771     *
772     * @return Returns true if the value was retrieved, else false.
773     */
774    @Override
775    public boolean getValue(int index, TypedValue outValue) {
776        String s = getString(index);
777        return s != null && ResourceHelper.parseFloatAttribute(mNames[index], s, outValue, false);
778    }
779
780    @Override
781    @SuppressWarnings("ResultOfMethodCallIgnored")
782    public int getType(int index) {
783        String value = getString(index);
784        if (value == null) {
785            return TYPE_NULL;
786        }
787        if (value.startsWith(PREFIX_RESOURCE_REF)) {
788            return TYPE_REFERENCE;
789        }
790        if (value.startsWith(PREFIX_THEME_REF)) {
791            return TYPE_ATTRIBUTE;
792        }
793        try {
794            // Don't care about the value. Only called to check if an exception is thrown.
795            convertValueToInt(value, 0);
796            if (value.startsWith("0x") || value.startsWith("0X")) {
797                return TYPE_INT_HEX;
798            }
799            // is it a color?
800            if (value.startsWith("#")) {
801                int length = value.length() - 1;
802                if (length == 3) {  // rgb
803                    return TYPE_INT_COLOR_RGB4;
804                }
805                if (length == 4) {  // argb
806                    return TYPE_INT_COLOR_ARGB4;
807                }
808                if (length == 6) {  // rrggbb
809                    return TYPE_INT_COLOR_RGB8;
810                }
811                if (length == 8) {  // aarrggbb
812                    return TYPE_INT_COLOR_ARGB8;
813                }
814            }
815            if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
816                return TYPE_INT_BOOLEAN;
817            }
818            return TYPE_INT_DEC;
819        } catch (NumberFormatException ignored) {
820            try {
821                Float.parseFloat(value);
822                return TYPE_FLOAT;
823            } catch (NumberFormatException ignore) {
824            }
825            // Might be a dimension.
826            if (ResourceHelper.parseFloatAttribute(null, value, new TypedValue(), false)) {
827                return TYPE_DIMENSION;
828            }
829        }
830        // TODO: handle fractions.
831        return TYPE_STRING;
832    }
833
834    /**
835     * Determines whether there is an attribute at <var>index</var>.
836     *
837     * @param index Index of attribute to retrieve.
838     *
839     * @return True if the attribute has a value, false otherwise.
840     */
841    @Override
842    public boolean hasValue(int index) {
843        return index >= 0 && index < mResourceData.length && mResourceData[index] != null;
844    }
845
846    @Override
847    public boolean hasValueOrEmpty(int index) {
848        return hasValue(index) || index >= 0 && index < mResourceData.length &&
849                mEmptyIds != null && Arrays.binarySearch(mEmptyIds, index) >= 0;
850    }
851
852    /**
853     * Retrieve the raw TypedValue for the attribute at <var>index</var>
854     * and return a temporary object holding its data.  This object is only
855     * valid until the next call on to {@link TypedArray}.
856     *
857     * @param index Index of attribute to retrieve.
858     *
859     * @return Returns a TypedValue object if the attribute is defined,
860     *         containing its data; otherwise returns null.  (You will not
861     *         receive a TypedValue whose type is TYPE_NULL.)
862     */
863    @Override
864    public TypedValue peekValue(int index) {
865        if (index < 0 || index >= mResourceData.length) {
866            return null;
867        }
868
869        if (getValue(index, mValue)) {
870            return mValue;
871        }
872
873        return null;
874    }
875
876    /**
877     * Returns a message about the parser state suitable for printing error messages.
878     */
879    @Override
880    public String getPositionDescription() {
881        return "<internal -- stub if needed>";
882    }
883
884    /**
885     * Give back a previously retrieved TypedArray, for later re-use.
886     */
887    @Override
888    public void recycle() {
889        // pass
890    }
891
892    @Override
893    public String toString() {
894        return Arrays.toString(mResourceData);
895    }
896
897    /**
898     * Searches for the string in the attributes (flag or enums) and returns the integer.
899     * If found, it will return an integer matching the value.
900     *
901     * @param index Index of attribute to retrieve.
902     *
903     * @return Attribute int value, or null if not defined.
904     */
905    private Integer resolveEnumAttribute(int index) {
906        // Get the map of attribute-constant -> IntegerValue
907        Map<String, Integer> map = null;
908        if (mIsFramework[index]) {
909            map = Bridge.getEnumValues(mNames[index]);
910        } else {
911            // get the styleable matching the resolved name
912            RenderResources res = mContext.getRenderResources();
913            ResourceValue attr = res.getProjectResource(ResourceType.ATTR, mNames[index]);
914            if (attr instanceof AttrResourceValue) {
915                map = ((AttrResourceValue) attr).getAttributeValues();
916            }
917        }
918
919        if (map != null) {
920            // accumulator to store the value of the 1+ constants.
921            int result = 0;
922            boolean found = false;
923
924            // split the value in case this is a mix of several flags.
925            String[] keywords = mResourceData[index].getValue().split("\\|");
926            for (String keyword : keywords) {
927                Integer i = map.get(keyword.trim());
928                if (i != null) {
929                    result |= i;
930                    found = true;
931                }
932                // TODO: We should act smartly and log a warning for incorrect keywords. However,
933                // this method is currently called even if the resourceValue is not an enum.
934            }
935            if (found) {
936                return result;
937            }
938        }
939
940        return null;
941    }
942
943    /**
944     * Copied from {@link XmlUtils#convertValueToInt(CharSequence, int)}, but adapted to account
945     * for aapt, and the fact that host Java VM's Integer.parseInt("XXXXXXXX", 16) cannot handle
946     * "XXXXXXXX" > 80000000.
947     */
948    private static int convertValueToInt(@Nullable String charSeq, int defValue) {
949        if (null == charSeq || charSeq.isEmpty())
950            return defValue;
951
952        int sign = 1;
953        int index = 0;
954        int len = charSeq.length();
955        int base = 10;
956
957        if ('-' == charSeq.charAt(0)) {
958            sign = -1;
959            index++;
960        }
961
962        if ('0' == charSeq.charAt(index)) {
963            //  Quick check for a zero by itself
964            if (index == (len - 1))
965                return 0;
966
967            char c = charSeq.charAt(index + 1);
968
969            if ('x' == c || 'X' == c) {
970                index += 2;
971                base = 16;
972            } else {
973                index++;
974                // Leave the base as 10. aapt removes the preceding zero, and thus when framework
975                // sees the value, it only gets the decimal value.
976            }
977        } else if ('#' == charSeq.charAt(index)) {
978            return ResourceHelper.getColor(charSeq) * sign;
979        } else if ("true".equals(charSeq) || "TRUE".equals(charSeq)) {
980            return -1;
981        } else if ("false".equals(charSeq) || "FALSE".equals(charSeq)) {
982            return 0;
983        }
984
985        // Use Long, since we want to handle hex ints > 80000000.
986        return ((int)Long.parseLong(charSeq.substring(index), base)) * sign;
987    }
988
989    static TypedArray obtain(Resources res, int len) {
990        return res instanceof BridgeResources ?
991                new BridgeTypedArray(((BridgeResources) res), null, len, true) : null;
992    }
993}
994