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.view;
18
19import com.android.ide.common.rendering.api.LayoutLog;
20import com.android.ide.common.rendering.api.LayoutlibCallback;
21import com.android.ide.common.rendering.api.MergeCookie;
22import com.android.ide.common.rendering.api.ResourceReference;
23import com.android.ide.common.rendering.api.ResourceValue;
24import com.android.layoutlib.bridge.Bridge;
25import com.android.layoutlib.bridge.BridgeConstants;
26import com.android.layoutlib.bridge.MockView;
27import com.android.layoutlib.bridge.android.BridgeContext;
28import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
29import com.android.layoutlib.bridge.android.support.DrawerLayoutUtil;
30import com.android.layoutlib.bridge.android.support.RecyclerViewUtil;
31import com.android.layoutlib.bridge.impl.ParserFactory;
32import com.android.layoutlib.bridge.util.ReflectionUtils;
33import com.android.resources.ResourceType;
34import com.android.tools.layoutlib.annotations.NotNull;
35import com.android.tools.layoutlib.annotations.Nullable;
36import com.android.util.Pair;
37
38import org.xmlpull.v1.XmlPullParser;
39
40import android.annotation.NonNull;
41import android.content.Context;
42import android.content.res.TypedArray;
43import android.graphics.drawable.Animatable;
44import android.graphics.drawable.Drawable;
45import android.util.AttributeSet;
46import android.widget.ImageView;
47import android.widget.NumberPicker;
48
49import java.io.File;
50import java.lang.reflect.Constructor;
51import java.lang.reflect.InvocationTargetException;
52import java.lang.reflect.Method;
53import java.util.HashMap;
54import java.util.Map;
55import java.util.function.BiFunction;
56
57import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext;
58
59/**
60 * Custom implementation of {@link LayoutInflater} to handle custom views.
61 */
62public final class BridgeInflater extends LayoutInflater {
63
64    private final LayoutlibCallback mLayoutlibCallback;
65
66    private boolean mIsInMerge = false;
67    private ResourceReference mResourceReference;
68    private Map<View, String> mOpenDrawerLayouts;
69
70    // Keep in sync with the same value in LayoutInflater.
71    private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme };
72
73    /**
74     * List of class prefixes which are tried first by default.
75     * <p/>
76     * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater.
77     */
78    private static final String[] sClassPrefixList = {
79        "android.widget.",
80        "android.webkit.",
81        "android.app."
82    };
83    private BiFunction<String, AttributeSet, View> mCustomInflater;
84
85    public static String[] getClassPrefixList() {
86        return sClassPrefixList;
87    }
88
89    private BridgeInflater(LayoutInflater original, Context newContext) {
90        super(original, newContext);
91        newContext = getBaseContext(newContext);
92        mLayoutlibCallback = (newContext instanceof BridgeContext) ?
93                ((BridgeContext) newContext).getLayoutlibCallback() :
94                null;
95    }
96
97    /**
98     * Instantiate a new BridgeInflater with an {@link LayoutlibCallback} object.
99     *
100     * @param context The Android application context.
101     * @param layoutlibCallback the {@link LayoutlibCallback} object.
102     */
103    public BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback) {
104        super(context);
105        mLayoutlibCallback = layoutlibCallback;
106        mConstructorArgs[0] = context;
107    }
108
109    @Override
110    public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
111        View view = createViewFromCustomInflater(name, attrs);
112
113        if (view == null) {
114            try {
115                // First try to find a class using the default Android prefixes
116                for (String prefix : sClassPrefixList) {
117                    try {
118                        view = createView(name, prefix, attrs);
119                        if (view != null) {
120                            break;
121                        }
122                    } catch (ClassNotFoundException e) {
123                        // Ignore. We'll try again using the base class below.
124                    }
125                }
126
127                // Next try using the parent loader. This will most likely only work for
128                // fully-qualified class names.
129                try {
130                    if (view == null) {
131                        view = super.onCreateView(name, attrs);
132                    }
133                } catch (ClassNotFoundException e) {
134                    // Ignore. We'll try again using the custom view loader below.
135                }
136
137                // Finally try again using the custom view loader
138                if (view == null) {
139                    view = loadCustomView(name, attrs);
140                }
141            } catch (InflateException e) {
142                // Don't catch the InflateException below as that results in hiding the real cause.
143                throw e;
144            } catch (Exception e) {
145                // Wrap the real exception in a ClassNotFoundException, so that the calling method
146                // can deal with it.
147                throw new ClassNotFoundException("onCreateView", e);
148            }
149        }
150
151        setupViewInContext(view, attrs);
152
153        return view;
154    }
155
156    /**
157     * Finds the createView method in the given customInflaterClass. Since createView is
158     * currently package protected, it will show in the declared class so we iterate up the
159     * hierarchy and return the first instance we find.
160     * The returned method will be accessible.
161     */
162    @NotNull
163    private static Method getCreateViewMethod(Class<?> customInflaterClass) throws NoSuchMethodException {
164        Class<?> current = customInflaterClass;
165        do {
166            try {
167                Method method = current.getDeclaredMethod("createView", View.class, String.class,
168                                Context.class, AttributeSet.class, boolean.class, boolean.class,
169                                boolean.class, boolean.class);
170                method.setAccessible(true);
171                return method;
172            } catch (NoSuchMethodException ignore) {
173            }
174            current = current.getSuperclass();
175        } while (current != null && current != Object.class);
176
177        throw new NoSuchMethodException();
178    }
179
180    /**
181     * Finds the custom inflater class. If it's defined in the theme, we'll use that one (if the
182     * class does not exist, null is returned).
183     * If {@code viewInflaterClass} is not defined in the theme, we'll try to instantiate
184     * {@code android.support.v7.app.AppCompatViewInflater}
185     */
186    @Nullable
187    private static Class<?> findCustomInflater(@NotNull BridgeContext bc,
188            @NotNull LayoutlibCallback layoutlibCallback) {
189        ResourceValue value = bc.getRenderResources().findItemInTheme("viewInflaterClass", false);
190        String inflaterName = value != null ? value.getValue() : null;
191
192        if (inflaterName != null) {
193            try {
194                return layoutlibCallback.findClass(inflaterName);
195            } catch (ClassNotFoundException ignore) {
196            }
197
198            // viewInflaterClass was defined but we couldn't find the class
199        } else if (bc.isAppCompatTheme()) {
200            // Older versions of AppCompat do not define the viewInflaterClass so try to get it
201            // manually
202            try {
203                return layoutlibCallback.findClass("android.support.v7.app.AppCompatViewInflater");
204            } catch (ClassNotFoundException ignore) {
205            }
206        }
207
208        return null;
209    }
210
211    /**
212     * Checks if there is a custom inflater and, when present, tries to instantiate the view
213     * using it.
214     */
215    @Nullable
216    private View createViewFromCustomInflater(@NotNull String name, @NotNull AttributeSet attrs) {
217        if (mCustomInflater == null) {
218            Context context = getContext();
219            context = getBaseContext(context);
220            if (context instanceof BridgeContext) {
221                BridgeContext bc = (BridgeContext) context;
222                Class<?> inflaterClass = findCustomInflater(bc, mLayoutlibCallback);
223
224                if (inflaterClass != null) {
225                    try {
226                        Constructor<?> constructor =  inflaterClass.getDeclaredConstructor();
227                        constructor.setAccessible(true);
228                        Object inflater = constructor.newInstance();
229                        Method method = getCreateViewMethod(inflaterClass);
230                        Context finalContext = context;
231                        mCustomInflater = (viewName, attributeSet) -> {
232                            try {
233                                return (View) method.invoke(inflater, null, viewName, finalContext,
234                                        attributeSet,
235                                        false,
236                                        false /*readAndroidTheme*/, // No need after L
237                                        true /*readAppTheme*/,
238                                        true /*wrapContext*/);
239                            } catch (IllegalAccessException | InvocationTargetException e) {
240                                assert false : "Call to createView failed";
241                            }
242                            return null;
243                        };
244                    } catch (InvocationTargetException | IllegalAccessException |
245                            NoSuchMethodException | InstantiationException ignore) {
246                    }
247                }
248            }
249
250            if (mCustomInflater == null) {
251                // There is no custom inflater. We'll create a nop custom inflater to avoid the
252                // penalty of trying to instantiate again
253                mCustomInflater = (s, attributeSet) -> null;
254            }
255        }
256
257        return mCustomInflater.apply(name, attrs);
258    }
259
260    @Override
261    public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
262            boolean ignoreThemeAttr) {
263        View view = null;
264        if (name.equals("view")) {
265            // This is usually done by the superclass but this allows us catching the error and
266            // reporting something useful.
267            name = attrs.getAttributeValue(null, "class");
268
269            if (name == null) {
270                Bridge.getLog().error(LayoutLog.TAG_BROKEN, "Unable to inflate view tag without " +
271                  "class attribute", null);
272                // We weren't able to resolve the view so we just pass a mock View to be able to
273                // continue rendering.
274                view = new MockView(context, attrs);
275                ((MockView) view).setText("view");
276            }
277        }
278
279        try {
280            if (view == null) {
281                view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr);
282            }
283        } catch (InflateException e) {
284            // Creation of ContextThemeWrapper code is same as in the super method.
285            // Apply a theme wrapper, if allowed and one is specified.
286            if (!ignoreThemeAttr) {
287                final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
288                final int themeResId = ta.getResourceId(0, 0);
289                if (themeResId != 0) {
290                    context = new ContextThemeWrapper(context, themeResId);
291                }
292                ta.recycle();
293            }
294            if (!(e.getCause() instanceof ClassNotFoundException)) {
295                // There is some unknown inflation exception in inflating a View that was found.
296                view = new MockView(context, attrs);
297                ((MockView) view).setText(name);
298                Bridge.getLog().error(LayoutLog.TAG_BROKEN, e.getMessage(), e, null);
299            } else {
300                final Object lastContext = mConstructorArgs[0];
301                mConstructorArgs[0] = context;
302                // try to load the class from using the custom view loader
303                try {
304                    view = loadCustomView(name, attrs);
305                } catch (Exception e2) {
306                    // Wrap the real exception in an InflateException so that the calling
307                    // method can deal with it.
308                    InflateException exception = new InflateException();
309                    if (!e2.getClass().equals(ClassNotFoundException.class)) {
310                        exception.initCause(e2);
311                    } else {
312                        exception.initCause(e);
313                    }
314                    throw exception;
315                } finally {
316                    mConstructorArgs[0] = lastContext;
317                }
318            }
319        }
320
321        setupViewInContext(view, attrs);
322
323        return view;
324    }
325
326    @Override
327    public View inflate(int resource, ViewGroup root) {
328        Context context = getContext();
329        context = getBaseContext(context);
330        if (context instanceof BridgeContext) {
331            BridgeContext bridgeContext = (BridgeContext)context;
332
333            ResourceValue value = null;
334
335            @SuppressWarnings("deprecation")
336            Pair<ResourceType, String> layoutInfo = Bridge.resolveResourceId(resource);
337            if (layoutInfo != null) {
338                value = bridgeContext.getRenderResources().getFrameworkResource(
339                        ResourceType.LAYOUT, layoutInfo.getSecond());
340            } else {
341                layoutInfo = mLayoutlibCallback.resolveResourceId(resource);
342
343                if (layoutInfo != null) {
344                    value = bridgeContext.getRenderResources().getProjectResource(
345                            ResourceType.LAYOUT, layoutInfo.getSecond());
346                }
347            }
348
349            if (value != null) {
350                File f = new File(value.getValue());
351                if (f.isFile()) {
352                    try {
353                        XmlPullParser parser = ParserFactory.create(f, true);
354
355                        BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser(
356                                parser, bridgeContext, value.isFramework());
357
358                        return inflate(bridgeParser, root);
359                    } catch (Exception e) {
360                        Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
361                                "Failed to parse file " + f.getAbsolutePath(), e, null);
362
363                        return null;
364                    }
365                }
366            }
367        }
368        return null;
369    }
370
371    /**
372     * Instantiates the given view name and returns the instance. If the view doesn't exist, a
373     * MockView or null might be returned.
374     * @param name the custom view name
375     * @param attrs the {@link AttributeSet} to be passed to the view constructor
376     * @param silent if true, errors while loading the view won't be reported and, if the view
377     * doesn't exist, null will be returned.
378     */
379    private View loadCustomView(String name, AttributeSet attrs, boolean silent) throws Exception {
380        if (mLayoutlibCallback != null) {
381            // first get the classname in case it's not the node name
382            if (name.equals("view")) {
383                name = attrs.getAttributeValue(null, "class");
384                if (name == null) {
385                    return null;
386                }
387            }
388
389            mConstructorArgs[1] = attrs;
390
391            Object customView = silent ?
392                    mLayoutlibCallback.loadClass(name, mConstructorSignature, mConstructorArgs)
393                    : mLayoutlibCallback.loadView(name, mConstructorSignature, mConstructorArgs);
394
395            if (customView instanceof View) {
396                return (View)customView;
397            }
398        }
399
400        return null;
401    }
402
403    private View loadCustomView(String name, AttributeSet attrs) throws Exception {
404        return loadCustomView(name, attrs, false);
405    }
406
407    private void setupViewInContext(View view, AttributeSet attrs) {
408        Context context = getContext();
409        context = getBaseContext(context);
410        if (context instanceof BridgeContext) {
411            BridgeContext bc = (BridgeContext) context;
412            // get the view key
413            Object viewKey = getViewKeyFromParser(attrs, bc, mResourceReference, mIsInMerge);
414            if (viewKey != null) {
415                bc.addViewKey(view, viewKey);
416            }
417            String scrollPosX = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollX");
418            if (scrollPosX != null && scrollPosX.endsWith("px")) {
419                int value = Integer.parseInt(scrollPosX.substring(0, scrollPosX.length() - 2));
420                bc.setScrollXPos(view, value);
421            }
422            String scrollPosY = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY");
423            if (scrollPosY != null && scrollPosY.endsWith("px")) {
424                int value = Integer.parseInt(scrollPosY.substring(0, scrollPosY.length() - 2));
425                bc.setScrollYPos(view, value);
426            }
427            if (ReflectionUtils.isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) {
428                Integer resourceId = null;
429                String attrListItemValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
430                        BridgeConstants.ATTR_LIST_ITEM);
431                int attrItemCountValue = attrs.getAttributeIntValue(BridgeConstants.NS_TOOLS_URI,
432                        BridgeConstants.ATTR_ITEM_COUNT, -1);
433                if (attrListItemValue != null && !attrListItemValue.isEmpty()) {
434                    ResourceValue resValue = bc.getRenderResources().findResValue(attrListItemValue, false);
435                    if (resValue.isFramework()) {
436                        resourceId = Bridge.getResourceId(resValue.getResourceType(),
437                                resValue.getName());
438                    } else {
439                        resourceId = mLayoutlibCallback.getResourceId(resValue.getResourceType(),
440                                resValue.getName());
441                    }
442                }
443                if (resourceId == null) {
444                    resourceId = 0;
445                }
446                RecyclerViewUtil.setAdapter(view, bc, mLayoutlibCallback, resourceId, attrItemCountValue);
447            } else if (ReflectionUtils.isInstanceOf(view, DrawerLayoutUtil.CN_DRAWER_LAYOUT)) {
448                String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
449                        BridgeConstants.ATTR_OPEN_DRAWER);
450                if (attrVal != null) {
451                    getDrawerLayoutMap().put(view, attrVal);
452                }
453            }
454            else if (view instanceof NumberPicker) {
455                NumberPicker numberPicker = (NumberPicker) view;
456                String minValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "minValue");
457                if (minValue != null) {
458                    numberPicker.setMinValue(Integer.parseInt(minValue));
459                }
460                String maxValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "maxValue");
461                if (maxValue != null) {
462                    numberPicker.setMaxValue(Integer.parseInt(maxValue));
463                }
464            }
465            else if (view instanceof ImageView) {
466                ImageView img = (ImageView) view;
467                Drawable drawable = img.getDrawable();
468                if (drawable instanceof Animatable) {
469                    if (!((Animatable) drawable).isRunning()) {
470                        ((Animatable) drawable).start();
471                    }
472                }
473            }
474
475        }
476    }
477
478    public void setIsInMerge(boolean isInMerge) {
479        mIsInMerge = isInMerge;
480    }
481
482    public void setResourceReference(ResourceReference reference) {
483        mResourceReference = reference;
484    }
485
486    @Override
487    public LayoutInflater cloneInContext(Context newContext) {
488        return new BridgeInflater(this, newContext);
489    }
490
491    /*package*/ static Object getViewKeyFromParser(AttributeSet attrs, BridgeContext bc,
492            ResourceReference resourceReference, boolean isInMerge) {
493
494        if (!(attrs instanceof BridgeXmlBlockParser)) {
495            return null;
496        }
497        BridgeXmlBlockParser parser = ((BridgeXmlBlockParser) attrs);
498
499        // get the view key
500        Object viewKey = parser.getViewCookie();
501
502        if (viewKey == null) {
503            int currentDepth = parser.getDepth();
504
505            // test whether we are in an included file or in a adapter binding view.
506            BridgeXmlBlockParser previousParser = bc.getPreviousParser();
507            if (previousParser != null) {
508                // looks like we are inside an embedded layout.
509                // only apply the cookie of the calling node (<include>) if we are at the
510                // top level of the embedded layout. If there is a merge tag, then
511                // skip it and look for the 2nd level
512                int testDepth = isInMerge ? 2 : 1;
513                if (currentDepth == testDepth) {
514                    viewKey = previousParser.getViewCookie();
515                    // if we are in a merge, wrap the cookie in a MergeCookie.
516                    if (viewKey != null && isInMerge) {
517                        viewKey = new MergeCookie(viewKey);
518                    }
519                }
520            } else if (resourceReference != null && currentDepth == 1) {
521                // else if there's a resource reference, this means we are in an adapter
522                // binding case. Set the resource ref as the view cookie only for the top
523                // level view.
524                viewKey = resourceReference;
525            }
526        }
527
528        return viewKey;
529    }
530
531    public void postInflateProcess(View view) {
532        if (mOpenDrawerLayouts != null) {
533            String gravity = mOpenDrawerLayouts.get(view);
534            if (gravity != null) {
535                DrawerLayoutUtil.openDrawer(view, gravity);
536            }
537            mOpenDrawerLayouts.remove(view);
538        }
539    }
540
541    @NonNull
542    private Map<View, String> getDrawerLayoutMap() {
543        if (mOpenDrawerLayouts == null) {
544            mOpenDrawerLayouts = new HashMap<View, String>(4);
545        }
546        return mOpenDrawerLayouts;
547    }
548
549    public void onDoneInflation() {
550        if (mOpenDrawerLayouts != null) {
551            mOpenDrawerLayouts.clear();
552        }
553    }
554}
555