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