• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
17 
18 import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
19 
20 import com.android.annotations.NonNull;
21 import com.android.ide.common.api.IClientRulesEngine;
22 import com.android.ide.common.api.INode;
23 import com.android.ide.common.api.Rect;
24 import com.android.ide.common.rendering.HardwareConfigHelper;
25 import com.android.ide.common.rendering.LayoutLibrary;
26 import com.android.ide.common.rendering.api.Capability;
27 import com.android.ide.common.rendering.api.DrawableParams;
28 import com.android.ide.common.rendering.api.HardwareConfig;
29 import com.android.ide.common.rendering.api.IImageFactory;
30 import com.android.ide.common.rendering.api.ILayoutPullParser;
31 import com.android.ide.common.rendering.api.LayoutLog;
32 import com.android.ide.common.rendering.api.RenderSession;
33 import com.android.ide.common.rendering.api.ResourceValue;
34 import com.android.ide.common.rendering.api.Result;
35 import com.android.ide.common.rendering.api.SessionParams;
36 import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
37 import com.android.ide.common.rendering.api.ViewInfo;
38 import com.android.ide.common.resources.ResourceResolver;
39 import com.android.ide.common.resources.configuration.FolderConfiguration;
40 import com.android.ide.eclipse.adt.AdtPlugin;
41 import com.android.ide.eclipse.adt.internal.editors.layout.ContextPullParser;
42 import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
43 import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
44 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
45 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
46 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
47 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
48 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
49 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
50 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
51 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
52 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
53 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
54 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
55 import com.android.sdklib.IAndroidTarget;
56 import com.android.sdklib.devices.Device;
57 import com.google.common.base.Charsets;
58 import com.google.common.io.Files;
59 
60 import org.eclipse.core.resources.IProject;
61 import org.xmlpull.v1.XmlPullParser;
62 import org.xmlpull.v1.XmlPullParserException;
63 
64 import java.awt.image.BufferedImage;
65 import java.io.File;
66 import java.io.IOException;
67 import java.io.StringReader;
68 import java.util.Collections;
69 import java.util.HashMap;
70 import java.util.List;
71 import java.util.Map;
72 import java.util.Set;
73 
74 /**
75  * The {@link RenderService} provides rendering and layout information for
76  * Android layouts. This is a wrapper around the layout library.
77  */
78 public class RenderService {
79     /** Reference to the file being edited. Can also be used to access the {@link IProject}. */
80     private final GraphicalEditorPart mEditor;
81 
82     // The following fields are inferred from the editor and not customizable by the
83     // client of the render service:
84 
85     private final IProject mProject;
86     private final ProjectCallback mProjectCallback;
87     private final ResourceResolver mResourceResolver;
88     private final int mMinSdkVersion;
89     private final int mTargetSdkVersion;
90     private final LayoutLibrary mLayoutLib;
91     private final IImageFactory mImageFactory;
92     private final HardwareConfigHelper mHardwareConfigHelper;
93 
94     // The following fields are optional or configurable using the various chained
95     // setters:
96 
97     private UiDocumentNode mModel;
98     private Reference mIncludedWithin;
99     private RenderingMode mRenderingMode = RenderingMode.NORMAL;
100     private LayoutLog mLogger;
101     private Integer mOverrideBgColor;
102     private boolean mShowDecorations = true;
103     private Set<UiElementNode> mExpandNodes = Collections.<UiElementNode>emptySet();
104 
105     /** Use the {@link #create} factory instead */
RenderService(GraphicalEditorPart editor)106     private RenderService(GraphicalEditorPart editor) {
107         mEditor = editor;
108 
109         mProject = editor.getProject();
110         LayoutCanvas canvas = editor.getCanvasControl();
111         mImageFactory = canvas.getImageOverlay();
112         ConfigurationChooser chooser = editor.getConfigurationChooser();
113         Configuration config = chooser.getConfiguration();
114         FolderConfiguration folderConfig = config.getFullConfig();
115 
116         Device device = config.getDevice();
117         assert device != null; // Should only attempt render with configuration that has device
118         mHardwareConfigHelper = new HardwareConfigHelper(device);
119         mHardwareConfigHelper.setOrientation(
120                 folderConfig.getScreenOrientationQualifier().getValue());
121 
122         mLayoutLib = editor.getReadyLayoutLib(true /*displayError*/);
123         mResourceResolver = editor.getResourceResolver();
124         mProjectCallback = editor.getProjectCallback(true /*reset*/, mLayoutLib);
125         mMinSdkVersion = editor.getMinSdkVersion();
126         mTargetSdkVersion = editor.getTargetSdkVersion();
127     }
128 
RenderService(GraphicalEditorPart editor, Configuration configuration, ResourceResolver resourceResolver)129     private RenderService(GraphicalEditorPart editor,
130             Configuration configuration, ResourceResolver resourceResolver) {
131         mEditor = editor;
132 
133         mProject = editor.getProject();
134         LayoutCanvas canvas = editor.getCanvasControl();
135         mImageFactory = canvas.getImageOverlay();
136         FolderConfiguration folderConfig = configuration.getFullConfig();
137 
138         Device device = configuration.getDevice();
139         assert device != null;
140         mHardwareConfigHelper = new HardwareConfigHelper(device);
141         mHardwareConfigHelper.setOrientation(
142                 folderConfig.getScreenOrientationQualifier().getValue());
143 
144         mLayoutLib = editor.getReadyLayoutLib(true /*displayError*/);
145         mResourceResolver =  resourceResolver != null ? resourceResolver : editor.getResourceResolver();
146         mProjectCallback = editor.getProjectCallback(true /*reset*/, mLayoutLib);
147         mMinSdkVersion = editor.getMinSdkVersion();
148         mTargetSdkVersion = editor.getTargetSdkVersion();
149     }
150 
151     /**
152      * Returns true if this configuration supports the given rendering
153      * capability
154      *
155      * @param target the target to look up the layout library for
156      * @param capability the capability to check
157      * @return true if the capability is supported
158      */
supports( @onNull IAndroidTarget target, @NonNull Capability capability)159     public static boolean supports(
160             @NonNull IAndroidTarget target,
161             @NonNull Capability capability) {
162         Sdk sdk = Sdk.getCurrent();
163         if (sdk != null) {
164             AndroidTargetData targetData = sdk.getTargetData(target);
165             if (targetData != null) {
166                 LayoutLibrary layoutLib = targetData.getLayoutLibrary();
167                 if (layoutLib != null) {
168                     return layoutLib.supports(capability);
169                 }
170             }
171         }
172 
173         return false;
174     }
175 
176     /**
177      * Creates a new {@link RenderService} associated with the given editor.
178      *
179      * @param editor the editor to provide configuration data such as the render target
180      * @return a {@link RenderService} which can perform rendering services
181      */
create(GraphicalEditorPart editor)182     public static RenderService create(GraphicalEditorPart editor) {
183         RenderService renderService = new RenderService(editor);
184 
185         return renderService;
186     }
187 
188     /**
189      * Creates a new {@link RenderService} associated with the given editor.
190      *
191      * @param editor the editor to provide configuration data such as the render target
192      * @param configuration the configuration to use (and fallback to editor for the rest)
193      * @param resolver a resource resolver to use to look up resources
194      * @return a {@link RenderService} which can perform rendering services
195      */
create(GraphicalEditorPart editor, Configuration configuration, ResourceResolver resolver)196     public static RenderService create(GraphicalEditorPart editor,
197             Configuration configuration, ResourceResolver resolver) {
198         RenderService renderService = new RenderService(editor, configuration, resolver);
199 
200         return renderService;
201     }
202 
203     /**
204      * Renders the given model, using this editor's theme and screen settings, and returns
205      * the result as a {@link RenderSession}.
206      *
207      * @param model the model to be rendered, which can be different than the editor's own
208      *            {@link #getModel()}.
209      * @param width the width to use for the layout, or -1 to use the width of the screen
210      *            associated with this editor
211      * @param height the height to use for the layout, or -1 to use the height of the screen
212      *            associated with this editor
213      * @param explodeNodes a set of nodes to explode, or null for none
214      * @param overrideBgColor If non-null, use the given color as a background to render over
215      *        rather than the normal background requested by the theme
216      * @param noDecor If true, don't draw window decorations like the system bar
217      * @param logger a logger where rendering errors are reported
218      * @param renderingMode the {@link RenderingMode} to use for rendering
219      * @return the resulting rendered image wrapped in an {@link RenderSession}
220      */
221 
222     /**
223      * Sets the {@link LayoutLog} to be used during rendering. If none is specified, a
224      * silent logger will be used.
225      *
226      * @param logger the log to be used
227      * @return this (such that chains of setters can be stringed together)
228      */
setLog(LayoutLog logger)229     public RenderService setLog(LayoutLog logger) {
230         mLogger = logger;
231         return this;
232     }
233 
234     /**
235      * Sets the model to be rendered, which can be different than the editor's own
236      * {@link GraphicalEditorPart#getModel()}.
237      *
238      * @param model the model to be rendered
239      * @return this (such that chains of setters can be stringed together)
240      */
setModel(UiDocumentNode model)241     public RenderService setModel(UiDocumentNode model) {
242         mModel = model;
243         return this;
244     }
245 
246     /**
247      * Overrides the width and height to be used during rendering (which might be adjusted if
248      * the {@link #setRenderingMode(RenderingMode)} is {@link RenderingMode#FULL_EXPAND}.
249      *
250      * A value of -1 will make the rendering use the normal width and height coming from the
251      * {@link Configuration#getDevice()} object.
252      *
253      * @param overrideRenderWidth the width in pixels of the layout to be rendered
254      * @param overrideRenderHeight the height in pixels of the layout to be rendered
255      * @return this (such that chains of setters can be stringed together)
256      */
setOverrideRenderSize(int overrideRenderWidth, int overrideRenderHeight)257     public RenderService setOverrideRenderSize(int overrideRenderWidth, int overrideRenderHeight) {
258         mHardwareConfigHelper.setOverrideRenderSize(overrideRenderWidth, overrideRenderHeight);
259         return this;
260     }
261 
262     /**
263      * Sets the max width and height to be used during rendering (which might be adjusted if
264      * the {@link #setRenderingMode(RenderingMode)} is {@link RenderingMode#FULL_EXPAND}.
265      *
266      * A value of -1 will make the rendering use the normal width and height coming from the
267      * {@link Configuration#getDevice()} object.
268      *
269      * @param maxRenderWidth the max width in pixels of the layout to be rendered
270      * @param maxRenderHeight the max height in pixels of the layout to be rendered
271      * @return this (such that chains of setters can be stringed together)
272      */
setMaxRenderSize(int maxRenderWidth, int maxRenderHeight)273     public RenderService setMaxRenderSize(int maxRenderWidth, int maxRenderHeight) {
274         mHardwareConfigHelper.setMaxRenderSize(maxRenderWidth, maxRenderHeight);
275         return this;
276     }
277 
278     /**
279      * Sets the {@link RenderingMode} to be used during rendering. If none is specified,
280      * the default is {@link RenderingMode#NORMAL}.
281      *
282      * @param renderingMode the rendering mode to be used
283      * @return this (such that chains of setters can be stringed together)
284      */
setRenderingMode(RenderingMode renderingMode)285     public RenderService setRenderingMode(RenderingMode renderingMode) {
286         mRenderingMode = renderingMode;
287         return this;
288     }
289 
290     /**
291      * Sets the overriding background color to be used, if any. The color should be a
292      * bitmask of AARRGGBB. The default is null.
293      *
294      * @param overrideBgColor the overriding background color to be used in the rendering,
295      *            in the form of a AARRGGBB bitmask, or null to use no custom background.
296      * @return this (such that chains of setters can be stringed together)
297      */
setOverrideBgColor(Integer overrideBgColor)298     public RenderService setOverrideBgColor(Integer overrideBgColor) {
299         mOverrideBgColor = overrideBgColor;
300         return this;
301     }
302 
303     /**
304      * Sets whether the rendering should include decorations such as a system bar, an
305      * application bar etc depending on the SDK target and theme. The default is true.
306      *
307      * @param showDecorations true if the rendering should include system bars etc.
308      * @return this (such that chains of setters can be stringed together)
309      */
setDecorations(boolean showDecorations)310     public RenderService setDecorations(boolean showDecorations) {
311         mShowDecorations = showDecorations;
312         return this;
313     }
314 
315     /**
316      * Sets the nodes to expand during rendering. These will be padded with approximately
317      * 20 pixels and also highlighted by the {@link EmptyViewsOverlay}. The default is an
318      * empty collection.
319      *
320      * @param nodesToExpand the nodes to be expanded
321      * @return this (such that chains of setters can be stringed together)
322      */
setNodesToExpand(Set<UiElementNode> nodesToExpand)323     public RenderService setNodesToExpand(Set<UiElementNode> nodesToExpand) {
324         mExpandNodes = nodesToExpand;
325         return this;
326     }
327 
328     /**
329      * Sets the {@link Reference} to an outer layout that this layout should be rendered
330      * within. The outer layout <b>must</b> contain an include tag which points to this
331      * layout. The default is null.
332      *
333      * @param includedWithin a reference to an outer layout to render this layout within
334      * @return this (such that chains of setters can be stringed together)
335      */
setIncludedWithin(Reference includedWithin)336     public RenderService setIncludedWithin(Reference includedWithin) {
337         mIncludedWithin = includedWithin;
338         return this;
339     }
340 
341     /** Initializes any remaining optional fields after all setters have been called */
finishConfiguration()342     private void finishConfiguration() {
343         if (mLogger == null) {
344             // Silent logging
345             mLogger = new LayoutLog();
346         }
347     }
348 
349     /**
350      * Renders the model and returns the result as a {@link RenderSession}.
351      * @return the {@link RenderSession} resulting from rendering the current model
352      */
createRenderSession()353     public RenderSession createRenderSession() {
354         assert mModel != null : "Incomplete service config";
355         finishConfiguration();
356 
357         if (mResourceResolver == null) {
358             // Abort the rendering if the resources are not found.
359             return null;
360         }
361 
362         HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig();
363 
364         UiElementPullParser modelParser = new UiElementPullParser(mModel,
365                 false, mExpandNodes, hardwareConfig.getDensity(), mProject);
366         ILayoutPullParser topParser = modelParser;
367 
368         // Code to support editing included layout
369         // first reset the layout parser just in case.
370         mProjectCallback.setLayoutParser(null, null);
371 
372         if (mIncludedWithin != null) {
373             // Outer layout name:
374             String contextLayoutName = mIncludedWithin.getName();
375 
376             // Find the layout file.
377             ResourceValue contextLayout = mResourceResolver.findResValue(
378                     LAYOUT_RESOURCE_PREFIX + contextLayoutName, false  /* forceFrameworkOnly*/);
379             if (contextLayout != null) {
380                 File layoutFile = new File(contextLayout.getValue());
381                 if (layoutFile.isFile()) {
382                     try {
383                         // Get the name of the layout actually being edited, without the extension
384                         // as it's what IXmlPullParser.getParser(String) will receive.
385                         String queryLayoutName = mEditor.getLayoutResourceName();
386                         mProjectCallback.setLayoutParser(queryLayoutName, modelParser);
387                         topParser = new ContextPullParser(mProjectCallback, layoutFile);
388                         topParser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
389                         String xmlText = Files.toString(layoutFile, Charsets.UTF_8);
390                         topParser.setInput(new StringReader(xmlText));
391                     } catch (IOException e) {
392                         AdtPlugin.log(e, null);
393                     } catch (XmlPullParserException e) {
394                         AdtPlugin.log(e, null);
395                     }
396                 }
397             }
398         }
399 
400         SessionParams params = new SessionParams(
401                 topParser,
402                 mRenderingMode,
403                 mProject /* projectKey */,
404                 hardwareConfig,
405                 mResourceResolver,
406                 mProjectCallback,
407                 mMinSdkVersion,
408                 mTargetSdkVersion,
409                 mLogger);
410 
411         // Request margin and baseline information.
412         // TODO: Be smarter about setting this; start without it, and on the first request
413         // for an extended view info, re-render in the same session, and then set a flag
414         // which will cause this to create extended view info each time from then on in the
415         // same session
416         params.setExtendedViewInfoMode(true);
417 
418         if (!mShowDecorations) {
419             params.setForceNoDecor();
420         } else {
421             ManifestInfo manifestInfo = ManifestInfo.get(mProject);
422             try {
423                 params.setAppLabel(manifestInfo.getApplicationLabel());
424                 params.setAppIcon(manifestInfo.getApplicationIcon());
425             } catch (Exception e) {
426                 // ignore.
427             }
428         }
429 
430         if (mOverrideBgColor != null) {
431             params.setOverrideBgColor(mOverrideBgColor.intValue());
432         }
433 
434         // set the Image Overlay as the image factory.
435         params.setImageFactory(mImageFactory);
436 
437         try {
438             mProjectCallback.setLogger(mLogger);
439             mProjectCallback.setResourceResolver(mResourceResolver);
440             return mLayoutLib.createSession(params);
441         } catch (RuntimeException t) {
442             // Exceptions from the bridge
443             mLogger.error(null, t.getLocalizedMessage(), t, null);
444             throw t;
445         } finally {
446             mProjectCallback.setLogger(null);
447             mProjectCallback.setResourceResolver(null);
448         }
449     }
450 
451     /**
452      * Renders the given resource value (which should refer to a drawable) and returns it
453      * as an image
454      *
455      * @param drawableResourceValue the drawable resource value to be rendered, or null
456      * @return the image, or null if something went wrong
457      */
renderDrawable(ResourceValue drawableResourceValue)458     public BufferedImage renderDrawable(ResourceValue drawableResourceValue) {
459         if (drawableResourceValue == null) {
460             return null;
461         }
462 
463         finishConfiguration();
464 
465         HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig();
466 
467         DrawableParams params = new DrawableParams(drawableResourceValue, mProject, hardwareConfig,
468                 mResourceResolver, mProjectCallback, mMinSdkVersion,
469                 mTargetSdkVersion, mLogger);
470         params.setForceNoDecor();
471         Result result = mLayoutLib.renderDrawable(params);
472         if (result != null && result.isSuccess()) {
473             Object data = result.getData();
474             if (data instanceof BufferedImage) {
475                 return (BufferedImage) data;
476             }
477         }
478 
479         return null;
480     }
481 
482     /**
483      * Measure the children of the given parent node, applying the given filter to the
484      * pull parser's attribute values.
485      *
486      * @param parent the parent node to measure children for
487      * @param filter the filter to apply to the attribute values
488      * @return a map from node children of the parent to new bounds of the nodes
489      */
measureChildren(INode parent, final IClientRulesEngine.AttributeFilter filter)490     public Map<INode, Rect> measureChildren(INode parent,
491             final IClientRulesEngine.AttributeFilter filter) {
492         finishConfiguration();
493         HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig();
494 
495         final NodeFactory mNodeFactory = mEditor.getCanvasControl().getNodeFactory();
496         UiElementNode parentNode = ((NodeProxy) parent).getNode();
497         UiElementPullParser topParser = new UiElementPullParser(parentNode,
498                 false, Collections.<UiElementNode>emptySet(), hardwareConfig.getDensity(),
499                 mProject) {
500             @Override
501             public String getAttributeValue(String namespace, String localName) {
502                 if (filter != null) {
503                     Object cookie = getViewCookie();
504                     if (cookie instanceof UiViewElementNode) {
505                         NodeProxy node = mNodeFactory.create((UiViewElementNode) cookie);
506                         if (node != null) {
507                             String value = filter.getAttribute(node, namespace, localName);
508                             if (value != null) {
509                                 return value;
510                             }
511                             // null means no preference, not "unset".
512                         }
513                     }
514                 }
515 
516                 return super.getAttributeValue(namespace, localName);
517             }
518 
519             /**
520              * The parser usually assumes that the top level node is a document node that
521              * should be skipped, and that's not the case when we render in the middle of
522              * the tree, so override {@link UiElementPullParser#onNextFromStartDocument}
523              * to change this behavior
524              */
525             @Override
526             public void onNextFromStartDocument() {
527                 mParsingState = START_TAG;
528             }
529         };
530 
531         SessionParams params = new SessionParams(
532                 topParser,
533                 RenderingMode.FULL_EXPAND,
534                 mProject /* projectKey */,
535                 hardwareConfig,
536                 mResourceResolver,
537                 mProjectCallback,
538                 mMinSdkVersion,
539                 mTargetSdkVersion,
540                 mLogger);
541         params.setLayoutOnly();
542         params.setForceNoDecor();
543 
544         RenderSession session = null;
545         try {
546             mProjectCallback.setLogger(mLogger);
547             mProjectCallback.setResourceResolver(mResourceResolver);
548             session = mLayoutLib.createSession(params);
549             if (session.getResult().isSuccess()) {
550                 assert session.getRootViews().size() == 1;
551                 ViewInfo root = session.getRootViews().get(0);
552                 List<ViewInfo> children = root.getChildren();
553                 Map<INode, Rect> map = new HashMap<INode, Rect>(children.size());
554                 for (ViewInfo info : children) {
555                     if (info.getCookie() instanceof UiViewElementNode) {
556                         UiViewElementNode uiNode = (UiViewElementNode) info.getCookie();
557                         NodeProxy node = mNodeFactory.create(uiNode);
558                         map.put(node, new Rect(info.getLeft(), info.getTop(),
559                                 info.getRight() - info.getLeft(),
560                                 info.getBottom() - info.getTop()));
561                     }
562                 }
563 
564                 return map;
565             }
566         } catch (RuntimeException t) {
567             // Exceptions from the bridge
568             mLogger.error(null, t.getLocalizedMessage(), t, null);
569             throw t;
570         } finally {
571             mProjectCallback.setLogger(null);
572             mProjectCallback.setResourceResolver(null);
573             if (session != null) {
574                 session.dispose();
575             }
576         }
577 
578         return null;
579     }
580 }
581