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