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