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