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.android;
18
19import com.android.ide.common.rendering.api.IProjectCallback;
20import com.android.ide.common.rendering.api.LayoutLog;
21import com.android.ide.common.rendering.api.ResourceValue;
22import com.android.layoutlib.bridge.Bridge;
23import com.android.layoutlib.bridge.BridgeConstants;
24import com.android.layoutlib.bridge.impl.ResourceHelper;
25import com.android.ninepatch.NinePatch;
26import com.android.resources.ResourceType;
27import com.android.util.Pair;
28
29import org.kxml2.io.KXmlParser;
30import org.xmlpull.v1.XmlPullParser;
31import org.xmlpull.v1.XmlPullParserException;
32
33import android.content.res.AssetFileDescriptor;
34import android.content.res.AssetManager;
35import android.content.res.ColorStateList;
36import android.content.res.Configuration;
37import android.content.res.Resources;
38import android.content.res.TypedArray;
39import android.content.res.XmlResourceParser;
40import android.graphics.drawable.Drawable;
41import android.util.AttributeSet;
42import android.util.DisplayMetrics;
43import android.util.TypedValue;
44import android.view.ViewGroup.LayoutParams;
45
46import java.io.File;
47import java.io.FileInputStream;
48import java.io.FileNotFoundException;
49import java.io.InputStream;
50
51/**
52 *
53 */
54public final class BridgeResources extends Resources {
55
56    private BridgeContext mContext;
57    private IProjectCallback mProjectCallback;
58    private boolean[] mPlatformResourceFlag = new boolean[1];
59
60    /**
61     * Simpler wrapper around FileInputStream. This is used when the input stream represent
62     * not a normal bitmap but a nine patch.
63     * This is useful when the InputStream is created in a method but used in another that needs
64     * to know whether this is 9-patch or not, such as BitmapFactory.
65     */
66    public class NinePatchInputStream extends FileInputStream {
67        private boolean mFakeMarkSupport = true;
68        public NinePatchInputStream(File file) throws FileNotFoundException {
69            super(file);
70        }
71
72        @Override
73        public boolean markSupported() {
74            if (mFakeMarkSupport) {
75                // this is needed so that BitmapFactory doesn't wrap this in a BufferedInputStream.
76                return true;
77            }
78
79            return super.markSupported();
80        }
81
82        public void disableFakeMarkSupport() {
83            // disable fake mark support so that in case codec actually try to use them
84            // we don't lie to them.
85            mFakeMarkSupport = false;
86        }
87    }
88
89    /**
90     * This initializes the static field {@link Resources#mSystem} which is used
91     * by methods who get global resources using {@link Resources#getSystem()}.
92     * <p/>
93     * They will end up using our bridge resources.
94     * <p/>
95     * {@link Bridge} calls this method after setting up a new bridge.
96     */
97    /*package*/ static Resources initSystem(BridgeContext context,
98            AssetManager assets,
99            DisplayMetrics metrics,
100            Configuration config,
101            IProjectCallback projectCallback) {
102        return Resources.mSystem = new BridgeResources(context,
103                assets,
104                metrics,
105                config,
106                projectCallback);
107    }
108
109    /**
110     * Disposes the static {@link Resources#mSystem} to make sure we don't leave objects
111     * around that would prevent us from unloading the library.
112     */
113    /*package*/ static void disposeSystem() {
114        if (Resources.mSystem instanceof BridgeResources) {
115            ((BridgeResources)(Resources.mSystem)).mContext = null;
116            ((BridgeResources)(Resources.mSystem)).mProjectCallback = null;
117        }
118        Resources.mSystem = null;
119    }
120
121    private BridgeResources(BridgeContext context, AssetManager assets, DisplayMetrics metrics,
122            Configuration config, IProjectCallback projectCallback) {
123        super(assets, metrics, config);
124        mContext = context;
125        mProjectCallback = projectCallback;
126    }
127
128    public BridgeTypedArray newTypeArray(int numEntries, boolean platformFile) {
129        return new BridgeTypedArray(this, mContext, numEntries, platformFile);
130    }
131
132    private ResourceValue getResourceValue(int id, boolean[] platformResFlag_out) {
133        // first get the String related to this id in the framework
134        Pair<ResourceType, String> resourceInfo = Bridge.resolveResourceId(id);
135
136        if (resourceInfo != null) {
137            platformResFlag_out[0] = true;
138            return mContext.getRenderResources().getFrameworkResource(
139                    resourceInfo.getFirst(), resourceInfo.getSecond());
140        }
141
142        // didn't find a match in the framework? look in the project.
143        if (mProjectCallback != null) {
144            resourceInfo = mProjectCallback.resolveResourceId(id);
145
146            if (resourceInfo != null) {
147                platformResFlag_out[0] = false;
148                return mContext.getRenderResources().getProjectResource(
149                        resourceInfo.getFirst(), resourceInfo.getSecond());
150            }
151        }
152
153        return null;
154    }
155
156    @Override
157    public Drawable getDrawable(int id) throws NotFoundException {
158        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
159
160        if (value != null) {
161            return ResourceHelper.getDrawable(value, mContext);
162        }
163
164        // id was not found or not resolved. Throw a NotFoundException.
165        throwException(id);
166
167        // this is not used since the method above always throws
168        return null;
169    }
170
171    @Override
172    public int getColor(int id) throws NotFoundException {
173        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
174
175        if (value != null) {
176            try {
177                return ResourceHelper.getColor(value.getValue());
178            } catch (NumberFormatException e) {
179                Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, e.getMessage(), e,
180                        null /*data*/);
181                return 0;
182            }
183        }
184
185        // id was not found or not resolved. Throw a NotFoundException.
186        throwException(id);
187
188        // this is not used since the method above always throws
189        return 0;
190    }
191
192    @Override
193    public ColorStateList getColorStateList(int id) throws NotFoundException {
194        ResourceValue resValue = getResourceValue(id, mPlatformResourceFlag);
195
196        if (resValue != null) {
197            ColorStateList stateList = ResourceHelper.getColorStateList(resValue, mContext);
198            if (stateList != null) {
199                return stateList;
200            }
201        }
202
203        // id was not found or not resolved. Throw a NotFoundException.
204        throwException(id);
205
206        // this is not used since the method above always throws
207        return null;
208    }
209
210    @Override
211    public CharSequence getText(int id) throws NotFoundException {
212        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
213
214        if (value != null) {
215            return value.getValue();
216        }
217
218        // id was not found or not resolved. Throw a NotFoundException.
219        throwException(id);
220
221        // this is not used since the method above always throws
222        return null;
223    }
224
225    @Override
226    public XmlResourceParser getLayout(int id) throws NotFoundException {
227        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
228
229        if (value != null) {
230            XmlPullParser parser = null;
231
232            try {
233                // check if the current parser can provide us with a custom parser.
234                if (mPlatformResourceFlag[0] == false) {
235                    parser = mProjectCallback.getParser(value.getName());
236                }
237
238                // create a new one manually if needed.
239                if (parser == null) {
240                    File xml = new File(value.getValue());
241                    if (xml.isFile()) {
242                        // we need to create a pull parser around the layout XML file, and then
243                        // give that to our XmlBlockParser
244                        parser = new KXmlParser();
245                        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
246                        parser.setInput(new FileInputStream(xml), "UTF-8"); //$NON-NLS-1$);
247                    }
248                }
249
250                if (parser != null) {
251                    return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]);
252                }
253            } catch (XmlPullParserException e) {
254                Bridge.getLog().error(LayoutLog.TAG_BROKEN,
255                        "Failed to configure parser for " + value.getValue(), e, null /*data*/);
256                // we'll return null below.
257            } catch (FileNotFoundException e) {
258                // this shouldn't happen since we check above.
259            }
260
261        }
262
263        // id was not found or not resolved. Throw a NotFoundException.
264        throwException(id);
265
266        // this is not used since the method above always throws
267        return null;
268    }
269
270    @Override
271    public XmlResourceParser getAnimation(int id) throws NotFoundException {
272        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
273
274        if (value != null) {
275            XmlPullParser parser = null;
276
277            try {
278                File xml = new File(value.getValue());
279                if (xml.isFile()) {
280                    // we need to create a pull parser around the layout XML file, and then
281                    // give that to our XmlBlockParser
282                    parser = new KXmlParser();
283                    parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
284                    parser.setInput(new FileInputStream(xml), "UTF-8"); //$NON-NLS-1$);
285
286                    return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]);
287                }
288            } catch (XmlPullParserException e) {
289                Bridge.getLog().error(LayoutLog.TAG_BROKEN,
290                        "Failed to configure parser for " + value.getValue(), e, null /*data*/);
291                // we'll return null below.
292            } catch (FileNotFoundException e) {
293                // this shouldn't happen since we check above.
294            }
295
296        }
297
298        // id was not found or not resolved. Throw a NotFoundException.
299        throwException(id);
300
301        // this is not used since the method above always throws
302        return null;
303    }
304
305    @Override
306    public TypedArray obtainAttributes(AttributeSet set, int[] attrs) {
307        return mContext.obtainStyledAttributes(set, attrs);
308    }
309
310    @Override
311    public TypedArray obtainTypedArray(int id) throws NotFoundException {
312        throw new UnsupportedOperationException();
313    }
314
315
316    @Override
317    public float getDimension(int id) throws NotFoundException {
318        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
319
320        if (value != null) {
321            String v = value.getValue();
322
323            if (v != null) {
324                if (v.equals(BridgeConstants.MATCH_PARENT) ||
325                        v.equals(BridgeConstants.FILL_PARENT)) {
326                    return LayoutParams.MATCH_PARENT;
327                } else if (v.equals(BridgeConstants.WRAP_CONTENT)) {
328                    return LayoutParams.WRAP_CONTENT;
329                }
330
331                if (ResourceHelper.stringToFloat(v, mTmpValue) &&
332                        mTmpValue.type == TypedValue.TYPE_DIMENSION) {
333                    return mTmpValue.getDimension(mMetrics);
334                }
335            }
336        }
337
338        // id was not found or not resolved. Throw a NotFoundException.
339        throwException(id);
340
341        // this is not used since the method above always throws
342        return 0;
343    }
344
345    @Override
346    public int getDimensionPixelOffset(int id) throws NotFoundException {
347        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
348
349        if (value != null) {
350            String v = value.getValue();
351
352            if (v != null) {
353                if (ResourceHelper.stringToFloat(v, mTmpValue) &&
354                        mTmpValue.type == TypedValue.TYPE_DIMENSION) {
355                    return TypedValue.complexToDimensionPixelOffset(mTmpValue.data, mMetrics);
356                }
357            }
358        }
359
360        // id was not found or not resolved. Throw a NotFoundException.
361        throwException(id);
362
363        // this is not used since the method above always throws
364        return 0;
365    }
366
367    @Override
368    public int getDimensionPixelSize(int id) throws NotFoundException {
369        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
370
371        if (value != null) {
372            String v = value.getValue();
373
374            if (v != null) {
375                if (ResourceHelper.stringToFloat(v, mTmpValue) &&
376                        mTmpValue.type == TypedValue.TYPE_DIMENSION) {
377                    return TypedValue.complexToDimensionPixelSize(mTmpValue.data, mMetrics);
378                }
379            }
380        }
381
382        // id was not found or not resolved. Throw a NotFoundException.
383        throwException(id);
384
385        // this is not used since the method above always throws
386        return 0;
387    }
388
389    @Override
390    public int getInteger(int id) throws NotFoundException {
391        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
392
393        if (value != null && value.getValue() != null) {
394            String v = value.getValue();
395            int radix = 10;
396            if (v.startsWith("0x")) {
397                v = v.substring(2);
398                radix = 16;
399            }
400            try {
401                return Integer.parseInt(v, radix);
402            } catch (NumberFormatException e) {
403                // return exception below
404            }
405        }
406
407        // id was not found or not resolved. Throw a NotFoundException.
408        throwException(id);
409
410        // this is not used since the method above always throws
411        return 0;
412    }
413
414    @Override
415    public String getResourceEntryName(int resid) throws NotFoundException {
416        throw new UnsupportedOperationException();
417    }
418
419    @Override
420    public String getResourceName(int resid) throws NotFoundException {
421        throw new UnsupportedOperationException();
422    }
423
424    @Override
425    public String getResourceTypeName(int resid) throws NotFoundException {
426        throw new UnsupportedOperationException();
427    }
428
429    @Override
430    public String getString(int id, Object... formatArgs) throws NotFoundException {
431        String s = getString(id);
432        if (s != null) {
433            return String.format(s, formatArgs);
434
435        }
436
437        // id was not found or not resolved. Throw a NotFoundException.
438        throwException(id);
439
440        // this is not used since the method above always throws
441        return null;
442    }
443
444    @Override
445    public String getString(int id) throws NotFoundException {
446        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
447
448        if (value != null && value.getValue() != null) {
449            return value.getValue();
450        }
451
452        // id was not found or not resolved. Throw a NotFoundException.
453        throwException(id);
454
455        // this is not used since the method above always throws
456        return null;
457    }
458
459    @Override
460    public void getValue(int id, TypedValue outValue, boolean resolveRefs)
461            throws NotFoundException {
462        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
463
464        if (value != null) {
465            String v = value.getValue();
466
467            if (v != null) {
468                if (ResourceHelper.stringToFloat(v, outValue)) {
469                    return;
470                }
471
472                // else it's a string
473                outValue.type = TypedValue.TYPE_STRING;
474                outValue.string = v;
475                return;
476            }
477        }
478
479        // id was not found or not resolved. Throw a NotFoundException.
480        throwException(id);
481    }
482
483    @Override
484    public void getValue(String name, TypedValue outValue, boolean resolveRefs)
485            throws NotFoundException {
486        throw new UnsupportedOperationException();
487    }
488
489    @Override
490    public XmlResourceParser getXml(int id) throws NotFoundException {
491        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
492
493        if (value != null) {
494            String v = value.getValue();
495
496            if (v != null) {
497                // check this is a file
498                File f = new File(value.getValue());
499                if (f.isFile()) {
500                    try {
501                        KXmlParser parser = new KXmlParser();
502                        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
503                        parser.setInput(new FileInputStream(f), "UTF-8"); //$NON-NLS-1$);
504
505                        return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]);
506                    } catch (XmlPullParserException e) {
507                        NotFoundException newE = new NotFoundException();
508                        newE.initCause(e);
509                        throw newE;
510                    } catch (FileNotFoundException e) {
511                        NotFoundException newE = new NotFoundException();
512                        newE.initCause(e);
513                        throw newE;
514                    }
515                }
516            }
517        }
518
519        // id was not found or not resolved. Throw a NotFoundException.
520        throwException(id);
521
522        // this is not used since the method above always throws
523        return null;
524    }
525
526    @Override
527    public XmlResourceParser loadXmlResourceParser(String file, int id,
528            int assetCookie, String type) throws NotFoundException {
529        // even though we know the XML file to load directly, we still need to resolve the
530        // id so that we can know if it's a platform or project resource.
531        // (mPlatformResouceFlag will get the result and will be used later).
532        getResourceValue(id, mPlatformResourceFlag);
533
534        File f = new File(file);
535        try {
536            KXmlParser parser = new KXmlParser();
537            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
538            parser.setInput(new FileInputStream(f), "UTF-8"); //$NON-NLS-1$);
539
540            return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]);
541        } catch (XmlPullParserException e) {
542            NotFoundException newE = new NotFoundException();
543            newE.initCause(e);
544            throw newE;
545        } catch (FileNotFoundException e) {
546            NotFoundException newE = new NotFoundException();
547            newE.initCause(e);
548            throw newE;
549        }
550    }
551
552
553    @Override
554    public InputStream openRawResource(int id) throws NotFoundException {
555        ResourceValue value = getResourceValue(id, mPlatformResourceFlag);
556
557        if (value != null) {
558            String path = value.getValue();
559
560            if (path != null) {
561                // check this is a file
562                File f = new File(path);
563                if (f.isFile()) {
564                    try {
565                        // if it's a nine-patch return a custom input stream so that
566                        // other methods (mainly bitmap factory) can detect it's a 9-patch
567                        // and actually load it as a 9-patch instead of a normal bitmap
568                        if (path.toLowerCase().endsWith(NinePatch.EXTENSION_9PATCH)) {
569                            return new NinePatchInputStream(f);
570                        }
571                        return new FileInputStream(f);
572                    } catch (FileNotFoundException e) {
573                        NotFoundException newE = new NotFoundException();
574                        newE.initCause(e);
575                        throw newE;
576                    }
577                }
578            }
579        }
580
581        // id was not found or not resolved. Throw a NotFoundException.
582        throwException(id);
583
584        // this is not used since the method above always throws
585        return null;
586    }
587
588    @Override
589    public InputStream openRawResource(int id, TypedValue value) throws NotFoundException {
590        getValue(id, value, true);
591
592        String path = value.string.toString();
593
594        File f = new File(path);
595        if (f.isFile()) {
596            try {
597                // if it's a nine-patch return a custom input stream so that
598                // other methods (mainly bitmap factory) can detect it's a 9-patch
599                // and actually load it as a 9-patch instead of a normal bitmap
600                if (path.toLowerCase().endsWith(NinePatch.EXTENSION_9PATCH)) {
601                    return new NinePatchInputStream(f);
602                }
603                return new FileInputStream(f);
604            } catch (FileNotFoundException e) {
605                NotFoundException exception = new NotFoundException();
606                exception.initCause(e);
607                throw exception;
608            }
609        }
610
611        throw new NotFoundException();
612    }
613
614    @Override
615    public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException {
616        throw new UnsupportedOperationException();
617    }
618
619    /**
620     * Builds and throws a {@link Resources.NotFoundException} based on a resource id and a resource type.
621     * @param id the id of the resource
622     * @throws NotFoundException
623     */
624    private void throwException(int id) throws NotFoundException {
625        // first get the String related to this id in the framework
626        Pair<ResourceType, String> resourceInfo = Bridge.resolveResourceId(id);
627
628        // if the name is unknown in the framework, get it from the custom view loader.
629        if (resourceInfo == null && mProjectCallback != null) {
630            resourceInfo = mProjectCallback.resolveResourceId(id);
631        }
632
633        String message = null;
634        if (resourceInfo != null) {
635            message = String.format(
636                    "Could not find %1$s resource matching value 0x%2$X (resolved name: %3$s) in current configuration.",
637                    resourceInfo.getFirst(), id, resourceInfo.getSecond());
638        } else {
639            message = String.format(
640                    "Could not resolve resource value: 0x%1$X.", id);
641        }
642
643        throw new NotFoundException(message);
644    }
645}
646