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.util.AttributeSet;
42 
43 import java.io.File;
44 import java.util.HashMap;
45 import java.util.Map;
46 
47 import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext;
48 
49 /**
50  * Custom implementation of {@link LayoutInflater} to handle custom views.
51  */
52 public final class BridgeInflater extends LayoutInflater {
53 
54     private final LayoutlibCallback mLayoutlibCallback;
55     private boolean mIsInMerge = false;
56     private ResourceReference mResourceReference;
57     private Map<View, String> mOpenDrawerLayouts;
58 
59     // Keep in sync with the same value in LayoutInflater.
60     private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme };
61 
62     /**
63      * List of class prefixes which are tried first by default.
64      * <p/>
65      * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater.
66      */
67     private static final String[] sClassPrefixList = {
68         "android.widget.",
69         "android.webkit.",
70         "android.app."
71     };
72 
getClassPrefixList()73     public static String[] getClassPrefixList() {
74         return sClassPrefixList;
75     }
76 
BridgeInflater(LayoutInflater original, Context newContext)77     protected BridgeInflater(LayoutInflater original, Context newContext) {
78         super(original, newContext);
79         newContext = getBaseContext(newContext);
80         if (newContext instanceof BridgeContext) {
81             mLayoutlibCallback = ((BridgeContext) newContext).getLayoutlibCallback();
82         } else {
83             mLayoutlibCallback = null;
84         }
85     }
86 
87     /**
88      * Instantiate a new BridgeInflater with an {@link LayoutlibCallback} object.
89      *
90      * @param context The Android application context.
91      * @param layoutlibCallback the {@link LayoutlibCallback} object.
92      */
BridgeInflater(Context context, LayoutlibCallback layoutlibCallback)93     public BridgeInflater(Context context, LayoutlibCallback layoutlibCallback) {
94         super(context);
95         mLayoutlibCallback = layoutlibCallback;
96         mConstructorArgs[0] = context;
97     }
98 
99     @Override
onCreateView(String name, AttributeSet attrs)100     public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
101         View view = null;
102 
103         try {
104             // First try to find a class using the default Android prefixes
105             for (String prefix : sClassPrefixList) {
106                 try {
107                     view = createView(name, prefix, attrs);
108                     if (view != null) {
109                         break;
110                     }
111                 } catch (ClassNotFoundException e) {
112                     // Ignore. We'll try again using the base class below.
113                 }
114             }
115 
116             // Next try using the parent loader. This will most likely only work for
117             // fully-qualified class names.
118             try {
119                 if (view == null) {
120                     view = super.onCreateView(name, attrs);
121                 }
122             } catch (ClassNotFoundException e) {
123                 // Ignore. We'll try again using the custom view loader below.
124             }
125 
126             // Finally try again using the custom view loader
127             if (view == null) {
128                 view = loadCustomView(name, attrs);
129             }
130         } catch (InflateException e) {
131             // Don't catch the InflateException below as that results in hiding the real cause.
132             throw e;
133         } catch (Exception e) {
134             // Wrap the real exception in a ClassNotFoundException, so that the calling method
135             // can deal with it.
136             throw new ClassNotFoundException("onCreateView", e);
137         }
138 
139         setupViewInContext(view, attrs);
140 
141         return view;
142     }
143 
144     @Override
createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)145     public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
146             boolean ignoreThemeAttr) {
147         View view;
148         try {
149             view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr);
150         } catch (InflateException e) {
151             // Creation of ContextThemeWrapper code is same as in the super method.
152             // Apply a theme wrapper, if allowed and one is specified.
153             if (!ignoreThemeAttr) {
154                 final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
155                 final int themeResId = ta.getResourceId(0, 0);
156                 if (themeResId != 0) {
157                     context = new ContextThemeWrapper(context, themeResId);
158                 }
159                 ta.recycle();
160             }
161             if (!(e.getCause() instanceof ClassNotFoundException)) {
162                 // There is some unknown inflation exception in inflating a View that was found.
163                 view = new MockView(context, attrs);
164                 ((MockView) view).setText(name);
165                 Bridge.getLog().error(LayoutLog.TAG_BROKEN, e.getMessage(), e, null);
166             } else {
167                 final Object lastContext = mConstructorArgs[0];
168                 mConstructorArgs[0] = context;
169                 // try to load the class from using the custom view loader
170                 try {
171                     view = loadCustomView(name, attrs);
172                 } catch (Exception e2) {
173                     // Wrap the real exception in an InflateException so that the calling
174                     // method can deal with it.
175                     InflateException exception = new InflateException();
176                     if (!e2.getClass().equals(ClassNotFoundException.class)) {
177                         exception.initCause(e2);
178                     } else {
179                         exception.initCause(e);
180                     }
181                     throw exception;
182                 } finally {
183                     mConstructorArgs[0] = lastContext;
184                 }
185             }
186         }
187 
188         setupViewInContext(view, attrs);
189 
190         return view;
191     }
192 
193     @Override
inflate(int resource, ViewGroup root)194     public View inflate(int resource, ViewGroup root) {
195         Context context = getContext();
196         context = getBaseContext(context);
197         if (context instanceof BridgeContext) {
198             BridgeContext bridgeContext = (BridgeContext)context;
199 
200             ResourceValue value = null;
201 
202             @SuppressWarnings("deprecation")
203             Pair<ResourceType, String> layoutInfo = Bridge.resolveResourceId(resource);
204             if (layoutInfo != null) {
205                 value = bridgeContext.getRenderResources().getFrameworkResource(
206                         ResourceType.LAYOUT, layoutInfo.getSecond());
207             } else {
208                 layoutInfo = mLayoutlibCallback.resolveResourceId(resource);
209 
210                 if (layoutInfo != null) {
211                     value = bridgeContext.getRenderResources().getProjectResource(
212                             ResourceType.LAYOUT, layoutInfo.getSecond());
213                 }
214             }
215 
216             if (value != null) {
217                 File f = new File(value.getValue());
218                 if (f.isFile()) {
219                     try {
220                         XmlPullParser parser = ParserFactory.create(f, true);
221 
222                         BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser(
223                                 parser, bridgeContext, value.isFramework());
224 
225                         return inflate(bridgeParser, root);
226                     } catch (Exception e) {
227                         Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
228                                 "Failed to parse file " + f.getAbsolutePath(), e, null);
229 
230                         return null;
231                     }
232                 }
233             }
234         }
235         return null;
236     }
237 
loadCustomView(String name, AttributeSet attrs)238     private View loadCustomView(String name, AttributeSet attrs) throws Exception {
239         if (mLayoutlibCallback != null) {
240             // first get the classname in case it's not the node name
241             if (name.equals("view")) {
242                 name = attrs.getAttributeValue(null, "class");
243             }
244 
245             mConstructorArgs[1] = attrs;
246 
247             Object customView = mLayoutlibCallback.loadView(name, mConstructorSignature,
248                     mConstructorArgs);
249 
250             if (customView instanceof View) {
251                 return (View)customView;
252             }
253         }
254 
255         return null;
256     }
257 
setupViewInContext(View view, AttributeSet attrs)258     private void setupViewInContext(View view, AttributeSet attrs) {
259         Context context = getContext();
260         context = getBaseContext(context);
261         if (context instanceof BridgeContext) {
262             BridgeContext bc = (BridgeContext) context;
263             // get the view key
264             Object viewKey = getViewKeyFromParser(attrs, bc, mResourceReference, mIsInMerge);
265             if (viewKey != null) {
266                 bc.addViewKey(view, viewKey);
267             }
268             String scrollPosX = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollX");
269             if (scrollPosX != null && scrollPosX.endsWith("px")) {
270                 int value = Integer.parseInt(scrollPosX.substring(0, scrollPosX.length() - 2));
271                 bc.setScrollXPos(view, value);
272             }
273             String scrollPosY = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY");
274             if (scrollPosY != null && scrollPosY.endsWith("px")) {
275                 int value = Integer.parseInt(scrollPosY.substring(0, scrollPosY.length() - 2));
276                 bc.setScrollYPos(view, value);
277             }
278             if (ReflectionUtils.isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) {
279                 Integer resourceId = null;
280                 String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
281                         BridgeConstants.ATTR_LIST_ITEM);
282                 if (attrVal != null && !attrVal.isEmpty()) {
283                     ResourceValue resValue = bc.getRenderResources().findResValue(attrVal, false);
284                     if (resValue.isFramework()) {
285                         resourceId = Bridge.getResourceId(resValue.getResourceType(),
286                                 resValue.getName());
287                     } else {
288                         resourceId = mLayoutlibCallback.getResourceId(resValue.getResourceType(),
289                                 resValue.getName());
290                     }
291                 }
292                 if (resourceId == null) {
293                     resourceId = 0;
294                 }
295                 RecyclerViewUtil.setAdapter(view, bc, mLayoutlibCallback, resourceId);
296             } else if (ReflectionUtils.isInstanceOf(view, DrawerLayoutUtil.CN_DRAWER_LAYOUT)) {
297                 String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
298                         BridgeConstants.ATTR_OPEN_DRAWER);
299                 if (attrVal != null) {
300                     getDrawerLayoutMap().put(view, attrVal);
301                 }
302             }
303 
304         }
305     }
306 
setIsInMerge(boolean isInMerge)307     public void setIsInMerge(boolean isInMerge) {
308         mIsInMerge = isInMerge;
309     }
310 
setResourceReference(ResourceReference reference)311     public void setResourceReference(ResourceReference reference) {
312         mResourceReference = reference;
313     }
314 
315     @Override
cloneInContext(Context newContext)316     public LayoutInflater cloneInContext(Context newContext) {
317         return new BridgeInflater(this, newContext);
318     }
319 
getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, ResourceReference resourceReference, boolean isInMerge)320     /*package*/ static Object getViewKeyFromParser(AttributeSet attrs, BridgeContext bc,
321             ResourceReference resourceReference, boolean isInMerge) {
322 
323         if (!(attrs instanceof BridgeXmlBlockParser)) {
324             return null;
325         }
326         BridgeXmlBlockParser parser = ((BridgeXmlBlockParser) attrs);
327 
328         // get the view key
329         Object viewKey = parser.getViewCookie();
330 
331         if (viewKey == null) {
332             int currentDepth = parser.getDepth();
333 
334             // test whether we are in an included file or in a adapter binding view.
335             BridgeXmlBlockParser previousParser = bc.getPreviousParser();
336             if (previousParser != null) {
337                 // looks like we are inside an embedded layout.
338                 // only apply the cookie of the calling node (<include>) if we are at the
339                 // top level of the embedded layout. If there is a merge tag, then
340                 // skip it and look for the 2nd level
341                 int testDepth = isInMerge ? 2 : 1;
342                 if (currentDepth == testDepth) {
343                     viewKey = previousParser.getViewCookie();
344                     // if we are in a merge, wrap the cookie in a MergeCookie.
345                     if (viewKey != null && isInMerge) {
346                         viewKey = new MergeCookie(viewKey);
347                     }
348                 }
349             } else if (resourceReference != null && currentDepth == 1) {
350                 // else if there's a resource reference, this means we are in an adapter
351                 // binding case. Set the resource ref as the view cookie only for the top
352                 // level view.
353                 viewKey = resourceReference;
354             }
355         }
356 
357         return viewKey;
358     }
359 
postInflateProcess(View view)360     public void postInflateProcess(View view) {
361         if (mOpenDrawerLayouts != null) {
362             String gravity = mOpenDrawerLayouts.get(view);
363             if (gravity != null) {
364                 DrawerLayoutUtil.openDrawer(view, gravity);
365             }
366             mOpenDrawerLayouts.remove(view);
367         }
368     }
369 
370     @NonNull
getDrawerLayoutMap()371     private Map<View, String> getDrawerLayoutMap() {
372         if (mOpenDrawerLayouts == null) {
373             mOpenDrawerLayouts = new HashMap<View, String>(4);
374         }
375         return mOpenDrawerLayouts;
376     }
377 
onDoneInflation()378     public void onDoneInflation() {
379         if (mOpenDrawerLayouts != null) {
380             mOpenDrawerLayouts.clear();
381         }
382     }
383 }
384