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