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