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 com.android.layoutlib.bridge;
18
19import com.android.internal.util.XmlUtils;
20import com.android.layoutlib.api.IResourceValue;
21import com.android.layoutlib.api.IStyleResourceValue;
22
23import org.kxml2.io.KXmlParser;
24import org.xmlpull.v1.XmlPullParser;
25
26import android.content.res.ColorStateList;
27import android.content.res.Resources;
28import android.content.res.TypedArray;
29import android.graphics.drawable.Drawable;
30import android.util.DisplayMetrics;
31import android.util.TypedValue;
32import android.view.ViewGroup.LayoutParams;
33
34import java.io.File;
35import java.io.FileReader;
36import java.util.Map;
37
38/**
39 * TODO: describe.
40 */
41public final class BridgeTypedArray extends TypedArray {
42
43    @SuppressWarnings("hiding")
44    private BridgeResources mResources;
45    private BridgeContext mContext;
46    @SuppressWarnings("hiding")
47    private IResourceValue[] mData;
48    private String[] mNames;
49    private final boolean mPlatformFile;
50
51    public BridgeTypedArray(BridgeResources resources, BridgeContext context, int len,
52            boolean platformFile) {
53        super(null, null, null, 0);
54        mResources = resources;
55        mContext = context;
56        mPlatformFile = platformFile;
57        mData = new IResourceValue[len];
58        mNames = new String[len];
59    }
60
61    /** A bridge-specific method that sets a value in the type array */
62    public void bridgeSetValue(int index, String name, IResourceValue value) {
63        mData[index] = value;
64        mNames[index] = name;
65    }
66
67    /**
68     * Seals the array after all calls to {@link #bridgeSetValue(int, String, IResourceValue)} have
69     * been done.
70     * <p/>This allows to compute the list of non default values, permitting
71     * {@link #getIndexCount()} to return the proper value.
72     */
73    public void sealArray() {
74        // fills TypedArray.mIndices which is used to implement getIndexCount/getIndexAt
75        // first count the array size
76        int count = 0;
77        for (IResourceValue data : mData) {
78            if (data != null) {
79                count++;
80            }
81        }
82
83        // allocate the table with an extra to store the size
84        mIndices = new int[count+1];
85        mIndices[0] = count;
86
87        // fill the array with the indices.
88        int index = 1;
89        for (int i = 0 ; i < mData.length ; i++) {
90            if (mData[i] != null) {
91                mIndices[index++] = i;
92            }
93        }
94    }
95
96    /**
97     * Return the number of values in this array.
98     */
99    @Override
100    public int length() {
101        return mData.length;
102    }
103
104    /**
105     * Return the Resources object this array was loaded from.
106     */
107    @Override
108    public Resources getResources() {
109        return mResources;
110    }
111
112    /**
113     * Retrieve the styled string value for the attribute at <var>index</var>.
114     *
115     * @param index Index of attribute to retrieve.
116     *
117     * @return CharSequence holding string data.  May be styled.  Returns
118     *         null if the attribute is not defined.
119     */
120    @Override
121    public CharSequence getText(int index) {
122        if (mData[index] != null) {
123            // FIXME: handle styled strings!
124            return mData[index].getValue();
125        }
126
127        return null;
128    }
129
130    /**
131     * Retrieve the string value for the attribute at <var>index</var>.
132     *
133     * @param index Index of attribute to retrieve.
134     *
135     * @return String holding string data.  Any styling information is
136     * removed.  Returns null if the attribute is not defined.
137     */
138    @Override
139    public String getString(int index) {
140        if (mData[index] != null) {
141            return mData[index].getValue();
142        }
143
144        return null;
145    }
146
147    /**
148     * Retrieve the boolean value for the attribute at <var>index</var>.
149     *
150     * @param index Index of attribute to retrieve.
151     * @param defValue Value to return if the attribute is not defined.
152     *
153     * @return Attribute boolean value, or defValue if not defined.
154     */
155    @Override
156    public boolean getBoolean(int index, boolean defValue) {
157        if (mData[index] == null) {
158            return defValue;
159        }
160
161        String s = mData[index].getValue();
162        if (s != null) {
163            return XmlUtils.convertValueToBoolean(s, defValue);
164        }
165
166        return defValue;
167    }
168
169    /**
170     * Retrieve the integer value for the attribute at <var>index</var>.
171     *
172     * @param index Index of attribute to retrieve.
173     * @param defValue Value to return if the attribute is not defined.
174     *
175     * @return Attribute int value, or defValue if not defined.
176     */
177    @Override
178    public int getInt(int index, int defValue) {
179        if (mData[index] == null) {
180            return defValue;
181        }
182
183        String s = mData[index].getValue();
184
185        try {
186            return (s == null) ? defValue : XmlUtils.convertValueToInt(s, defValue);
187        } catch (NumberFormatException e) {
188            // pass
189        }
190
191        // Field is not null and is not an integer.
192        // Check for possible constants and try to find them.
193        // Get the map of attribute-constant -> IntegerValue
194        Map<String, Integer> map = Bridge.getEnumValues(mNames[index]);
195
196        if (map != null) {
197            // accumulator to store the value of the 1+ constants.
198            int result = 0;
199
200            // split the value in case this is a mix of several flags.
201            String[] keywords = s.split("\\|");
202            for (String keyword : keywords) {
203                Integer i = map.get(keyword.trim());
204                if (i != null) {
205                    result |= i.intValue();
206                } else {
207                    mContext.getLogger().warning(String.format(
208                            "Unknown constant \"%s\" in attribute \"%2$s\"",
209                            keyword, mNames[index]));
210                }
211            }
212            return result;
213        }
214
215        return defValue;
216    }
217
218    /**
219     * Retrieve the float value for the attribute at <var>index</var>.
220     *
221     * @param index Index of attribute to retrieve.
222     *
223     * @return Attribute float value, or defValue if not defined..
224     */
225    @Override
226    public float getFloat(int index, float defValue) {
227        if (mData[index] == null) {
228            return defValue;
229        }
230
231        String s = mData[index].getValue();
232
233        if (s != null) {
234            try {
235                return Float.parseFloat(s);
236            } catch (NumberFormatException e) {
237                mContext.getLogger().warning(String.format(
238                        "Unable to convert \"%s\" into a float in attribute \"%2$s\"",
239                        s, mNames[index]));
240
241                // we'll return the default value below.
242            }
243        }
244        return defValue;
245    }
246
247    /**
248     * Retrieve the color value for the attribute at <var>index</var>.  If
249     * the attribute references a color resource holding a complex
250     * {@link android.content.res.ColorStateList}, then the default color from
251     * the set is returned.
252     *
253     * @param index Index of attribute to retrieve.
254     * @param defValue Value to return if the attribute is not defined or
255     *                 not a resource.
256     *
257     * @return Attribute color value, or defValue if not defined.
258     */
259    @Override
260    public int getColor(int index, int defValue) {
261        if (mData[index] == null) {
262            return defValue;
263        }
264
265        String s = mData[index].getValue();
266        try {
267            return ResourceHelper.getColor(s);
268        } catch (NumberFormatException e) {
269            mContext.getLogger().warning(String.format(
270                    "Unable to convert \"%s\" into a color in attribute \"%2$s\"",
271                    s, mNames[index]));
272
273            // we'll return the default value below.
274        }
275
276        return defValue;
277    }
278
279    /**
280     * Retrieve the ColorStateList for the attribute at <var>index</var>.
281     * The value may be either a single solid color or a reference to
282     * a color or complex {@link android.content.res.ColorStateList} description.
283     *
284     * @param index Index of attribute to retrieve.
285     *
286     * @return ColorStateList for the attribute, or null if not defined.
287     */
288    @Override
289    public ColorStateList getColorStateList(int index) {
290        if (mData[index] == null) {
291            return null;
292        }
293
294        String value = mData[index].getValue();
295
296        if (value == null) {
297            return null;
298        }
299
300        try {
301            int color = ResourceHelper.getColor(value);
302            return ColorStateList.valueOf(color);
303        } catch (NumberFormatException e) {
304            // if it's not a color value, we'll attempt to read the xml based color below.
305        }
306
307        // let the framework inflate the ColorStateList from the XML file.
308        try {
309            File f = new File(value);
310            if (f.isFile()) {
311                KXmlParser parser = new KXmlParser();
312                parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
313                parser.setInput(new FileReader(f));
314
315                ColorStateList colorStateList = ColorStateList.createFromXml(
316                        mContext.getResources(),
317                        // FIXME: we need to know if this resource is platform or not
318                        new BridgeXmlBlockParser(parser, mContext, false));
319                return colorStateList;
320            }
321        } catch (Exception e) {
322            // this is an error and not warning since the file existence is checked before
323            // attempting to parse it.
324            mContext.getLogger().error(e);
325
326            // return null below.
327        }
328
329        // looks like were unable to resolve the color value.
330        mContext.getLogger().warning(String.format(
331                "Unable to resolve color value \"%1$s\" in attribute \"%2$s\"",
332                value, mNames[index]));
333
334        return null;
335    }
336
337    /**
338     * Retrieve the integer value for the attribute at <var>index</var>.
339     *
340     * @param index Index of attribute to retrieve.
341     * @param defValue Value to return if the attribute is not defined or
342     *                 not a resource.
343     *
344     * @return Attribute integer value, or defValue if not defined.
345     */
346    @Override
347    public int getInteger(int index, int defValue) {
348        if (mData[index] == null) {
349            return defValue;
350        }
351
352        String s = mData[index].getValue();
353
354        if (s != null) {
355            try {
356                return Integer.parseInt(s);
357            } catch (NumberFormatException e) {
358                mContext.getLogger().warning(String.format(
359                        "Unable to convert \"%s\" into a integer in attribute \"%2$s\"",
360                        s, mNames[index]));
361
362                // The default value is returned below.
363            }
364        }
365
366        return defValue;
367    }
368
369    /**
370     * Retrieve a dimensional unit attribute at <var>index</var>.  Unit
371     * conversions are based on the current {@link DisplayMetrics}
372     * associated with the resources this {@link TypedArray} object
373     * came from.
374     *
375     * @param index Index of attribute to retrieve.
376     * @param defValue Value to return if the attribute is not defined or
377     *                 not a resource.
378     *
379     * @return Attribute dimension value multiplied by the appropriate
380     * metric, or defValue if not defined.
381     *
382     * @see #getDimensionPixelOffset
383     * @see #getDimensionPixelSize
384     */
385    @Override
386    public float getDimension(int index, float defValue) {
387        if (mData[index] == null) {
388            return defValue;
389        }
390
391        String s = mData[index].getValue();
392
393        if (s == null) {
394            return defValue;
395        } else if (s.equals(BridgeConstants.MATCH_PARENT) ||
396                s.equals(BridgeConstants.FILL_PARENT)) {
397            return LayoutParams.MATCH_PARENT;
398        } else if (s.equals(BridgeConstants.WRAP_CONTENT)) {
399            return LayoutParams.WRAP_CONTENT;
400        }
401
402        if (ResourceHelper.stringToFloat(s, mValue)) {
403            return mValue.getDimension(mResources.mMetrics);
404        }
405
406        // looks like we were unable to resolve the dimension value
407        mContext.getLogger().warning(String.format(
408                "Unable to resolve dimension value \"%1$s\" in attribute \"%2$s\"",
409                s, mNames[index]));
410
411        return defValue;
412    }
413
414    /**
415     * Retrieve a dimensional unit attribute at <var>index</var> for use
416     * as an offset in raw pixels.  This is the same as
417     * {@link #getDimension}, except the returned value is converted to
418     * integer pixels for you.  An offset conversion involves simply
419     * truncating the base value to an integer.
420     *
421     * @param index Index of attribute to retrieve.
422     * @param defValue Value to return if the attribute is not defined or
423     *                 not a resource.
424     *
425     * @return Attribute dimension value multiplied by the appropriate
426     * metric and truncated to integer pixels, or defValue if not defined.
427     *
428     * @see #getDimension
429     * @see #getDimensionPixelSize
430     */
431    @Override
432    public int getDimensionPixelOffset(int index, int defValue) {
433        return (int) getDimension(index, defValue);
434    }
435
436    /**
437     * Retrieve a dimensional unit attribute at <var>index</var> for use
438     * as a size in raw pixels.  This is the same as
439     * {@link #getDimension}, except the returned value is converted to
440     * integer pixels for use as a size.  A size conversion involves
441     * rounding the base value, and ensuring that a non-zero base value
442     * is at least one pixel in size.
443     *
444     * @param index Index of attribute to retrieve.
445     * @param defValue Value to return if the attribute is not defined or
446     *                 not a resource.
447     *
448     * @return Attribute dimension value multiplied by the appropriate
449     * metric and truncated to integer pixels, or defValue if not defined.
450     *
451     * @see #getDimension
452     * @see #getDimensionPixelOffset
453     */
454    @Override
455    public int getDimensionPixelSize(int index, int defValue) {
456        if (mData[index] == null) {
457            return defValue;
458        }
459
460        String s = mData[index].getValue();
461
462        if (s == null) {
463            return defValue;
464        } else if (s.equals(BridgeConstants.MATCH_PARENT) ||
465                s.equals(BridgeConstants.FILL_PARENT)) {
466            return LayoutParams.MATCH_PARENT;
467        } else if (s.equals(BridgeConstants.WRAP_CONTENT)) {
468            return LayoutParams.WRAP_CONTENT;
469        }
470
471        // FIXME huh?
472
473        float f = getDimension(index, defValue);
474        final int res = (int)(f+0.5f);
475        if (res != 0) return res;
476        if (f == 0) return 0;
477        if (f > 0) return 1;
478
479        throw new UnsupportedOperationException("Can't convert to dimension: " +
480                Integer.toString(index));
481    }
482
483    /**
484     * Special version of {@link #getDimensionPixelSize} for retrieving
485     * {@link android.view.ViewGroup}'s layout_width and layout_height
486     * attributes.  This is only here for performance reasons; applications
487     * should use {@link #getDimensionPixelSize}.
488     *
489     * @param index Index of the attribute to retrieve.
490     * @param name Textual name of attribute for error reporting.
491     *
492     * @return Attribute dimension value multiplied by the appropriate
493     * metric and truncated to integer pixels.
494     */
495    @Override
496    public int getLayoutDimension(int index, String name) {
497        return getDimensionPixelSize(index, 0);
498    }
499
500    @Override
501    public int getLayoutDimension(int index, int defValue) {
502        return getDimensionPixelSize(index, defValue);
503    }
504
505    /**
506     * Retrieve a fractional unit attribute at <var>index</var>.
507     *
508     * @param index Index of attribute to retrieve.
509     * @param base The base value of this fraction.  In other words, a
510     *             standard fraction is multiplied by this value.
511     * @param pbase The parent base value of this fraction.  In other
512     *             words, a parent fraction (nn%p) is multiplied by this
513     *             value.
514     * @param defValue Value to return if the attribute is not defined or
515     *                 not a resource.
516     *
517     * @return Attribute fractional value multiplied by the appropriate
518     * base value, or defValue if not defined.
519     */
520    @Override
521    public float getFraction(int index, int base, int pbase, float defValue) {
522        if (mData[index] == null) {
523            return defValue;
524        }
525
526        String value = mData[index].getValue();
527        if (value == null) {
528            return defValue;
529        }
530
531        if (ResourceHelper.stringToFloat(value, mValue)) {
532            return mValue.getFraction(base, pbase);
533        }
534
535        // looks like we were unable to resolve the fraction value
536        mContext.getLogger().warning(String.format(
537                "Unable to resolve fraction value \"%1$s\" in attribute \"%2$s\"",
538                value, mNames[index]));
539
540        return defValue;
541    }
542
543    /**
544     * Retrieve the resource identifier for the attribute at
545     * <var>index</var>.  Note that attribute resource as resolved when
546     * the overall {@link TypedArray} object is retrieved.  As a
547     * result, this function will return the resource identifier of the
548     * final resource value that was found, <em>not</em> necessarily the
549     * original resource that was specified by the attribute.
550     *
551     * @param index Index of attribute to retrieve.
552     * @param defValue Value to return if the attribute is not defined or
553     *                 not a resource.
554     *
555     * @return Attribute resource identifier, or defValue if not defined.
556     */
557    @Override
558    public int getResourceId(int index, int defValue) {
559        // get the IResource for this index
560        IResourceValue resValue = mData[index];
561
562        // no data, return the default value.
563        if (resValue == null) {
564            return defValue;
565        }
566
567        // check if this is a style resource
568        if (resValue instanceof IStyleResourceValue) {
569            // get the id that will represent this style.
570            return mContext.getDynamicIdByStyle((IStyleResourceValue)resValue);
571        }
572
573        // if the attribute was a reference to an id, and not a declaration of an id (@+id), then
574        // the xml attribute value was "resolved" which leads us to a IResourceValue with
575        // getType() returning "id" and getName() returning the id name
576        // (and getValue() returning null!). We need to handle this!
577        if (resValue.getType() != null && resValue.getType().equals(BridgeConstants.RES_ID)) {
578            // if this is a framework id
579            if (mPlatformFile || resValue.isFramework()) {
580                // look for idName in the android R classes
581                return mContext.getFrameworkIdValue(resValue.getName(), defValue);
582            }
583
584            // look for idName in the project R class.
585            return mContext.getProjectIdValue(resValue.getName(), defValue);
586        }
587
588        // else, try to get the value, and resolve it somehow.
589        String value = resValue.getValue();
590        if (value == null) {
591            return defValue;
592        }
593
594        // if the value is just an integer, return it.
595        try {
596            int i = Integer.parseInt(value);
597            if (Integer.toString(i).equals(value)) {
598                return i;
599            }
600        } catch (NumberFormatException e) {
601            // pass
602        }
603
604        // Handle the @id/<name>, @+id/<name> and @android:id/<name>
605        // We need to return the exact value that was compiled (from the various R classes),
606        // as these values can be reused internally with calls to findViewById().
607        // There's a trick with platform layouts that not use "android:" but their IDs are in
608        // fact in the android.R and com.android.internal.R classes.
609        // The field mPlatformFile will indicate that all IDs are to be looked up in the android R
610        // classes exclusively.
611
612        // if this is a reference to an id, find it.
613        if (value.startsWith("@id/") || value.startsWith("@+") ||
614                value.startsWith("@android:id/")) {
615
616            int pos = value.indexOf('/');
617            String idName = value.substring(pos + 1);
618
619            // if this is a framework id
620            if (mPlatformFile || value.startsWith("@android") || value.startsWith("@+android")) {
621                // look for idName in the android R classes
622                return mContext.getFrameworkIdValue(idName, defValue);
623            }
624
625            // look for idName in the project R class.
626            return mContext.getProjectIdValue(idName, defValue);
627        }
628
629        // not a direct id valid reference? resolve it
630        Integer idValue = null;
631
632        if (resValue.isFramework()) {
633            idValue = Bridge.getResourceValue(resValue.getType(), resValue.getName());
634        } else {
635            idValue = mContext.getProjectCallback().getResourceValue(
636                    resValue.getType(), resValue.getName());
637        }
638
639        if (idValue != null) {
640            return idValue.intValue();
641        }
642
643        mContext.getLogger().warning(String.format(
644                "Unable to resolve id \"%1$s\" for attribute \"%2$s\"", value, mNames[index]));
645        return defValue;
646    }
647
648    /**
649     * Retrieve the Drawable for the attribute at <var>index</var>.  This
650     * gets the resource ID of the selected attribute, and uses
651     * {@link Resources#getDrawable Resources.getDrawable} of the owning
652     * Resources object to retrieve its Drawable.
653     *
654     * @param index Index of attribute to retrieve.
655     *
656     * @return Drawable for the attribute, or null if not defined.
657     */
658    @Override
659    public Drawable getDrawable(int index) {
660        if (mData[index] == null) {
661            return null;
662        }
663
664        IResourceValue value = mData[index];
665        String stringValue = value.getValue();
666        if (stringValue == null || BridgeConstants.REFERENCE_NULL.equals(stringValue)) {
667            return null;
668        }
669
670        Drawable d = ResourceHelper.getDrawable(value, mContext, mData[index].isFramework());
671
672        if (d != null) {
673            return d;
674        }
675
676        // looks like we were unable to resolve the drawable
677        mContext.getLogger().warning(String.format(
678                "Unable to resolve drawable \"%1$s\" in attribute \"%2$s\"", stringValue,
679                mNames[index]));
680
681        return null;
682    }
683
684
685    /**
686     * Retrieve the CharSequence[] for the attribute at <var>index</var>.
687     * This gets the resource ID of the selected attribute, and uses
688     * {@link Resources#getTextArray Resources.getTextArray} of the owning
689     * Resources object to retrieve its String[].
690     *
691     * @param index Index of attribute to retrieve.
692     *
693     * @return CharSequence[] for the attribute, or null if not defined.
694     */
695    @Override
696    public CharSequence[] getTextArray(int index) {
697        if (mData[index] == null) {
698            return null;
699        }
700
701        String value = mData[index].getValue();
702        if (value != null) {
703            return new CharSequence[] { value };
704        }
705
706        mContext.getLogger().warning(String.format(
707                String.format("Unknown value for getTextArray(%d) => %s", //DEBUG
708                index, mData[index].getName())));
709
710        return null;
711    }
712
713    /**
714     * Retrieve the raw TypedValue for the attribute at <var>index</var>.
715     *
716     * @param index Index of attribute to retrieve.
717     * @param outValue TypedValue object in which to place the attribute's
718     *                 data.
719     *
720     * @return Returns true if the value was retrieved, else false.
721     */
722    @Override
723    public boolean getValue(int index, TypedValue outValue) {
724        if (mData[index] == null) {
725            return false;
726        }
727
728        String s = mData[index].getValue();
729
730        return ResourceHelper.stringToFloat(s, outValue);
731    }
732
733    /**
734     * Determines whether there is an attribute at <var>index</var>.
735     *
736     * @param index Index of attribute to retrieve.
737     *
738     * @return True if the attribute has a value, false otherwise.
739     */
740    @Override
741    public boolean hasValue(int index) {
742        return mData[index] != null;
743    }
744
745    /**
746     * Retrieve the raw TypedValue for the attribute at <var>index</var>
747     * and return a temporary object holding its data.  This object is only
748     * valid until the next call on to {@link TypedArray}.
749     *
750     * @param index Index of attribute to retrieve.
751     *
752     * @return Returns a TypedValue object if the attribute is defined,
753     *         containing its data; otherwise returns null.  (You will not
754     *         receive a TypedValue whose type is TYPE_NULL.)
755     */
756    @Override
757    public TypedValue peekValue(int index) {
758        if (getValue(index, mValue)) {
759            return mValue;
760        }
761
762        return null;
763    }
764
765    /**
766     * Returns a message about the parser state suitable for printing error messages.
767     */
768    @Override
769    public String getPositionDescription() {
770        return "<internal -- stub if needed>";
771    }
772
773    /**
774     * Give back a previously retrieved StyledAttributes, for later re-use.
775     */
776    @Override
777    public void recycle() {
778        // pass
779    }
780
781    @Override
782    public boolean getValueAt(int index, TypedValue outValue) {
783        // pass
784        return false;
785    }
786
787    @Override
788    public String toString() {
789        return mData.toString();
790    }
791 }
792