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