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