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