1/*
2 * Copyright (C) 2016 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.SdkConstants;
20import com.android.ide.common.rendering.api.ArrayResourceValue;
21import com.android.ide.common.rendering.api.DensityBasedResourceValue;
22import com.android.ide.common.rendering.api.LayoutLog;
23import com.android.ide.common.rendering.api.LayoutlibCallback;
24import com.android.ide.common.rendering.api.PluralsResourceValue;
25import com.android.ide.common.rendering.api.RenderResources;
26import com.android.ide.common.rendering.api.ResourceValue;
27import com.android.layoutlib.bridge.Bridge;
28import com.android.layoutlib.bridge.BridgeConstants;
29import com.android.layoutlib.bridge.android.BridgeContext;
30import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
31import com.android.layoutlib.bridge.impl.ParserFactory;
32import com.android.layoutlib.bridge.impl.ResourceHelper;
33import com.android.layoutlib.bridge.util.NinePatchInputStream;
34import com.android.ninepatch.NinePatch;
35import com.android.resources.ResourceType;
36import com.android.resources.ResourceUrl;
37import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
38import com.android.tools.layoutlib.annotations.VisibleForTesting;
39import com.android.util.Pair;
40
41import org.xmlpull.v1.XmlPullParser;
42import org.xmlpull.v1.XmlPullParserException;
43
44import android.annotation.NonNull;
45import android.annotation.Nullable;
46import android.content.res.Resources.NotFoundException;
47import android.content.res.Resources.Theme;
48import android.graphics.Color;
49import android.graphics.Typeface;
50import android.graphics.drawable.Drawable;
51import android.icu.text.PluralRules;
52import android.util.AttributeSet;
53import android.util.DisplayMetrics;
54import android.util.LruCache;
55import android.util.TypedValue;
56import android.view.DisplayAdjustments;
57import android.view.ViewGroup.LayoutParams;
58
59import java.io.File;
60import java.io.FileInputStream;
61import java.io.FileNotFoundException;
62import java.io.InputStream;
63import java.util.Iterator;
64import java.util.Objects;
65import java.util.WeakHashMap;
66
67import static com.android.SdkConstants.ANDROID_PKG;
68import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
69
70@SuppressWarnings("deprecation")
71public class Resources_Delegate {
72    private static WeakHashMap<Resources, LayoutlibCallback> sLayoutlibCallbacks = new
73            WeakHashMap<>();
74    private static WeakHashMap<Resources, BridgeContext> sContexts = new
75            WeakHashMap<>();
76
77    private static boolean[] mPlatformResourceFlag = new boolean[1];
78    // TODO: This cache is cleared every time a render session is disposed. Look into making this
79    // more long lived.
80    private static LruCache<String, Drawable.ConstantState> sDrawableCache = new LruCache<>(50);
81
82    public static Resources initSystem(@NonNull BridgeContext context,
83            @NonNull AssetManager assets,
84            @NonNull DisplayMetrics metrics,
85            @NonNull Configuration config,
86            @NonNull LayoutlibCallback layoutlibCallback) {
87        assert Resources.mSystem == null  :
88                "Resources_Delegate.initSystem called twice before disposeSystem was called";
89        Resources resources = new Resources(Resources_Delegate.class.getClassLoader());
90        resources.setImpl(new ResourcesImpl(assets, metrics, config, new DisplayAdjustments()));
91        sContexts.put(resources, Objects.requireNonNull(context));
92        sLayoutlibCallbacks.put(resources, Objects.requireNonNull(layoutlibCallback));
93        return Resources.mSystem = resources;
94    }
95
96    /** Returns the {@link BridgeContext} associated to the given {@link Resources} */
97    @VisibleForTesting
98    @NonNull
99    public static BridgeContext getContext(@NonNull Resources resources) {
100        assert sContexts.containsKey(resources) :
101                "Resources_Delegate.getContext called before initSystem";
102        return sContexts.get(resources);
103    }
104
105    /** Returns the {@link LayoutlibCallback} associated to the given {@link Resources} */
106    @VisibleForTesting
107    @NonNull
108    public static LayoutlibCallback getLayoutlibCallback(@NonNull Resources resources) {
109        assert sLayoutlibCallbacks.containsKey(resources) :
110                "Resources_Delegate.getLayoutlibCallback called before initSystem";
111        return sLayoutlibCallbacks.get(resources);
112    }
113
114    /**
115     * Disposes the static {@link Resources#mSystem} to make sure we don't leave objects around that
116     * would prevent us from unloading the library.
117     */
118    public static void disposeSystem() {
119        sDrawableCache.evictAll();
120        sContexts.clear();
121        sLayoutlibCallbacks.clear();
122        Resources.mSystem = null;
123    }
124
125    public static BridgeTypedArray newTypeArray(Resources resources, int numEntries,
126            boolean platformFile) {
127        return new BridgeTypedArray(resources, getContext(resources), numEntries, platformFile);
128    }
129
130    private static Pair<ResourceType, String> getResourceInfo(Resources resources, int id,
131            boolean[] platformResFlag_out) {
132        // first get the String related to this id in the framework
133        Pair<ResourceType, String> resourceInfo = Bridge.resolveResourceId(id);
134
135        assert Resources.mSystem != null : "Resources_Delegate.initSystem wasn't called";
136        // Set the layoutlib callback and context for resources
137        if (resources != Resources.mSystem &&
138                (!sContexts.containsKey(resources) || !sLayoutlibCallbacks.containsKey(resources))) {
139            sLayoutlibCallbacks.put(resources, getLayoutlibCallback(Resources.mSystem));
140            sContexts.put(resources, getContext(Resources.mSystem));
141        }
142
143        if (resourceInfo != null) {
144            platformResFlag_out[0] = true;
145            return resourceInfo;
146        }
147
148        // didn't find a match in the framework? look in the project.
149        resourceInfo = getLayoutlibCallback(resources).resolveResourceId(id);
150
151        if (resourceInfo != null) {
152            platformResFlag_out[0] = false;
153            return resourceInfo;
154        }
155        return null;
156    }
157
158    private static Pair<String, ResourceValue> getResourceValue(Resources resources, int id,
159            boolean[] platformResFlag_out) {
160        Pair<ResourceType, String> resourceInfo =
161                getResourceInfo(resources, id, platformResFlag_out);
162
163        if (resourceInfo != null) {
164            String attributeName = resourceInfo.getSecond();
165            RenderResources renderResources = getContext(resources).getRenderResources();
166            ResourceValue value = platformResFlag_out[0] ?
167                    renderResources.getFrameworkResource(resourceInfo.getFirst(), attributeName) :
168                    renderResources.getProjectResource(resourceInfo.getFirst(), attributeName);
169
170            if (value == null) {
171                // Unable to resolve the attribute, just leave the unresolved value
172                value = new ResourceValue(resourceInfo.getFirst(), attributeName, attributeName,
173                        platformResFlag_out[0]);
174            }
175            return Pair.of(attributeName, value);
176        }
177
178        return null;
179    }
180
181    @LayoutlibDelegate
182    static Drawable getDrawable(Resources resources, int id) {
183        return getDrawable(resources, id, null);
184    }
185
186    @LayoutlibDelegate
187    static Drawable getDrawable(Resources resources, int id, Theme theme) {
188        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
189        if (value != null) {
190            String key = value.getSecond().getValue();
191
192            Drawable.ConstantState constantState = key != null ? sDrawableCache.get(key) : null;
193            Drawable drawable;
194            if (constantState != null) {
195                drawable = constantState.newDrawable(resources, theme);
196            } else {
197                drawable =
198                        ResourceHelper.getDrawable(value.getSecond(), getContext(resources), theme);
199
200                if (key != null) {
201                    sDrawableCache.put(key, drawable.getConstantState());
202                }
203            }
204
205            return drawable;
206        }
207
208        // id was not found or not resolved. Throw a NotFoundException.
209        throwException(resources, id);
210
211        // this is not used since the method above always throws
212        return null;
213    }
214
215    @LayoutlibDelegate
216    static int getColor(Resources resources, int id) {
217        return getColor(resources, id, null);
218    }
219
220    @LayoutlibDelegate
221    static int getColor(Resources resources, int id, Theme theme) throws NotFoundException {
222        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
223
224        if (value != null) {
225            ResourceValue resourceValue = value.getSecond();
226            try {
227                return ResourceHelper.getColor(resourceValue.getValue());
228            } catch (NumberFormatException e) {
229                // Check if the value passed is a file. If it is, mostly likely, user is referencing
230                // a color state list from a place where they should reference only a pure color.
231                String message;
232                if (new File(resourceValue.getValue()).isFile()) {
233                    String resource = (resourceValue.isFramework() ? "@android:" : "@") + "color/"
234                            + resourceValue.getName();
235                    message = "Hexadecimal color expected, found Color State List for " + resource;
236                } else {
237                    message = e.getMessage();
238                }
239                Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, message, e, null);
240                return 0;
241            }
242        }
243
244        // Suppress possible NPE. getColorStateList will never return null, it will instead
245        // throw an exception, but intelliJ can't figure that out
246        //noinspection ConstantConditions
247        return getColorStateList(resources, id, theme).getDefaultColor();
248    }
249
250    @LayoutlibDelegate
251    static ColorStateList getColorStateList(Resources resources, int id) throws NotFoundException {
252        return getColorStateList(resources, id, null);
253    }
254
255    @LayoutlibDelegate
256    static ColorStateList getColorStateList(Resources resources, int id, Theme theme)
257            throws NotFoundException {
258        Pair<String, ResourceValue> resValue =
259                getResourceValue(resources, id, mPlatformResourceFlag);
260
261        if (resValue != null) {
262            ColorStateList stateList = ResourceHelper.getColorStateList(resValue.getSecond(),
263                    getContext(resources), theme);
264            if (stateList != null) {
265                return stateList;
266            }
267        }
268
269        // id was not found or not resolved. Throw a NotFoundException.
270        throwException(resources, id);
271
272        // this is not used since the method above always throws
273        return null;
274    }
275
276    @LayoutlibDelegate
277    static CharSequence getText(Resources resources, int id, CharSequence def) {
278        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
279
280        if (value != null) {
281            ResourceValue resValue = value.getSecond();
282
283            assert resValue != null;
284            if (resValue != null) {
285                String v = resValue.getValue();
286                if (v != null) {
287                    return v;
288                }
289            }
290        }
291
292        return def;
293    }
294
295    @LayoutlibDelegate
296    static CharSequence getText(Resources resources, int id) throws NotFoundException {
297        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
298
299        if (value != null) {
300            ResourceValue resValue = value.getSecond();
301
302            assert resValue != null;
303            if (resValue != null) {
304                String v = resValue.getValue();
305                if (v != null) {
306                    return v;
307                }
308            }
309        }
310
311        // id was not found or not resolved. Throw a NotFoundException.
312        throwException(resources, id);
313
314        // this is not used since the method above always throws
315        return null;
316    }
317
318    @LayoutlibDelegate
319    static CharSequence[] getTextArray(Resources resources, int id) throws NotFoundException {
320        ResourceValue resValue = getArrayResourceValue(resources, id);
321        if (resValue == null) {
322            // Error already logged by getArrayResourceValue.
323            return new CharSequence[0];
324        } else if (!(resValue instanceof ArrayResourceValue)) {
325            return new CharSequence[]{
326                    resolveReference(resources, resValue.getValue(), resValue.isFramework())};
327        }
328        ArrayResourceValue arv = ((ArrayResourceValue) resValue);
329        return fillValues(resources, arv, new CharSequence[arv.getElementCount()]);
330    }
331
332    @LayoutlibDelegate
333    static String[] getStringArray(Resources resources, int id) throws NotFoundException {
334        ResourceValue resValue = getArrayResourceValue(resources, id);
335        if (resValue == null) {
336            // Error already logged by getArrayResourceValue.
337            return new String[0];
338        } else if (!(resValue instanceof ArrayResourceValue)) {
339            return new String[]{
340                    resolveReference(resources, resValue.getValue(), resValue.isFramework())};
341        }
342        ArrayResourceValue arv = ((ArrayResourceValue) resValue);
343        return fillValues(resources, arv, new String[arv.getElementCount()]);
344    }
345
346    /**
347     * Resolve each element in resValue and copy them to {@code values}. The values copied are
348     * always Strings. The ideal signature for the method should be &lt;T super String&gt;, but java
349     * generics don't support it.
350     */
351    static <T extends CharSequence> T[] fillValues(Resources resources, ArrayResourceValue resValue,
352            T[] values) {
353        int i = 0;
354        for (Iterator<String> iterator = resValue.iterator(); iterator.hasNext(); i++) {
355            @SuppressWarnings("unchecked")
356            T s = (T) resolveReference(resources, iterator.next(), resValue.isFramework());
357            values[i] = s;
358        }
359        return values;
360    }
361
362    @LayoutlibDelegate
363    static int[] getIntArray(Resources resources, int id) throws NotFoundException {
364        ResourceValue rv = getArrayResourceValue(resources, id);
365        if (rv == null) {
366            // Error already logged by getArrayResourceValue.
367            return new int[0];
368        } else if (!(rv instanceof ArrayResourceValue)) {
369            // This is an older IDE that can only give us the first element of the array.
370            String firstValue = resolveReference(resources, rv.getValue(), rv.isFramework());
371            try {
372                return new int[]{getInt(firstValue)};
373            } catch (NumberFormatException e) {
374                Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
375                        "Integer resource array contains non-integer value: " +
376                                firstValue, null);
377                return new int[1];
378            }
379        }
380        ArrayResourceValue resValue = ((ArrayResourceValue) rv);
381        int[] values = new int[resValue.getElementCount()];
382        int i = 0;
383        for (Iterator<String> iterator = resValue.iterator(); iterator.hasNext(); i++) {
384            String element = resolveReference(resources, iterator.next(), resValue.isFramework());
385            try {
386                if (element.startsWith("#")) {
387                    // This integer represents a color (starts with #)
388                    values[i] = Color.parseColor(element);
389                } else {
390                    values[i] = getInt(element);
391                }
392            } catch (NumberFormatException e) {
393                Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
394                        "Integer resource array contains non-integer value: " + element, null);
395            } catch (IllegalArgumentException e2) {
396                Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
397                        "Integer resource array contains wrong color format: " + element, null);
398            }
399        }
400        return values;
401    }
402
403    /**
404     * Try to find the ArrayResourceValue for the given id.
405     * <p/>
406     * If the ResourceValue found is not of type {@link ResourceType#ARRAY}, the method logs an
407     * error and return null. However, if the ResourceValue found has type {@code
408     * ResourceType.ARRAY}, but the value is not an instance of {@link ArrayResourceValue}, the
409     * method returns the ResourceValue. This happens on older versions of the IDE, which did not
410     * parse the array resources properly.
411     * <p/>
412     *
413     * @throws NotFoundException if no resource if found
414     */
415    @Nullable
416    private static ResourceValue getArrayResourceValue(Resources resources, int id)
417            throws NotFoundException {
418        Pair<String, ResourceValue> v = getResourceValue(resources, id, mPlatformResourceFlag);
419
420        if (v != null) {
421            ResourceValue resValue = v.getSecond();
422
423            assert resValue != null;
424            if (resValue != null) {
425                final ResourceType type = resValue.getResourceType();
426                if (type != ResourceType.ARRAY) {
427                    Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE,
428                            String.format(
429                                    "Resource with id 0x%1$X is not an array resource, but %2$s",
430                                    id, type == null ? "null" : type.getDisplayName()),
431                            null);
432                    return null;
433                }
434                if (!(resValue instanceof ArrayResourceValue)) {
435                    Bridge.getLog().warning(LayoutLog.TAG_UNSUPPORTED,
436                            "Obtaining resource arrays via getTextArray, getStringArray or getIntArray is not fully supported in this version of the IDE.",
437                            null);
438                }
439                return resValue;
440            }
441        }
442
443        // id was not found or not resolved. Throw a NotFoundException.
444        throwException(resources, id);
445
446        // this is not used since the method above always throws
447        return null;
448    }
449
450    @NonNull
451    private static String resolveReference(Resources resources, @NonNull String ref,
452            boolean forceFrameworkOnly) {
453        if (ref.startsWith(PREFIX_RESOURCE_REF) || ref.startsWith
454                (SdkConstants.PREFIX_THEME_REF)) {
455            ResourceValue rv =
456                    getContext(resources).getRenderResources().findResValue(ref, forceFrameworkOnly);
457            rv = getContext(resources).getRenderResources().resolveResValue(rv);
458            if (rv != null) {
459                return rv.getValue();
460            }
461        }
462        // Not a reference.
463        return ref;
464    }
465
466    @LayoutlibDelegate
467    static XmlResourceParser getLayout(Resources resources, int id) throws NotFoundException {
468        Pair<String, ResourceValue> v = getResourceValue(resources, id, mPlatformResourceFlag);
469
470        if (v != null) {
471            ResourceValue value = v.getSecond();
472
473            try {
474                return ResourceHelper.getXmlBlockParser(getContext(resources), value);
475            } catch (XmlPullParserException e) {
476                Bridge.getLog().error(LayoutLog.TAG_BROKEN,
477                        "Failed to configure parser for " + value.getValue(), e, null /*data*/);
478                // we'll return null below.
479            } catch (FileNotFoundException e) {
480                // this shouldn't happen since we check above.
481            }
482
483        }
484
485        // id was not found or not resolved. Throw a NotFoundException.
486        throwException(resources, id);
487
488        // this is not used since the method above always throws
489        return null;
490    }
491
492    @LayoutlibDelegate
493    static XmlResourceParser getAnimation(Resources resources, int id) throws NotFoundException {
494        Pair<String, ResourceValue> v = getResourceValue(resources, id, mPlatformResourceFlag);
495
496        if (v != null) {
497            ResourceValue value = v.getSecond();
498
499            try {
500                return ResourceHelper.getXmlBlockParser(getContext(resources), value);
501            } catch (XmlPullParserException e) {
502                Bridge.getLog().error(LayoutLog.TAG_BROKEN,
503                        "Failed to configure parser for " + value.getValue(), e, null /*data*/);
504                // we'll return null below.
505            } catch (FileNotFoundException e) {
506                // this shouldn't happen since we check above.
507            }
508
509        }
510
511        // id was not found or not resolved. Throw a NotFoundException.
512        throwException(resources, id);
513
514        // this is not used since the method above always throws
515        return null;
516    }
517
518    @LayoutlibDelegate
519    static TypedArray obtainAttributes(Resources resources, AttributeSet set, int[] attrs) {
520        return getContext(resources).obtainStyledAttributes(set, attrs);
521    }
522
523    @LayoutlibDelegate
524    static TypedArray obtainAttributes(Resources resources, Resources.Theme theme, AttributeSet
525            set, int[] attrs) {
526        return Resources.obtainAttributes_Original(resources, theme, set, attrs);
527    }
528
529    @LayoutlibDelegate
530    static TypedArray obtainTypedArray(Resources resources, int id) throws NotFoundException {
531        throw new UnsupportedOperationException();
532    }
533
534    @LayoutlibDelegate
535    static float getDimension(Resources resources, int id) throws NotFoundException {
536        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
537
538        if (value != null) {
539            ResourceValue resValue = value.getSecond();
540
541            assert resValue != null;
542            if (resValue != null) {
543                String v = resValue.getValue();
544                if (v != null) {
545                    if (v.equals(BridgeConstants.MATCH_PARENT) ||
546                            v.equals(BridgeConstants.FILL_PARENT)) {
547                        return LayoutParams.MATCH_PARENT;
548                    } else if (v.equals(BridgeConstants.WRAP_CONTENT)) {
549                        return LayoutParams.WRAP_CONTENT;
550                    }
551                    TypedValue tmpValue = new TypedValue();
552                    if (ResourceHelper.parseFloatAttribute(
553                            value.getFirst(), v, tmpValue, true /*requireUnit*/) &&
554                            tmpValue.type == TypedValue.TYPE_DIMENSION) {
555                        return tmpValue.getDimension(resources.getDisplayMetrics());
556                    }
557                }
558            }
559        }
560
561        // id was not found or not resolved. Throw a NotFoundException.
562        throwException(resources, id);
563
564        // this is not used since the method above always throws
565        return 0;
566    }
567
568    @LayoutlibDelegate
569    static int getDimensionPixelOffset(Resources resources, int id) throws NotFoundException {
570        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
571
572        if (value != null) {
573            ResourceValue resValue = value.getSecond();
574
575            assert resValue != null;
576            if (resValue != null) {
577                String v = resValue.getValue();
578                if (v != null) {
579                    TypedValue tmpValue = new TypedValue();
580                    if (ResourceHelper.parseFloatAttribute(
581                            value.getFirst(), v, tmpValue, true /*requireUnit*/) &&
582                            tmpValue.type == TypedValue.TYPE_DIMENSION) {
583                        return TypedValue.complexToDimensionPixelOffset(tmpValue.data,
584                                resources.getDisplayMetrics());
585                    }
586                }
587            }
588        }
589
590        // id was not found or not resolved. Throw a NotFoundException.
591        throwException(resources, id);
592
593        // this is not used since the method above always throws
594        return 0;
595    }
596
597    @LayoutlibDelegate
598    static int getDimensionPixelSize(Resources resources, int id) throws NotFoundException {
599        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
600
601        if (value != null) {
602            ResourceValue resValue = value.getSecond();
603
604            assert resValue != null;
605            if (resValue != null) {
606                String v = resValue.getValue();
607                if (v != null) {
608                    TypedValue tmpValue = new TypedValue();
609                    if (ResourceHelper.parseFloatAttribute(
610                            value.getFirst(), v, tmpValue, true /*requireUnit*/) &&
611                            tmpValue.type == TypedValue.TYPE_DIMENSION) {
612                        return TypedValue.complexToDimensionPixelSize(tmpValue.data,
613                                resources.getDisplayMetrics());
614                    }
615                }
616            }
617        }
618
619        // id was not found or not resolved. Throw a NotFoundException.
620        throwException(resources, id);
621
622        // this is not used since the method above always throws
623        return 0;
624    }
625
626    @LayoutlibDelegate
627    static int getInteger(Resources resources, int id) throws NotFoundException {
628        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
629
630        if (value != null) {
631            ResourceValue resValue = value.getSecond();
632
633            assert resValue != null;
634            if (resValue != null) {
635                String v = resValue.getValue();
636                if (v != null) {
637                    try {
638                        return getInt(v);
639                    } catch (NumberFormatException e) {
640                        // return exception below
641                    }
642                }
643            }
644        }
645
646        // id was not found or not resolved. Throw a NotFoundException.
647        throwException(resources, id);
648
649        // this is not used since the method above always throws
650        return 0;
651    }
652
653    @LayoutlibDelegate
654    static float getFloat(Resources resources, int id) {
655        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
656
657        if (value != null) {
658            ResourceValue resValue = value.getSecond();
659
660            if (resValue != null) {
661                String v = resValue.getValue();
662                if (v != null) {
663                    try {
664                        return Float.parseFloat(v);
665                    } catch (NumberFormatException ignore) {
666                    }
667                }
668            }
669        }
670        return 0;
671    }
672
673    @LayoutlibDelegate
674    static boolean getBoolean(Resources resources, int id) throws NotFoundException {
675        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
676
677        if (value != null) {
678            ResourceValue resValue = value.getSecond();
679
680            if (resValue != null) {
681                String v = resValue.getValue();
682                if (v != null) {
683                    return Boolean.parseBoolean(v);
684                }
685            }
686        }
687
688        // id was not found or not resolved. Throw a NotFoundException.
689        throwException(resources, id);
690
691        // this is not used since the method above always throws
692        return false;
693    }
694
695    @LayoutlibDelegate
696    static String getResourceEntryName(Resources resources, int resid) throws NotFoundException {
697        Pair<ResourceType, String> resourceInfo = getResourceInfo(resources, resid, new boolean[1]);
698        if (resourceInfo != null) {
699            return resourceInfo.getSecond();
700        }
701        throwException(resid, null);
702        return null;
703
704    }
705
706    @LayoutlibDelegate
707    static String getResourceName(Resources resources, int resid) throws NotFoundException {
708        boolean[] platformOut = new boolean[1];
709        Pair<ResourceType, String> resourceInfo = getResourceInfo(resources, resid, platformOut);
710        String packageName;
711        if (resourceInfo != null) {
712            if (platformOut[0]) {
713                packageName = SdkConstants.ANDROID_NS_NAME;
714            } else {
715                packageName = getContext(resources).getPackageName();
716                packageName = packageName == null ? SdkConstants.APP_PREFIX : packageName;
717            }
718            return packageName + ':' + resourceInfo.getFirst().getName() + '/' +
719                    resourceInfo.getSecond();
720        }
721        throwException(resid, null);
722        return null;
723    }
724
725    @LayoutlibDelegate
726    static String getResourcePackageName(Resources resources, int resid) throws NotFoundException {
727        boolean[] platformOut = new boolean[1];
728        Pair<ResourceType, String> resourceInfo = getResourceInfo(resources, resid, platformOut);
729        if (resourceInfo != null) {
730            if (platformOut[0]) {
731                return SdkConstants.ANDROID_NS_NAME;
732            }
733            String packageName = getContext(resources).getPackageName();
734            return packageName == null ? SdkConstants.APP_PREFIX : packageName;
735        }
736        throwException(resid, null);
737        return null;
738    }
739
740    @LayoutlibDelegate
741    static String getResourceTypeName(Resources resources, int resid) throws NotFoundException {
742        Pair<ResourceType, String> resourceInfo = getResourceInfo(resources, resid, new boolean[1]);
743        if (resourceInfo != null) {
744            return resourceInfo.getFirst().getName();
745        }
746        throwException(resid, null);
747        return null;
748    }
749
750    @LayoutlibDelegate
751    static String getString(Resources resources, int id, Object... formatArgs)
752            throws NotFoundException {
753        String s = getString(resources, id);
754        if (s != null) {
755            return String.format(s, formatArgs);
756
757        }
758
759        // id was not found or not resolved. Throw a NotFoundException.
760        throwException(resources, id);
761
762        // this is not used since the method above always throws
763        return null;
764    }
765
766    @LayoutlibDelegate
767    static String getString(Resources resources, int id) throws NotFoundException {
768        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
769
770        if (value != null && value.getSecond().getValue() != null) {
771            return value.getSecond().getValue();
772        }
773
774        // id was not found or not resolved. Throw a NotFoundException.
775        throwException(resources, id);
776
777        // this is not used since the method above always throws
778        return null;
779    }
780
781    @LayoutlibDelegate
782    static String getQuantityString(Resources resources, int id, int quantity) throws
783            NotFoundException {
784        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
785
786        if (value != null) {
787            if (value.getSecond() instanceof PluralsResourceValue) {
788                PluralsResourceValue pluralsResourceValue = (PluralsResourceValue) value.getSecond();
789                PluralRules pluralRules = PluralRules.forLocale(resources.getConfiguration().getLocales()
790                        .get(0));
791                String strValue = pluralsResourceValue.getValue(pluralRules.select(quantity));
792                if (strValue == null) {
793                    strValue = pluralsResourceValue.getValue(PluralRules.KEYWORD_OTHER);
794                }
795
796                return strValue;
797            }
798            else {
799                return value.getSecond().getValue();
800            }
801        }
802
803        // id was not found or not resolved. Throw a NotFoundException.
804        throwException(resources, id);
805
806        // this is not used since the method above always throws
807        return null;
808    }
809
810    @LayoutlibDelegate
811    static String getQuantityString(Resources resources, int id, int quantity, Object... formatArgs)
812            throws NotFoundException {
813        String raw = getQuantityString(resources, id, quantity);
814        return String.format(resources.getConfiguration().getLocales().get(0), raw, formatArgs);
815    }
816
817    @LayoutlibDelegate
818    static CharSequence getQuantityText(Resources resources, int id, int quantity) throws
819            NotFoundException {
820        return getQuantityString(resources, id, quantity);
821    }
822
823    @LayoutlibDelegate
824    static Typeface getFont(Resources resources, int id) throws
825            NotFoundException {
826        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
827        if (value != null) {
828            return ResourceHelper.getFont(value.getSecond(), getContext(resources), null);
829        }
830
831        throwException(resources, id);
832
833        // this is not used since the method above always throws
834        return null;
835    }
836
837    @LayoutlibDelegate
838    static Typeface getFont(Resources resources, TypedValue outValue, int id) throws
839            NotFoundException {
840        Resources_Delegate.getValue(resources, id, outValue, true);
841        if (outValue.string != null) {
842            return ResourceHelper.getFont(outValue.string.toString(), getContext(resources), null,
843                    mPlatformResourceFlag[0]);
844        }
845
846        throwException(resources, id);
847
848        // this is not used since the method above always throws
849        return null;
850    }
851
852    @LayoutlibDelegate
853    static void getValue(Resources resources, int id, TypedValue outValue, boolean resolveRefs)
854            throws NotFoundException {
855        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
856
857        if (value != null) {
858            ResourceValue resVal = value.getSecond();
859            String v = resVal != null ? resVal.getValue() : null;
860
861            if (v != null) {
862                if (ResourceHelper.parseFloatAttribute(value.getFirst(), v, outValue,
863                        false /*requireUnit*/)) {
864                    return;
865                }
866                if (resVal instanceof DensityBasedResourceValue) {
867                    outValue.density =
868                            ((DensityBasedResourceValue) resVal).getResourceDensity().getDpiValue();
869                }
870
871                // else it's a string
872                outValue.type = TypedValue.TYPE_STRING;
873                outValue.string = v;
874                return;
875            }
876        }
877
878        // id was not found or not resolved. Throw a NotFoundException.
879        throwException(resources, id);
880    }
881
882    @LayoutlibDelegate
883    static void getValue(Resources resources, String name, TypedValue outValue, boolean resolveRefs)
884            throws NotFoundException {
885        throw new UnsupportedOperationException();
886    }
887
888    @LayoutlibDelegate
889    static void getValueForDensity(Resources resources, int id, int density, TypedValue outValue,
890            boolean resolveRefs) throws NotFoundException {
891        getValue(resources, id, outValue, resolveRefs);
892    }
893
894    @LayoutlibDelegate
895    static XmlResourceParser getXml(Resources resources, int id) throws NotFoundException {
896        Pair<String, ResourceValue> v = getResourceValue(resources, id, mPlatformResourceFlag);
897
898        if (v != null) {
899            ResourceValue value = v.getSecond();
900
901            try {
902                return ResourceHelper.getXmlBlockParser(getContext(resources), value);
903            } catch (XmlPullParserException e) {
904                Bridge.getLog().error(LayoutLog.TAG_BROKEN,
905                        "Failed to configure parser for " + value.getValue(), e, null /*data*/);
906                // we'll return null below.
907            } catch (FileNotFoundException e) {
908                // this shouldn't happen since we check above.
909            }
910        }
911
912        // id was not found or not resolved. Throw a NotFoundException.
913        throwException(resources, id);
914
915        // this is not used since the method above always throws
916        return null;
917    }
918
919    @LayoutlibDelegate
920    static XmlResourceParser loadXmlResourceParser(Resources resources, int id,
921            String type) throws NotFoundException {
922        return resources.loadXmlResourceParser_Original(id, type);
923    }
924
925    @LayoutlibDelegate
926    static XmlResourceParser loadXmlResourceParser(Resources resources, String file, int id,
927            int assetCookie, String type) throws NotFoundException {
928        // even though we know the XML file to load directly, we still need to resolve the
929        // id so that we can know if it's a platform or project resource.
930        // (mPlatformResouceFlag will get the result and will be used later).
931        getResourceValue(resources, id, mPlatformResourceFlag);
932
933        File f = new File(file);
934        try {
935            XmlPullParser parser = ParserFactory.create(f);
936
937            return new BridgeXmlBlockParser(parser, getContext(resources), mPlatformResourceFlag[0]);
938        } catch (XmlPullParserException e) {
939            NotFoundException newE = new NotFoundException();
940            newE.initCause(e);
941            throw newE;
942        } catch (FileNotFoundException e) {
943            NotFoundException newE = new NotFoundException();
944            newE.initCause(e);
945            throw newE;
946        }
947    }
948
949    @LayoutlibDelegate
950    static InputStream openRawResource(Resources resources, int id) throws NotFoundException {
951        Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
952
953        if (value != null) {
954            String path = value.getSecond().getValue();
955
956            if (path != null) {
957                // check this is a file
958                File f = new File(path);
959                if (f.isFile()) {
960                    try {
961                        // if it's a nine-patch return a custom input stream so that
962                        // other methods (mainly bitmap factory) can detect it's a 9-patch
963                        // and actually load it as a 9-patch instead of a normal bitmap
964                        if (path.toLowerCase().endsWith(NinePatch.EXTENSION_9PATCH)) {
965                            return new NinePatchInputStream(f);
966                        }
967                        return new FileInputStream(f);
968                    } catch (FileNotFoundException e) {
969                        NotFoundException newE = new NotFoundException();
970                        newE.initCause(e);
971                        throw newE;
972                    }
973                }
974            }
975        }
976
977        // id was not found or not resolved. Throw a NotFoundException.
978        throwException(resources, id);
979
980        // this is not used since the method above always throws
981        return null;
982    }
983
984    @LayoutlibDelegate
985    static InputStream openRawResource(Resources resources, int id, TypedValue value) throws
986            NotFoundException {
987        getValue(resources, id, value, true);
988
989        String path = value.string.toString();
990
991        File f = new File(path);
992        if (f.isFile()) {
993            try {
994                // if it's a nine-patch return a custom input stream so that
995                // other methods (mainly bitmap factory) can detect it's a 9-patch
996                // and actually load it as a 9-patch instead of a normal bitmap
997                if (path.toLowerCase().endsWith(NinePatch.EXTENSION_9PATCH)) {
998                    return new NinePatchInputStream(f);
999                }
1000                return new FileInputStream(f);
1001            } catch (FileNotFoundException e) {
1002                NotFoundException exception = new NotFoundException();
1003                exception.initCause(e);
1004                throw exception;
1005            }
1006        }
1007
1008        throw new NotFoundException();
1009    }
1010
1011    @LayoutlibDelegate
1012    static AssetFileDescriptor openRawResourceFd(Resources resources, int id) throws
1013            NotFoundException {
1014        throw new UnsupportedOperationException();
1015    }
1016
1017    @VisibleForTesting
1018    @Nullable
1019    static ResourceUrl resourceUrlFromName(@NonNull String name, @Nullable String defType,
1020            @Nullable
1021            String defPackage) {
1022        int colonIdx = name.indexOf(':');
1023        int slashIdx = name.indexOf('/');
1024
1025        if (colonIdx != -1 && slashIdx != -1) {
1026            // Easy case
1027            return ResourceUrl.parse(PREFIX_RESOURCE_REF + name);
1028        }
1029
1030        if (colonIdx == -1 && slashIdx == -1) {
1031            if (defType == null) {
1032                throw new IllegalArgumentException("name does not define a type an no defType was" +
1033                        " passed");
1034            }
1035
1036            // It does not define package or type
1037            return ResourceUrl.parse(
1038                    PREFIX_RESOURCE_REF + (defPackage != null ? defPackage + ":" : "") + defType +
1039                            "/" + name);
1040        }
1041
1042        if (colonIdx != -1) {
1043            if (defType == null) {
1044                throw new IllegalArgumentException("name does not define a type an no defType was" +
1045                        " passed");
1046            }
1047            // We have package but no type
1048            String pkg = name.substring(0, colonIdx);
1049            ResourceType type = ResourceType.getEnum(defType);
1050            return type != null ? ResourceUrl.create(pkg, type, name.substring(colonIdx + 1)) :
1051                    null;
1052        }
1053
1054        ResourceType type = ResourceType.getEnum(name.substring(0, slashIdx));
1055        if (type == null) {
1056            return null;
1057        }
1058        // We have type but no package
1059        return ResourceUrl.create(defPackage,
1060                type,
1061                name.substring(slashIdx + 1));
1062    }
1063
1064    @LayoutlibDelegate
1065    static int getIdentifier(Resources resources, String name, String defType, String defPackage) {
1066        if (name == null) {
1067            return 0;
1068        }
1069
1070        ResourceUrl url = resourceUrlFromName(name, defType, defPackage);
1071        Integer id = null;
1072        if (url != null) {
1073            id = ANDROID_PKG.equals(url.namespace) ? Bridge.getResourceId(url.type, url.name) :
1074                    getLayoutlibCallback(resources).getResourceId(url.type, url.name);
1075        }
1076
1077        return id != null ? id : 0;
1078    }
1079
1080    /**
1081     * Builds and throws a {@link Resources.NotFoundException} based on a resource id and a resource
1082     * type.
1083     *
1084     * @param id the id of the resource
1085     *
1086     * @throws NotFoundException
1087     */
1088    private static void throwException(Resources resources, int id) throws NotFoundException {
1089        throwException(id, getResourceInfo(resources, id, new boolean[1]));
1090    }
1091
1092    private static void throwException(int id, @Nullable Pair<ResourceType, String> resourceInfo) {
1093        String message;
1094        if (resourceInfo != null) {
1095            message = String.format(
1096                    "Could not find %1$s resource matching value 0x%2$X (resolved name: %3$s) in current configuration.",
1097                    resourceInfo.getFirst(), id, resourceInfo.getSecond());
1098        } else {
1099            message = String.format("Could not resolve resource value: 0x%1$X.", id);
1100        }
1101
1102        throw new NotFoundException(message);
1103    }
1104
1105    private static int getInt(String v) throws NumberFormatException {
1106        int radix = 10;
1107        if (v.startsWith("0x")) {
1108            v = v.substring(2);
1109            radix = 16;
1110        } else if (v.startsWith("0")) {
1111            radix = 8;
1112        }
1113        return Integer.parseInt(v, radix);
1114    }
1115}
1116