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 com.android.layoutlib.bridge.android;
18
19import com.android.ide.common.rendering.api.IProjectCallback;
20import com.android.ide.common.rendering.api.LayoutLog;
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.resources.ResourceType;
26import com.android.util.Pair;
27
28import org.kxml2.io.KXmlParser;
29import org.xmlpull.v1.XmlPullParser;
30
31import android.content.Context;
32import android.util.AttributeSet;
33import android.view.InflateException;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.ViewGroup;
37
38import java.io.File;
39import java.io.FileInputStream;
40
41/**
42 * Custom implementation of {@link LayoutInflater} to handle custom views.
43 */
44public final class BridgeInflater extends LayoutInflater {
45
46    private final IProjectCallback mProjectCallback;
47    private boolean mIsInMerge = false;
48    private ResourceReference mResourceReference;
49
50    /**
51     * List of class prefixes which are tried first by default.
52     * <p/>
53     * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater.
54     */
55    private static final String[] sClassPrefixList = {
56        "android.widget.",
57        "android.webkit."
58    };
59
60    protected BridgeInflater(LayoutInflater original, Context newContext) {
61        super(original, newContext);
62        mProjectCallback = null;
63    }
64
65    /**
66     * Instantiate a new BridgeInflater with an {@link IProjectCallback} object.
67     *
68     * @param context The Android application context.
69     * @param projectCallback the {@link IProjectCallback} object.
70     */
71    public BridgeInflater(Context context, IProjectCallback projectCallback) {
72        super(context);
73        mProjectCallback = projectCallback;
74        mConstructorArgs[0] = context;
75    }
76
77    @Override
78    public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
79        View view = null;
80
81        try {
82            // First try to find a class using the default Android prefixes
83            for (String prefix : sClassPrefixList) {
84                try {
85                    view = createView(name, prefix, attrs);
86                    if (view != null) {
87                        break;
88                    }
89                } catch (ClassNotFoundException e) {
90                    // Ignore. We'll try again using the base class below.
91                }
92            }
93
94            // Next try using the parent loader. This will most likely only work for
95            // fully-qualified class names.
96            try {
97                if (view == null) {
98                    view = super.onCreateView(name, attrs);
99                }
100            } catch (ClassNotFoundException e) {
101                // Ignore. We'll try again using the custom view loader below.
102            }
103
104            // Finally try again using the custom view loader
105            try {
106                if (view == null) {
107                    view = loadCustomView(name, attrs);
108                }
109            } catch (ClassNotFoundException e) {
110                // If the class was not found, we throw the exception directly, because this
111                // method is already expected to throw it.
112                throw e;
113            }
114        } catch (Exception e) {
115            // Wrap the real exception in a ClassNotFoundException, so that the calling method
116            // can deal with it.
117            ClassNotFoundException exception = new ClassNotFoundException("onCreateView", e);
118            throw exception;
119        }
120
121        setupViewInContext(view, attrs);
122
123        return view;
124    }
125
126    @Override
127    public View createViewFromTag(String name, AttributeSet attrs) {
128        View view = null;
129        try {
130            view = super.createViewFromTag(name, attrs);
131        } catch (InflateException e) {
132            // try to load the class from using the custom view loader
133            try {
134                view = loadCustomView(name, attrs);
135            } catch (Exception e2) {
136                // Wrap the real exception in an InflateException so that the calling
137                // method can deal with it.
138                InflateException exception = new InflateException();
139                if (e2.getClass().equals(ClassNotFoundException.class) == false) {
140                    exception.initCause(e2);
141                } else {
142                    exception.initCause(e);
143                }
144                throw exception;
145            }
146        }
147
148        setupViewInContext(view, attrs);
149
150        return view;
151    }
152
153    @Override
154    public View inflate(int resource, ViewGroup root) {
155        Context context = getContext();
156        if (context instanceof BridgeContext) {
157            BridgeContext bridgeContext = (BridgeContext)context;
158
159            ResourceValue value = null;
160
161            Pair<ResourceType, String> layoutInfo = Bridge.resolveResourceId(resource);
162            if (layoutInfo != null) {
163                value = bridgeContext.getRenderResources().getFrameworkResource(
164                        ResourceType.LAYOUT, layoutInfo.getSecond());
165            } else {
166                layoutInfo = mProjectCallback.resolveResourceId(resource);
167
168                if (layoutInfo != null) {
169                    value = bridgeContext.getRenderResources().getProjectResource(
170                            ResourceType.LAYOUT, layoutInfo.getSecond());
171                }
172            }
173
174            if (value != null) {
175                File f = new File(value.getValue());
176                if (f.isFile()) {
177                    try {
178                        KXmlParser parser = new KXmlParser();
179                        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
180                        parser.setInput(new FileInputStream(f), "UTF-8"); //$NON-NLS-1$
181
182                        BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser(
183                                parser, bridgeContext, false);
184
185                        return inflate(bridgeParser, root);
186                    } catch (Exception e) {
187                        Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
188                                "Failed to parse file " + f.getAbsolutePath(), e, null /*data*/);
189
190                        return null;
191                    }
192                }
193            }
194        }
195        return null;
196    }
197
198    private View loadCustomView(String name, AttributeSet attrs) throws ClassNotFoundException,
199            Exception{
200        if (mProjectCallback != null) {
201            // first get the classname in case it's not the node name
202            if (name.equals("view")) {
203                name = attrs.getAttributeValue(null, "class");
204            }
205
206            mConstructorArgs[1] = attrs;
207
208            Object customView = mProjectCallback.loadView(name, mConstructorSignature,
209                    mConstructorArgs);
210
211            if (customView instanceof View) {
212                return (View)customView;
213            }
214        }
215
216        return null;
217    }
218
219    private void setupViewInContext(View view, AttributeSet attrs) {
220        if (getContext() instanceof BridgeContext) {
221            BridgeContext bc = (BridgeContext) getContext();
222            if (attrs instanceof BridgeXmlBlockParser) {
223                BridgeXmlBlockParser parser = (BridgeXmlBlockParser) attrs;
224
225                // get the view key
226                Object viewKey = parser.getViewCookie();
227
228                if (viewKey == null) {
229                    int currentDepth = parser.getDepth();
230
231                    // test whether we are in an included file or in a adapter binding view.
232                    BridgeXmlBlockParser previousParser = bc.getPreviousParser();
233                    if (previousParser != null) {
234                        // looks like we inside an embedded layout.
235                        // only apply the cookie of the calling node (<include>) if we are at the
236                        // top level of the embedded layout. If there is a merge tag, then
237                        // skip it and look for the 2nd level
238                        int testDepth = mIsInMerge ? 2 : 1;
239                        if (currentDepth == testDepth) {
240                            viewKey = previousParser.getViewCookie();
241                            // if we are in a merge, wrap the cookie in a MergeCookie.
242                            if (viewKey != null && mIsInMerge) {
243                                viewKey = new MergeCookie(viewKey);
244                            }
245                        }
246                    } else if (mResourceReference != null && currentDepth == 1) {
247                        // else if there's a resource reference, this means we are in an adapter
248                        // binding case. Set the resource ref as the view cookie only for the top
249                        // level view.
250                        viewKey = mResourceReference;
251                    }
252                }
253
254                if (viewKey != null) {
255                    bc.addViewKey(view, viewKey);
256                }
257            }
258        }
259    }
260
261    public void setIsInMerge(boolean isInMerge) {
262        mIsInMerge = isInMerge;
263    }
264
265    public void setResourceReference(ResourceReference reference) {
266        mResourceReference = reference;
267    }
268
269    @Override
270    public LayoutInflater cloneInContext(Context newContext) {
271        return new BridgeInflater(this, newContext);
272    }
273}
274