• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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 com.android.ide.eclipse.adt.internal.editors.layout;
18 
19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_PKG_PREFIX;
20 import static com.android.ide.common.layout.LayoutConstants.CALENDAR_VIEW;
21 import static com.android.ide.common.layout.LayoutConstants.EXPANDABLE_LIST_VIEW;
22 import static com.android.ide.common.layout.LayoutConstants.FQCN_GRID_VIEW;
23 import static com.android.ide.common.layout.LayoutConstants.FQCN_SPINNER;
24 import static com.android.ide.common.layout.LayoutConstants.GRID_VIEW;
25 import static com.android.ide.common.layout.LayoutConstants.LIST_VIEW;
26 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_FRAGMENT;
27 
28 import com.android.ide.common.rendering.LayoutLibrary;
29 import com.android.ide.common.rendering.api.AdapterBinding;
30 import com.android.ide.common.rendering.api.DataBindingItem;
31 import com.android.ide.common.rendering.api.ILayoutPullParser;
32 import com.android.ide.common.rendering.api.IProjectCallback;
33 import com.android.ide.common.rendering.api.LayoutLog;
34 import com.android.ide.common.rendering.api.ResourceReference;
35 import com.android.ide.common.rendering.api.ResourceValue;
36 import com.android.ide.common.rendering.api.Result;
37 import com.android.ide.common.rendering.legacy.LegacyCallback;
38 import com.android.ide.common.resources.ResourceResolver;
39 import com.android.ide.eclipse.adt.AdtConstants;
40 import com.android.ide.eclipse.adt.AdtPlugin;
41 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata;
42 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
43 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
44 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectClassLoader;
45 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
46 import com.android.resources.ResourceType;
47 import com.android.sdklib.SdkConstants;
48 import com.android.sdklib.xml.ManifestData;
49 import com.android.util.Pair;
50 
51 import org.eclipse.core.resources.IProject;
52 import org.xmlpull.v1.XmlPullParser;
53 import org.xmlpull.v1.XmlPullParserException;
54 
55 import java.io.File;
56 import java.io.FileInputStream;
57 import java.io.FileNotFoundException;
58 import java.lang.reflect.Constructor;
59 import java.lang.reflect.Field;
60 import java.lang.reflect.Method;
61 import java.util.HashMap;
62 import java.util.Set;
63 import java.util.TreeSet;
64 
65 /**
66  * Loader for Android Project class in order to use them in the layout editor.
67  * <p/>This implements {@link IProjectCallback} for the old and new API through
68  * {@link LegacyCallback}
69  */
70 public final class ProjectCallback extends LegacyCallback {
71     private final HashMap<String, Class<?>> mLoadedClasses = new HashMap<String, Class<?>>();
72     private final Set<String> mMissingClasses = new TreeSet<String>();
73     private final Set<String> mBrokenClasses = new TreeSet<String>();
74     private final IProject mProject;
75     private final ClassLoader mParentClassLoader;
76     private final ProjectResources mProjectRes;
77     private boolean mUsed = false;
78     private String mNamespace;
79     private ProjectClassLoader mLoader = null;
80     private LayoutLog mLogger;
81     private LayoutLibrary mLayoutLib;
82 
83     private String mLayoutName;
84     private ILayoutPullParser mLayoutEmbeddedParser;
85     private ResourceResolver mResourceResolver;
86 
87     /**
88      * Creates a new {@link ProjectCallback} to be used with the layout lib.
89      *
90      * @param layoutLib The layout library this callback is going to be invoked from
91      * @param projectRes the {@link ProjectResources} for the project.
92      * @param project the project.
93      */
ProjectCallback(LayoutLibrary layoutLib, ProjectResources projectRes, IProject project)94     public ProjectCallback(LayoutLibrary layoutLib,
95             ProjectResources projectRes, IProject project) {
96         mLayoutLib = layoutLib;
97         mParentClassLoader = layoutLib.getClassLoader();
98         mProjectRes = projectRes;
99         mProject = project;
100     }
101 
getMissingClasses()102     public Set<String> getMissingClasses() {
103         return mMissingClasses;
104     }
105 
getUninstantiatableClasses()106     public Set<String> getUninstantiatableClasses() {
107         return mBrokenClasses;
108     }
109 
110     /**
111      * Sets the {@link LayoutLog} logger to use for error messages during problems
112      *
113      * @param logger the new logger to use, or null to clear it out
114      */
setLogger(LayoutLog logger)115     public void setLogger(LayoutLog logger) {
116         mLogger = logger;
117     }
118 
119     /**
120      * Returns the {@link LayoutLog} logger used for error messages, or null
121      *
122      * @return the logger being used, or null if no logger is in use
123      */
getLogger()124     public LayoutLog getLogger() {
125         return mLogger;
126     }
127 
128     /**
129      * {@inheritDoc}
130      *
131      * This implementation goes through the output directory of the Eclipse project and loads the
132      * <code>.class</code> file directly.
133      */
134     @Override
135     @SuppressWarnings("unchecked")
loadView(String className, Class[] constructorSignature, Object[] constructorParameters)136     public Object loadView(String className, Class[] constructorSignature,
137             Object[] constructorParameters)
138             throws ClassNotFoundException, Exception {
139         mUsed = true;
140 
141         // look for a cached version
142         Class<?> clazz = mLoadedClasses.get(className);
143         if (clazz != null) {
144             return instantiateClass(clazz, constructorSignature, constructorParameters);
145         }
146 
147         // load the class.
148 
149         try {
150             if (mLoader == null) {
151                 mLoader = new ProjectClassLoader(mParentClassLoader, mProject);
152             }
153             clazz = mLoader.loadClass(className);
154         } catch (Exception e) {
155             // Add the missing class to the list so that the renderer can print them later.
156             // no need to log this.
157             if (!className.equals(VIEW_FRAGMENT)) {
158                 mMissingClasses.add(className);
159             }
160         }
161 
162         try {
163             if (clazz != null) {
164                 // first try to instantiate it because adding it the list of loaded class so that
165                 // we don't add broken classes.
166                 Object view = instantiateClass(clazz, constructorSignature, constructorParameters);
167                 mLoadedClasses.put(className, clazz);
168 
169                 return view;
170             }
171         } catch (Throwable e) {
172             // Find root cause to log it.
173             while (e.getCause() != null) {
174                 e = e.getCause();
175             }
176 
177             AdtPlugin.log(e, "%1$s failed to instantiate.", className); //$NON-NLS-1$
178 
179             // Add the missing class to the list so that the renderer can print them later.
180             mBrokenClasses.add(className);
181         }
182 
183         // Create a mock view instead. We don't cache it in the mLoadedClasses map.
184         // If any exception is thrown, we'll return a CFN with the original class name instead.
185         try {
186             clazz = mLoader.loadClass(SdkConstants.CLASS_MOCK_VIEW);
187             Object view = instantiateClass(clazz, constructorSignature, constructorParameters);
188 
189             // Set the text of the mock view to the simplified name of the custom class
190             Method m = view.getClass().getMethod("setText",
191                                                  new Class<?>[] { CharSequence.class });
192             String label = getShortClassName(className);
193             if (label.equals(VIEW_FRAGMENT)) {
194                 label = "<fragment>\n"
195                         + "Pick preview layout from the \"Fragment Layout\" context menu";
196             }
197 
198             m.invoke(view, label);
199 
200             // Call MockView.setGravity(Gravity.CENTER) to get the text centered in
201             // MockViews.
202             // TODO: Do this in layoutlib's MockView class instead.
203             try {
204                 // Look up android.view.Gravity#CENTER - or can we just hard-code
205                 // the value (17) here?
206                 Class<?> gravity =
207                     Class.forName("android.view.Gravity", //$NON-NLS-1$
208                             true, view.getClass().getClassLoader());
209                 Field centerField = gravity.getField("CENTER"); //$NON-NLS-1$
210                 int center = centerField.getInt(null);
211                 m = view.getClass().getMethod("setGravity",
212                         new Class<?>[] { Integer.TYPE });
213                 // Center
214                 //int center = (0x0001 << 4) | (0x0001 << 0);
215                 m.invoke(view, Integer.valueOf(center));
216             } catch (Exception e) {
217                 // Not important to center views
218             }
219 
220             return view;
221         } catch (Exception e) {
222             // We failed to create and return a mock view.
223             // Just throw back a CNF with the original class name.
224             throw new ClassNotFoundException(className, e);
225         }
226     }
227 
getShortClassName(String fqcn)228     private String getShortClassName(String fqcn) {
229         // The name is typically a fully-qualified class name. Let's make it a tad shorter.
230 
231         if (fqcn.startsWith("android.")) {                                      //$NON-NLS-1$
232             // For android classes, convert android.foo.Name to android...Name
233             int first = fqcn.indexOf('.');
234             int last = fqcn.lastIndexOf('.');
235             if (last > first) {
236                 return fqcn.substring(0, first) + ".." + fqcn.substring(last);   //$NON-NLS-1$
237             }
238         } else {
239             // For custom non-android classes, it's best to keep the 2 first segments of
240             // the namespace, e.g. we want to get something like com.example...MyClass
241             int first = fqcn.indexOf('.');
242             first = fqcn.indexOf('.', first + 1);
243             int last = fqcn.lastIndexOf('.');
244             if (last > first) {
245                 return fqcn.substring(0, first) + ".." + fqcn.substring(last);   //$NON-NLS-1$
246             }
247         }
248 
249         return fqcn;
250     }
251 
252     /**
253      * Returns the namespace for the project. The namespace contains a standard part + the
254      * application package.
255      *
256      * @return The package namespace of the project or null in case of error.
257      */
258     @Override
getNamespace()259     public String getNamespace() {
260         if (mNamespace == null) {
261             ManifestData manifestData = AndroidManifestHelper.parseForData(mProject);
262             if (manifestData != null) {
263                 String javaPackage = manifestData.getPackage();
264                 mNamespace = String.format(AdtConstants.NS_CUSTOM_RESOURCES, javaPackage);
265             }
266         }
267 
268         return mNamespace;
269     }
270 
271     @Override
resolveResourceId(int id)272     public Pair<ResourceType, String> resolveResourceId(int id) {
273         if (mProjectRes != null) {
274             return mProjectRes.resolveResourceId(id);
275         }
276 
277         return null;
278     }
279 
280     @Override
resolveResourceId(int[] id)281     public String resolveResourceId(int[] id) {
282         if (mProjectRes != null) {
283             return mProjectRes.resolveStyleable(id);
284         }
285 
286         return null;
287     }
288 
289     @Override
getResourceId(ResourceType type, String name)290     public Integer getResourceId(ResourceType type, String name) {
291         if (mProjectRes != null) {
292             return mProjectRes.getResourceId(type, name);
293         }
294 
295         return null;
296     }
297 
298     /**
299      * Returns whether the loader has received requests to load custom views. Note that
300      * the custom view loading may not actually have succeeded; this flag only records
301      * whether it was <b>requested</b>.
302      * <p/>
303      * This allows to efficiently only recreate when needed upon code change in the
304      * project.
305      *
306      * @return true if the loader has been asked to load custom views
307      */
isUsed()308     public boolean isUsed() {
309         return mUsed;
310     }
311 
312     /**
313      * Instantiate a class object, using a specific constructor and parameters.
314      * @param clazz the class to instantiate
315      * @param constructorSignature the signature of the constructor to use
316      * @param constructorParameters the parameters to use in the constructor.
317      * @return A new class object, created using a specific constructor and parameters.
318      * @throws Exception
319      */
320     @SuppressWarnings("unchecked")
instantiateClass(Class<?> clazz, Class[] constructorSignature, Object[] constructorParameters)321     private Object instantiateClass(Class<?> clazz,
322             Class[] constructorSignature,
323             Object[] constructorParameters) throws Exception {
324         Constructor<?> constructor = null;
325 
326         try {
327             constructor = clazz.getConstructor(constructorSignature);
328 
329         } catch (NoSuchMethodException e) {
330             // Custom views can either implement a 3-parameter, 2-parameter or a
331             // 1-parameter. Let's synthetically build and try all the alternatives.
332             // That's kind of like switching to the other box.
333             //
334             // The 3-parameter constructor takes the following arguments:
335             // ...(Context context, AttributeSet attrs, int defStyle)
336 
337             int n = constructorSignature.length;
338             if (n == 0) {
339                 // There is no parameter-less constructor. Nobody should ask for one.
340                 throw e;
341             }
342 
343             for (int i = 3; i >= 1; i--) {
344                 if (i == n) {
345                     // Let's skip the one we know already fails
346                     continue;
347                 }
348                 Class[] sig = new Class[i];
349                 Object[] params = new Object[i];
350 
351                 int k = i;
352                 if (n < k) {
353                     k = n;
354                 }
355                 System.arraycopy(constructorSignature, 0, sig, 0, k);
356                 System.arraycopy(constructorParameters, 0, params, 0, k);
357 
358                 for (k++; k <= i; k++) {
359                     if (k == 2) {
360                         // Parameter 2 is the AttributeSet
361                         sig[k-1] = clazz.getClassLoader().loadClass("android.util.AttributeSet");
362                         params[k-1] = null;
363 
364                     } else if (k == 3) {
365                         // Parameter 3 is the int defstyle
366                         sig[k-1] = int.class;
367                         params[k-1] = 0;
368                     }
369                 }
370 
371                 constructorSignature = sig;
372                 constructorParameters = params;
373 
374                 try {
375                     // Try again...
376                     constructor = clazz.getConstructor(constructorSignature);
377                     if (constructor != null) {
378                         // Found a suitable constructor, now let's use it.
379                         // (But let's warn the user if the simple View constructor was found
380                         // since Unexpected Things may happen if the attribute set constructors
381                         // are not found)
382                         if (constructorSignature.length < 2 && mLogger != null) {
383                             mLogger.warning("wrongconstructor", //$NON-NLS-1$
384                                 String.format("Custom view %1$s is not using the 2- or 3-argument "
385                                     + "View constructors; XML attributes will not work",
386                                     clazz.getSimpleName()), null /*data*/);
387                         }
388                         break;
389                     }
390                 } catch (NoSuchMethodException e1) {
391                     // pass
392                 }
393             }
394 
395             // If all the alternatives failed, throw the initial exception.
396             if (constructor == null) {
397                 throw e;
398             }
399         }
400 
401         constructor.setAccessible(true);
402         return constructor.newInstance(constructorParameters);
403     }
404 
setLayoutParser(String layoutName, ILayoutPullParser layoutParser)405     public void setLayoutParser(String layoutName, ILayoutPullParser layoutParser) {
406         mLayoutName = layoutName;
407         mLayoutEmbeddedParser = layoutParser;
408     }
409 
410     @Override
getParser(String layoutName)411     public ILayoutPullParser getParser(String layoutName) {
412         // Try to compute the ResourceValue for this layout since layoutlib
413         // must be an older version which doesn't pass the value:
414         if (mResourceResolver != null) {
415             ResourceValue value = mResourceResolver.getProjectResource(ResourceType.LAYOUT,
416                     layoutName);
417             if (value != null) {
418                 return getParser(value);
419             }
420         }
421 
422         return getParser(layoutName, null);
423     }
424 
425     @Override
getParser(ResourceValue layoutResource)426     public ILayoutPullParser getParser(ResourceValue layoutResource) {
427         return getParser(layoutResource.getName(),
428                 new File(layoutResource.getValue()));
429     }
430 
getParser(String layoutName, File xml)431     private ILayoutPullParser getParser(String layoutName, File xml) {
432         if (layoutName.equals(mLayoutName)) {
433             ILayoutPullParser parser = mLayoutEmbeddedParser;
434             // The parser should only be used once!! If it is included more than once,
435             // subsequent includes should just use a plain pull parser that is not tied
436             // to the XML model
437             mLayoutEmbeddedParser = null;
438             return parser;
439         }
440 
441         // For included layouts, create a ContextPullParser such that we get the
442         // layout editor behavior in included layouts as well - which for example
443         // replaces <fragment> tags with <include>.
444         if (xml != null && xml.isFile()) {
445             ContextPullParser parser = new ContextPullParser(this, xml);
446             try {
447                 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
448                 parser.setInput(new FileInputStream(xml), "UTF-8"); //$NON-NLS-1$
449                 return parser;
450             } catch (XmlPullParserException e) {
451                 AdtPlugin.log(e, null);
452             } catch (FileNotFoundException e) {
453                 // Shouldn't happen since we check isFile() above
454             }
455         }
456 
457         return null;
458     }
459 
460     @Override
getAdapterItemValue(ResourceReference adapterView, Object adapterCookie, ResourceReference itemRef, int fullPosition, int typePosition, int fullChildPosition, int typeChildPosition, ResourceReference viewRef, ViewAttribute viewAttribute, Object defaultValue)461     public Object getAdapterItemValue(ResourceReference adapterView, Object adapterCookie,
462             ResourceReference itemRef,
463             int fullPosition, int typePosition, int fullChildPosition, int typeChildPosition,
464             ResourceReference viewRef, ViewAttribute viewAttribute, Object defaultValue) {
465 
466         // Special case for the palette preview
467         if (viewAttribute == ViewAttribute.TEXT
468                 && adapterView.getName().startsWith("android_widget_")) { //$NON-NLS-1$
469             String name = adapterView.getName();
470             if (viewRef.getName().equals("text2")) { //$NON-NLS-1$
471                 return "Sub Item";
472             }
473             if (fullPosition == 0) {
474                 String viewName = name.substring("android_widget_".length());
475                 if (viewName.equals(EXPANDABLE_LIST_VIEW)) {
476                     return "ExpandableList"; // ExpandableListView is too wide, character-wraps
477                 }
478                 return viewName;
479             } else {
480                 return "Next Item";
481             }
482         }
483 
484         if (itemRef.isFramework()) {
485             // Special case for list_view_item_2 and friends
486             if (viewRef.getName().equals("text2")) { //$NON-NLS-1$
487                 return "Sub Item " + (fullPosition + 1);
488             }
489         }
490 
491         if (viewAttribute == ViewAttribute.TEXT && ((String) defaultValue).length() == 0) {
492             return "Item " + (fullPosition + 1);
493         }
494 
495         return null;
496     }
497 
498     /**
499      * For the given class, finds and returns the nearest super class which is a ListView
500      * or an ExpandableListView or a GridView (which uses a list adapter), or returns null.
501      *
502      * @param clz the class of the view object
503      * @return the fully qualified class name of the list ancestor, or null if there
504      *         is no list view ancestor
505      */
getListAdapterViewFqcn(Class<?> clz)506     public static String getListAdapterViewFqcn(Class<?> clz) {
507         String fqcn = clz.getName();
508         if (fqcn.endsWith(LIST_VIEW)) { // including EXPANDABLE_LIST_VIEW
509             return fqcn;
510         } else if (fqcn.equals(FQCN_GRID_VIEW)) {
511             return fqcn;
512         } else if (fqcn.equals(FQCN_SPINNER)) {
513             return fqcn;
514         } else if (fqcn.startsWith(ANDROID_PKG_PREFIX)) {
515             return null;
516         }
517         Class<?> superClass = clz.getSuperclass();
518         if (superClass != null) {
519             return getListAdapterViewFqcn(superClass);
520         } else {
521             // Should not happen; we would have encountered android.view.View first,
522             // and it should have been covered by the ANDROID_PKG_PREFIX case above.
523             return null;
524         }
525     }
526 
527     /**
528      * Looks at the parent-chain of the view and if it finds a custom view, or a
529      * CalendarView, within the given distance then it returns true. A ListView within a
530      * CalendarView should not be assigned a custom list view type because it sets its own
531      * and then attempts to cast the layout to its own type which would fail if the normal
532      * default list item binding is used.
533      */
isWithinIllegalParent(Object viewObject, int depth)534     private boolean isWithinIllegalParent(Object viewObject, int depth) {
535         String fqcn = viewObject.getClass().getName();
536         if (fqcn.endsWith(CALENDAR_VIEW) || !fqcn.startsWith(ANDROID_PKG_PREFIX)) {
537             return true;
538         }
539 
540         if (depth > 0) {
541             Result result = mLayoutLib.getViewParent(viewObject);
542             if (result.isSuccess()) {
543                 Object parent = result.getData();
544                 if (parent != null) {
545                     return isWithinIllegalParent(parent, depth -1);
546                 }
547             }
548         }
549 
550         return false;
551     }
552 
553     @Override
getAdapterBinding(final ResourceReference adapterView, final Object adapterCookie, final Object viewObject)554     public AdapterBinding getAdapterBinding(final ResourceReference adapterView, final Object adapterCookie,
555             final Object viewObject) {
556         // Look for user-recorded preference for layout to be used for previews
557         if (adapterCookie instanceof UiViewElementNode) {
558             UiViewElementNode uiNode = (UiViewElementNode) adapterCookie;
559             LayoutMetadata metadata = LayoutMetadata.get();
560             AdapterBinding binding = metadata.getNodeBinding(viewObject, uiNode);
561             if (binding != null) {
562                 return binding;
563             }
564         }
565 
566         if (viewObject == null) {
567             return null;
568         }
569 
570         // Is this a ListView or ExpandableListView? If so, return its fully qualified
571         // class name, otherwise return null. This is used to filter out other types
572         // of AdapterViews (such as Spinners) where we don't want to use the list item
573         // binding.
574         String listFqcn = getListAdapterViewFqcn(viewObject.getClass());
575         if (listFqcn == null) {
576             return null;
577         }
578 
579         // Is this ListView nested within an "illegal" container, such as a CalendarView?
580         // If so, don't change the bindings below. Some views, such as CalendarView, and
581         // potentially some custom views, might be doing specific things with the ListView
582         // that could break if we add our own list binding, so for these leave the list
583         // alone.
584         if (isWithinIllegalParent(viewObject, 2)) {
585             return null;
586         }
587 
588         int count = listFqcn.endsWith(GRID_VIEW) ? 24 : 12;
589         AdapterBinding binding = new AdapterBinding(count);
590         if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) {
591             binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_EXPANDABLE_LIST_ITEM,
592                     true /* isFramework */, 1));
593         } else if (listFqcn.equals(FQCN_SPINNER)) {
594             binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_SPINNER_ITEM,
595                     true /* isFramework */, 1));
596         } else {
597             binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_LIST_ITEM,
598                     true /* isFramework */, 1));
599         }
600 
601         return binding;
602     }
603 
604     /**
605      * Sets the {@link ResourceResolver} to be used when looking up resources
606      *
607      * @param resolver the resolver to use
608      */
setResourceResolver(ResourceResolver resolver)609     public void setResourceResolver(ResourceResolver resolver) {
610         mResourceResolver = resolver;
611     }
612 }
613