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 
17 package android.content.res;
18 
19 import com.android.ide.common.rendering.api.IProjectCallback;
20 import com.android.ide.common.rendering.api.LayoutLog;
21 import com.android.ide.common.rendering.api.ResourceValue;
22 import com.android.layoutlib.bridge.Bridge;
23 import com.android.layoutlib.bridge.BridgeConstants;
24 import com.android.layoutlib.bridge.android.BridgeContext;
25 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
26 import com.android.layoutlib.bridge.impl.ParserFactory;
27 import com.android.layoutlib.bridge.impl.ResourceHelper;
28 import com.android.ninepatch.NinePatch;
29 import com.android.resources.ResourceType;
30 import com.android.util.Pair;
31 
32 import org.xmlpull.v1.XmlPullParser;
33 import org.xmlpull.v1.XmlPullParserException;
34 
35 import android.graphics.drawable.Drawable;
36 import android.util.AttributeSet;
37 import android.util.DisplayMetrics;
38 import android.util.TypedValue;
39 import android.view.ViewGroup.LayoutParams;
40 
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileNotFoundException;
44 import java.io.InputStream;
45 
46 /**
47  *
48  */
49 public 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;
NinePatchInputStream(File file)64         public NinePatchInputStream(File file) throws FileNotFoundException {
65             super(file);
66         }
67 
68         @Override
markSupported()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 
disableFakeMarkSupport()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      */
initSystem(BridgeContext context, AssetManager assets, DisplayMetrics metrics, Configuration config, IProjectCallback projectCallback)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      */
disposeSystem()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 
BridgeResources(BridgeContext context, AssetManager assets, DisplayMetrics metrics, Configuration config, IProjectCallback projectCallback)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 
newTypeArray(int numEntries, boolean platformFile)124     public BridgeTypedArray newTypeArray(int numEntries, boolean platformFile) {
125         return new BridgeTypedArray(this, mContext, numEntries, platformFile);
126     }
127 
getResourceValue(int id, boolean[] platformResFlag_out)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
getDrawable(int id)157     public Drawable getDrawable(int id) throws NotFoundException {
158         return getDrawable(id, null);
159     }
160 
161     @Override
getDrawable(int id, Theme theme)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
getColor(int id)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
getColorStateList(int id)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
getText(int id)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
getLayout(int id)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
getAnimation(int id)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
obtainAttributes(AttributeSet set, int[] attrs)318     public TypedArray obtainAttributes(AttributeSet set, int[] attrs) {
319         return mContext.obtainStyledAttributes(set, attrs);
320     }
321 
322     @Override
obtainTypedArray(int id)323     public TypedArray obtainTypedArray(int id) throws NotFoundException {
324         throw new UnsupportedOperationException();
325     }
326 
327 
328     @Override
getDimension(int id)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
getDimensionPixelOffset(int id)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
getDimensionPixelSize(int id)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
getInteger(int id)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
getBoolean(int id)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
getResourceEntryName(int resid)474     public String getResourceEntryName(int resid) throws NotFoundException {
475         throw new UnsupportedOperationException();
476     }
477 
478     @Override
getResourceName(int resid)479     public String getResourceName(int resid) throws NotFoundException {
480         throw new UnsupportedOperationException();
481     }
482 
483     @Override
getResourceTypeName(int resid)484     public String getResourceTypeName(int resid) throws NotFoundException {
485         throw new UnsupportedOperationException();
486     }
487 
488     @Override
getString(int id, Object... formatArgs)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
getString(int id)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
getValue(int id, TypedValue outValue, boolean resolveRefs)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
getValue(String name, TypedValue outValue, boolean resolveRefs)544     public void getValue(String name, TypedValue outValue, boolean resolveRefs)
545             throws NotFoundException {
546         throw new UnsupportedOperationException();
547     }
548 
549     @Override
getXml(int id)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
loadXmlResourceParser(String file, int id, int assetCookie, String type)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
openRawResource(int id)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
openRawResource(int id, TypedValue value)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
openRawResourceFd(int id)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      */
throwException(int id)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