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.util.Pair;
35
36import org.xmlpull.v1.XmlPullParser;
37
38import android.annotation.NonNull;
39import android.content.Context;
40import android.content.res.TypedArray;
41import android.util.AttributeSet;
42
43import java.io.File;
44import java.util.HashMap;
45import java.util.Map;
46
47import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext;
48
49/**
50 * Custom implementation of {@link LayoutInflater} to handle custom views.
51 */
52public 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
73    public static String[] getClassPrefixList() {
74        return sClassPrefixList;
75    }
76
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     */
93    public BridgeInflater(Context context, LayoutlibCallback layoutlibCallback) {
94        super(context);
95        mLayoutlibCallback = layoutlibCallback;
96        mConstructorArgs[0] = context;
97    }
98
99    @Override
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
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
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
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
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
307    public void setIsInMerge(boolean isInMerge) {
308        mIsInMerge = isInMerge;
309    }
310
311    public void setResourceReference(ResourceReference reference) {
312        mResourceReference = reference;
313    }
314
315    @Override
316    public LayoutInflater cloneInContext(Context newContext) {
317        return new BridgeInflater(this, newContext);
318    }
319
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
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
371    private Map<View, String> getDrawerLayoutMap() {
372        if (mOpenDrawerLayouts == null) {
373            mOpenDrawerLayouts = new HashMap<View, String>(4);
374        }
375        return mOpenDrawerLayouts;
376    }
377
378    public void onDoneInflation() {
379        if (mOpenDrawerLayouts != null) {
380            mOpenDrawerLayouts.clear();
381        }
382    }
383}
384