/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.idea.rendering; import com.android.ide.common.rendering.HardwareConfigHelper; import com.android.ide.common.rendering.LayoutLibrary; import com.android.ide.common.rendering.RenderSecurityManager; import com.android.ide.common.rendering.api.*; import com.android.ide.common.rendering.api.SessionParams.RenderingMode; import com.android.ide.common.resources.ResourceResolver; import com.android.ide.common.resources.configuration.LayoutDirectionQualifier; import com.android.resources.LayoutDirection; import com.android.resources.ResourceFolderType; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.devices.Device; import com.android.sdklib.repository.FullRevision; import com.android.sdklib.repository.MajorRevision; import com.android.sdklib.repository.descriptors.IPkgDesc; import com.android.sdklib.repository.descriptors.PkgDesc; import com.android.tools.idea.AndroidPsiUtils; import com.android.tools.idea.configurations.Configuration; import com.android.tools.idea.configurations.RenderContext; import com.android.tools.idea.gradle.structure.AndroidProjectSettingsService; import com.android.tools.idea.gradle.util.Projects; import com.android.tools.idea.model.AndroidModuleInfo; import com.android.tools.idea.model.ManifestInfo; import com.android.tools.idea.model.ManifestInfo.ActivityAttributes; import com.android.tools.idea.rendering.multi.CompatibilityRenderTarget; import com.android.tools.idea.rendering.multi.RenderPreviewMode; import com.android.tools.idea.sdk.wizard.SdkQuickfixWizard; import com.android.utils.HtmlBuilder; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Computable; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.maven.AndroidMavenUtil; import org.jetbrains.android.sdk.AndroidPlatform; import org.jetbrains.android.sdk.AndroidSdkAdditionalData; import org.jetbrains.android.sdk.AndroidSdkType; import org.jetbrains.android.sdk.AndroidSdkUtils; import org.jetbrains.android.uipreview.RenderingException; import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.w3c.dom.Element; import org.xmlpull.v1.XmlPullParserException; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Set; import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW; import static com.android.SdkConstants.SCROLL_VIEW; import static com.intellij.lang.annotation.HighlightSeverity.ERROR; import static com.intellij.lang.annotation.HighlightSeverity.WARNING; /** * The {@link RenderService} provides rendering and layout information for * Android layouts. This is a wrapper around the layout library. */ public class RenderService implements IImageFactory { @NotNull private final Module myModule; @NotNull private final XmlFile myPsiFile; @NotNull private final RenderLogger myLogger; @NotNull private final LayoutlibCallback myLayoutlibCallback; private final AndroidVersion myMinSdkVersion; private final AndroidVersion myTargetSdkVersion; @NotNull private final LayoutLibrary myLayoutLib; @NotNull private final HardwareConfigHelper myHardwareConfigHelper; @Nullable private IncludeReference myIncludedWithin; @NotNull private RenderingMode myRenderingMode = RenderingMode.NORMAL; @Nullable private Integer myOverrideBgColor; private boolean myShowDecorations = true; @NotNull private final Configuration myConfiguration; private long myTimeout; @Nullable private Set<XmlTag> myExpandNodes; @Nullable private RenderContext myRenderContext; @NotNull private final Locale myLocale; private final Object myCredential = new Object(); private ResourceFolderType myFolderType; /** * Creates a new {@link RenderService} associated with the given editor. * * @return a {@link RenderService} which can perform rendering services */ @Nullable public static RenderService create(@NotNull final AndroidFacet facet, @NotNull final Module module, @NotNull final PsiFile psiFile, @NotNull final Configuration configuration, @NotNull final RenderLogger logger, @Nullable final RenderContext renderContext) { final Project project = module.getProject(); AndroidPlatform platform = getPlatform(module); if (platform == null) { if (!AndroidMavenUtil.isMavenizedModule(module)) { RenderProblem.Html message = RenderProblem.create(ERROR); logger.addMessage(message); message.getHtmlBuilder().addLink("No Android SDK found. Please ", "configure", " an Android SDK.", logger.getLinkManager().createRunnableLink(new Runnable() { @Override public void run() { ProjectSettingsService service = ProjectSettingsService.getInstance(project); if (Projects.isGradleProject(project) && service instanceof AndroidProjectSettingsService) { ((AndroidProjectSettingsService)service).openSdkSettings(); return; } AndroidSdkUtils.openModuleDependenciesConfigurable(module); } })); } else { String message = AndroidBundle.message("android.maven.cannot.parse.android.sdk.error", module.getName()); logger.addMessage(RenderProblem.createPlain(ERROR, message)); } return null; } IAndroidTarget target = configuration.getTarget(); if (target == null) { logger.addMessage(RenderProblem.createPlain(ERROR, "No render target was chosen")); return null; } warnIfObsoleteLayoutLib(module, logger, renderContext, target); LayoutLibrary layoutLib; try { layoutLib = platform.getSdkData().getTargetData(target).getLayoutLibrary(project); if (layoutLib == null) { String message = AndroidBundle.message("android.layout.preview.cannot.load.library.error"); logger.addMessage(RenderProblem.createPlain(ERROR, message)); return null; } } catch (RenderingException e) { String message = e.getPresentableMessage(); message = message != null ? message : AndroidBundle.message("android.layout.preview.default.error.message"); logger.addMessage(RenderProblem.createPlain(ERROR, message, module.getProject(), logger.getLinkManager(), e)); return null; } catch (IOException e) { final String message = e.getMessage(); logger.error(null, "I/O error: " + (message != null ? ": " + message : ""), e); return null; } Device device = configuration.getDevice(); if (device == null) { logger.addMessage(RenderProblem.createPlain(ERROR, "No device selected")); return null; } RenderService service = new RenderService(facet, module, psiFile, configuration, logger, layoutLib, device); if (renderContext != null) { service.setRenderContext(renderContext); } return service; } protected static void warnIfObsoleteLayoutLib(final Module module, RenderLogger logger, final RenderContext renderContext, IAndroidTarget target) { if (!ourWarnAboutObsoleteLayoutLibVersions) { return; } final AndroidVersion version = target.getVersion(); final int revision; // Look up the current minimum required version for layoutlib for each API level. Note that these // are minimum revisions; if a later version is available, it will be installed. switch (version.getFeatureLevel()) { case 21: if (version.isPreview()) { revision = 4; } else { revision = 1; } break; case 20: revision = 1; break; case 19: revision = 3; break; case 18: revision = 2; break; case 17: revision = 2; break; case 16: revision = 4; break; case 15: revision = 3; break; case 14: revision = 3; break; case 13: revision = 1; break; case 12: revision = 3; break; case 11: revision = 2; break; case 10: revision = 2; break; case 8: revision = 3; break; default: revision = -1; break; } if (revision >= 0 && target.getRevision() < revision) { RenderProblem.Html problem = RenderProblem.create(WARNING); problem.tag("obsoleteLayoutlib"); HtmlBuilder builder = problem.getHtmlBuilder(); builder.add("Using an obsolete version of the " + target.getVersionName() + " layout library which contains many known bugs: "); builder.addLink("Install Update", logger.getLinkManager().createRunnableLink(new Runnable() { @Override public void run() { // Don't warn again //noinspection AssignmentToStaticFieldFromInstanceMethod ourWarnAboutObsoleteLayoutLibVersions = false; List<IPkgDesc> requested = Lists.newArrayList(); // The revision to install. Note that this will install a higher version than this if available; // e.g. even if we ask for version 4, if revision 7 is available it will be installed, not revision 4. requested.add(PkgDesc.Builder.newPlatform(version, new MajorRevision(revision), FullRevision.NOT_SPECIFIED).create()); SdkQuickfixWizard wizard = new SdkQuickfixWizard(module.getProject(), null, requested); wizard.init(); if (wizard.showAndGet()) { if (renderContext != null) { // Force the target to be recomputed; this will pick up the new revision object from the local sdk. Configuration configuration = renderContext.getConfiguration(); if (configuration != null) { configuration.getConfigurationManager().setTarget(null); } renderContext.requestRender(); // However, due to issue https://code.google.com/p/android/issues/detail?id=76096 it may not yet // take effect. Messages.showInfoMessage(module.getProject(), "Note: Due to a bug you may need to restart the IDE for the new layout library to fully take effect", "Restart Recommended"); } } } })); builder.addLink(", ", "Ignore For Now", null, logger.getLinkManager().createRunnableLink(new Runnable() { @Override public void run() { //noinspection AssignmentToStaticFieldFromInstanceMethod ourWarnAboutObsoleteLayoutLibVersions = false; if (renderContext != null) { renderContext.requestRender(); } } })); logger.addMessage(problem); } } private static boolean ourWarnAboutObsoleteLayoutLibVersions = true; /** * Use the {@link #create} factory instead */ private RenderService(@NotNull AndroidFacet facet, @NotNull Module module, @NotNull PsiFile psiFile, @NotNull Configuration configuration, @NotNull RenderLogger logger, @NotNull LayoutLibrary layoutLib, @NotNull Device device) { myModule = module; myLogger = logger; myLogger.setCredential(myCredential); if (!(psiFile instanceof XmlFile)) { throw new IllegalArgumentException("Can only render XML files: " + psiFile.getClass().getName()); } myPsiFile = (XmlFile)psiFile; myConfiguration = configuration; myHardwareConfigHelper = new HardwareConfigHelper(device); myHardwareConfigHelper.setOrientation(configuration.getFullConfig().getScreenOrientationQualifier().getValue()); myLayoutLib = layoutLib; AppResourceRepository appResources = AppResourceRepository.getAppResources(facet, true); myLayoutlibCallback = new LayoutlibCallback(myLayoutLib, appResources, myModule, facet, myLogger, myCredential, this); myLayoutlibCallback.loadAndParseRClass(); AndroidModuleInfo moduleInfo = AndroidModuleInfo.get(facet); myMinSdkVersion = moduleInfo.getMinSdkVersion(); myTargetSdkVersion = moduleInfo.getTargetSdkVersion(); myLocale = configuration.getLocale(); ApplicationManager.getApplication().runReadAction(new Runnable() { @Override public void run() { myFolderType = ResourceHelper.getFolderType(myPsiFile); } }); } @Nullable public AndroidPlatform getPlatform() { return getPlatform(myModule); } @Nullable private static AndroidPlatform getPlatform(@NotNull Module module) { Sdk sdk = ModuleRootManager.getInstance(module).getSdk(); if (sdk == null || !(sdk.getSdkType() instanceof AndroidSdkType)) { return null; } AndroidSdkAdditionalData data = (AndroidSdkAdditionalData)sdk.getSdkAdditionalData(); if (data == null) { return null; } return data.getAndroidPlatform(); } /** * Returns the {@link ResourceResolver} for this editor * * @return the resolver used to resolve resources for the current configuration of * this editor, or null */ @Nullable public ResourceResolver getResourceResolver() { return myConfiguration.getResourceResolver(); } @NotNull public Configuration getConfiguration() { return myConfiguration; } @Nullable public ResourceFolderType getFolderType() { return myFolderType; } @NotNull public Module getModule() { return myModule; } @NotNull public RenderLogger getLogger() { return myLogger; } @Nullable public Set<XmlTag> getExpandNodes() { return myExpandNodes; } @NotNull public HardwareConfigHelper getHardwareConfigHelper() { return myHardwareConfigHelper; } public boolean getShowDecorations() { return myShowDecorations; } public void dispose() { myLayoutlibCallback.setLogger(null); myLayoutlibCallback.setResourceResolver(null); } /** * Overrides the width and height to be used during rendering (which might be adjusted if * the {@link #setRenderingMode(com.android.ide.common.rendering.api.SessionParams.RenderingMode)} is * {@link com.android.ide.common.rendering.api.SessionParams.RenderingMode#FULL_EXPAND}. * <p/> * A value of -1 will make the rendering use the normal width and height coming from the * {@link Configuration#getDevice()} object. * * @param overrideRenderWidth the width in pixels of the layout to be rendered * @param overrideRenderHeight the height in pixels of the layout to be rendered * @return this (such that chains of setters can be stringed together) */ public RenderService setOverrideRenderSize(int overrideRenderWidth, int overrideRenderHeight) { myHardwareConfigHelper.setOverrideRenderSize(overrideRenderWidth, overrideRenderHeight); return this; } /** * Sets the max width and height to be used during rendering (which might be adjusted if * the {@link #setRenderingMode(com.android.ide.common.rendering.api.SessionParams.RenderingMode)} is * {@link com.android.ide.common.rendering.api.SessionParams.RenderingMode#FULL_EXPAND}. * <p/> * A value of -1 will make the rendering use the normal width and height coming from the * {@link Configuration#getDevice()} object. * * @param maxRenderWidth the max width in pixels of the layout to be rendered * @param maxRenderHeight the max height in pixels of the layout to be rendered * @return this (such that chains of setters can be stringed together) */ public RenderService setMaxRenderSize(int maxRenderWidth, int maxRenderHeight) { myHardwareConfigHelper.setMaxRenderSize(maxRenderWidth, maxRenderHeight); return this; } /** * Sets the {@link com.android.ide.common.rendering.api.SessionParams.RenderingMode} to be used during rendering. If none is specified, * the default is {@link com.android.ide.common.rendering.api.SessionParams.RenderingMode#NORMAL}. * * @param renderingMode the rendering mode to be used * @return this (such that chains of setters can be stringed together) */ public RenderService setRenderingMode(@NotNull RenderingMode renderingMode) { myRenderingMode = renderingMode; return this; } /** Returns the {@link RenderingMode} to be used */ @NotNull public RenderingMode getRenderingMode() { return myRenderingMode; } public RenderService setTimeout(long timeout) { myTimeout = timeout; return this; } /** * Sets the overriding background color to be used, if any. The color should be a * bitmask of AARRGGBB. The default is null. * * @param overrideBgColor the overriding background color to be used in the rendering, * in the form of a AARRGGBB bitmask, or null to use no custom background. * @return this (such that chains of setters can be stringed together) */ @NotNull public RenderService setOverrideBgColor(@Nullable Integer overrideBgColor) { myOverrideBgColor = overrideBgColor; return this; } /** * Sets whether the rendering should include decorations such as a system bar, an * application bar etc depending on the SDK target and theme. The default is true. * * @param showDecorations true if the rendering should include system bars etc. * @return this (such that chains of setters can be stringed together) */ public RenderService setDecorations(boolean showDecorations) { myShowDecorations = showDecorations; return this; } /** * Gets the context for the usage of this {@link RenderService}, which can * control for example how {@code <fragment/>} tags are processed when missing * preview data */ @Nullable public RenderContext getRenderContext() { return myRenderContext; } /** * Sets the context for the usage of this {@link RenderService}, which can * control for example how {@code <fragment/>} tags are processed when missing * preview data * * @param renderContext the render context * @return this, for constructor chaining */ @Nullable public RenderService setRenderContext(@Nullable RenderContext renderContext) { myRenderContext = renderContext; return this; } /** * Sets the nodes to expand during rendering. These will be padded with approximately * 20 pixels. The default is null. * * @param nodesToExpand the nodes to be expanded * @return this (such that chains of setters can be stringed together) */ @NotNull public RenderService setNodesToExpand(@Nullable Set<XmlTag> nodesToExpand) { myExpandNodes = nodesToExpand; return this; } /** * Sets the {@link IncludeReference} to an outer layout that this layout should be rendered * within. The outer layout <b>must</b> contain an include tag which points to this * layout. If not set explicitly to {@link IncludeReference#NONE}, it will look at the * root tag of the rendered layout and if {@link IncludeReference#ATTR_RENDER_IN} has * been set it will use that layout. * * @param includedWithin a reference to an outer layout to render this layout within * @return this (such that chains of setters can be stringed together) */ @NotNull public RenderService setIncludedWithin(@Nullable IncludeReference includedWithin) { myIncludedWithin = includedWithin; return this; } /** * Returns the layout to be included */ @NotNull public IncludeReference getIncludedWithin() { return myIncludedWithin != null ? myIncludedWithin : IncludeReference.NONE; } /** * Renders the model and returns the result as a {@link com.android.ide.common.rendering.api.RenderSession}. * * @return the {@link RenderResult resulting from rendering the current model */ @Nullable private RenderResult createRenderSession() { ResourceResolver resolver = getResourceResolver(); if (resolver == null) { // Abort the rendering if the resources are not found. return null; } ILayoutPullParser modelParser = LayoutPullParserFactory.create(this); if (modelParser == null) { return null; } myLayoutlibCallback.reset(); ILayoutPullParser includingParser = getIncludingLayoutParser(resolver, modelParser); if (includingParser != null) { modelParser = includingParser; } IAndroidTarget target = myConfiguration.getTarget(); int simulatedPlatform = target instanceof CompatibilityRenderTarget ? target.getVersion().getApiLevel() : 0; HardwareConfig hardwareConfig = myHardwareConfigHelper.getConfig(); final SessionParams params = new SessionParams(modelParser, myRenderingMode, myModule /* projectKey */, hardwareConfig, resolver, myLayoutlibCallback, myMinSdkVersion.getApiLevel(), myTargetSdkVersion.getApiLevel(), myLogger, simulatedPlatform); // Request margin and baseline information. // TODO: Be smarter about setting this; start without it, and on the first request // for an extended view info, re-render in the same session, and then set a flag // which will cause this to create extended view info each time from then on in the // same session params.setExtendedViewInfoMode(true); ManifestInfo manifestInfo = ManifestInfo.get(myModule); LayoutDirectionQualifier qualifier = myConfiguration.getFullConfig().getLayoutDirectionQualifier(); if (qualifier != null && qualifier.getValue() == LayoutDirection.RTL) { params.setRtlSupport(true); // We don't have a flag to force RTL regardless of locale, so just pick a RTL locale (note that // this is decoupled from resource lookup) params.setLocale("ur"); } else { params.setLocale(myLocale.toLocaleId()); try { params.setRtlSupport(manifestInfo.isRtlSupported()); } catch (Exception e) { // ignore. } } // Don't show navigation buttons on older platforms Device device = myConfiguration.getDevice(); if (!myShowDecorations || HardwareConfigHelper.isWear(device)) { params.setForceNoDecor(); } else { try { params.setAppLabel(manifestInfo.getApplicationLabel()); params.setAppIcon(manifestInfo.getApplicationIcon()); String activity = myConfiguration.getActivity(); if (activity != null) { params.setActivityName(activity); ActivityAttributes attributes = manifestInfo.getActivityAttributes(activity); if (attributes != null) { if (attributes.getLabel() != null) { params.setAppLabel(attributes.getLabel()); } if (attributes.getIcon() != null) { params.setAppIcon(attributes.getIcon()); } } } } catch (Exception e) { // ignore. } } if (myOverrideBgColor != null) { params.setOverrideBgColor(myOverrideBgColor.intValue()); } else if (requiresTransparency()) { params.setOverrideBgColor(0); } params.setImageFactory(this); if (myTimeout > 0) { params.setTimeout(myTimeout); } try { myLayoutlibCallback.setLogger(myLogger); myLayoutlibCallback.setResourceResolver(resolver); RenderResult result = ApplicationManager.getApplication().runReadAction(new Computable<RenderResult>() { @NotNull @Override public RenderResult compute() { RenderSecurityManager securityManager = createSecurityManager(); securityManager.setActive(true, myCredential); try { int retries = 0; RenderSession session = null; while (retries < 10) { session = myLayoutLib.createSession(params); Result result = session.getResult(); if (result.getStatus() != Result.Status.ERROR_TIMEOUT) { // Sometimes happens at startup; treat it as a timeout; typically a retry fixes it if (!result.isSuccess() && "The main Looper has already been prepared.".equals(result.getErrorMessage())) { retries++; continue; } break; } retries++; } return new RenderResult(RenderService.this, session, myPsiFile, myLogger); } finally { securityManager.dispose(myCredential); } } }); addDiagnostics(result.getSession()); result.setIncludedWithin(myIncludedWithin); return result; } catch (RuntimeException t) { // Exceptions from the bridge myLogger.error(null, t.getLocalizedMessage(), t, null); throw t; } } @Nullable private ILayoutPullParser getIncludingLayoutParser(ResourceResolver resolver, ILayoutPullParser modelParser) { // Code to support editing included layout if (myIncludedWithin == null) { String layout = IncludeReference.getIncludingLayout(myPsiFile); myIncludedWithin = layout != null ? IncludeReference.get(myModule, myPsiFile, resolver) : IncludeReference.NONE; } if (myIncludedWithin != IncludeReference.NONE) { assert Comparing.equal(myIncludedWithin.getToFile(), myPsiFile.getVirtualFile()); // TODO: Validate that we're really including the same layout here! //ResourceValue contextLayout = resolver.findResValue(myIncludedWithin.getFromResourceUrl(), false /* forceFrameworkOnly*/); //if (contextLayout != null) { // File layoutFile = new File(contextLayout.getValue()); // if (layoutFile.isFile()) { // VirtualFile layoutVirtualFile = myIncludedWithin.getFromFile(); try { // Get the name of the layout actually being edited, without the extension // as it's what IXmlPullParser.getParser(String) will receive. String queryLayoutName = ResourceHelper.getResourceName(myPsiFile); myLayoutlibCallback.setLayoutParser(queryLayoutName, modelParser); // Attempt to read from PSI ILayoutPullParser topParser; topParser = null; PsiFile psiFile = AndroidPsiUtils.getPsiFileSafely(myModule.getProject(), layoutVirtualFile); if (psiFile instanceof XmlFile) { LayoutPsiPullParser parser = LayoutPsiPullParser.create((XmlFile)psiFile, myLogger); // For included layouts, don't see view cookies; we want the leaf to point back to the include tag parser.setProvideViewCookies(false); topParser = parser; } if (topParser == null) { topParser = LayoutFilePullParser.create(myLayoutlibCallback, myIncludedWithin.getFromPath()); } return topParser; } catch (IOException e) { myLogger.error(null, String.format("Could not read layout file %1$s", myIncludedWithin.getFromPath()), e); } catch (XmlPullParserException e) { myLogger.error(null, String.format("XML parsing error: %1$s", e.getMessage()), e.getDetail() != null ? e.getDetail() : e); } } return null; } private RenderSecurityManager createSecurityManager() { String projectPath = null; String sdkPath = null; if (RenderSecurityManager.RESTRICT_READS) { projectPath = myModule.getProject().getBasePath(); AndroidPlatform platform = getPlatform(); if (platform != null) { sdkPath = platform.getSdkData().getLocation().getPath(); } } @SuppressWarnings("ConstantConditions") RenderSecurityManager securityManager = new RenderSecurityManager(sdkPath, projectPath); securityManager.setLogger(new LogWrapper(RenderLogger.LOG)); securityManager.setAppTempDir(PathManager.getTempPath()); return securityManager; } /** Returns true if the given file can be rendered */ public static boolean canRender(@Nullable PsiFile file) { return file != null && LayoutPullParserFactory.isSupported(file); } private static final Object RENDERING_LOCK = new Object(); @Nullable public RenderResult render() { // During development only: //assert !ApplicationManager.getApplication().isReadAccessAllowed() : "Do not hold read lock during render!"; synchronized (RENDERING_LOCK) { RenderResult renderResult; try { renderResult = createRenderSession(); } catch (final Exception e) { String message = e.getMessage(); if (message == null) { message = e.toString(); } myLogger.addMessage(RenderProblem.createPlain(ERROR, message, myModule.getProject(), myLogger.getLinkManager(), e)); renderResult = new RenderResult(this, null, myPsiFile, myLogger); } return renderResult; } } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") private void addDiagnostics(@Nullable RenderSession session) { if (session == null) { return; } Result r = session.getResult(); if (!myLogger.hasProblems() && !r.isSuccess()) { if (r.getException() != null || r.getErrorMessage() != null) { myLogger.error(null, r.getErrorMessage(), r.getException(), null); } else if (r.getStatus() == Result.Status.ERROR_TIMEOUT) { myLogger.error(null, "Rendering timed out.", null); } else { myLogger.error(null, "Unknown render problem: " + r.getStatus(), null); } } else if (myIncludedWithin != null && myIncludedWithin != IncludeReference.NONE) { ILayoutPullParser layoutEmbeddedParser = myLayoutlibCallback.getLayoutEmbeddedParser(); if (layoutEmbeddedParser != null) { // Should have been nulled out if used myLogger.error(null, String.format("The surrounding layout (%1$s) did not actually include this layout. " + "Remove tools:" + IncludeReference.ATTR_RENDER_IN + "=... from the root tag.", myIncludedWithin.getFromResourceUrl()), null); } } } /** * Renders the given resource value (which should refer to a drawable) and returns it * as an image * * @param drawableResourceValue the drawable resource value to be rendered, or null * @return the image, or null if something went wrong */ @Nullable public BufferedImage renderDrawable(ResourceValue drawableResourceValue) { if (drawableResourceValue == null) { return null; } HardwareConfig hardwareConfig = myHardwareConfigHelper.getConfig(); DrawableParams params = new DrawableParams(drawableResourceValue, myModule, hardwareConfig, getResourceResolver(), myLayoutlibCallback, myMinSdkVersion.getApiLevel(), myTargetSdkVersion.getApiLevel(), myLogger); params.setForceNoDecor(); Result result = myLayoutLib.renderDrawable(params); if (result != null && result.isSuccess()) { Object data = result.getData(); if (data instanceof BufferedImage) { return (BufferedImage)data; } } return null; } @NotNull public LayoutLibrary getLayoutLib() { return myLayoutLib; } @NotNull public LayoutlibCallback getLayoutlibCallback() { return myLayoutlibCallback; } @NotNull public XmlFile getPsiFile() { return myPsiFile; } public boolean supportsCapability(@NotNull Capability capability) { return myLayoutLib.supports(capability); } public static boolean supportsCapability(@NotNull final Module module, @NotNull IAndroidTarget target, @NotNull Capability capability) { Project project = module.getProject(); AndroidPlatform platform = getPlatform(module); if (platform != null) { try { LayoutLibrary library = platform.getSdkData().getTargetData(target).getLayoutLibrary(project); if (library != null) { return library.supports(capability); } } catch (RenderingException e) { // Ignore: if service can't be found, that capability isn't available } catch (IOException e) { // Ditto } } return false; } @Nullable public static LayoutLibrary getLayoutLibrary(@Nullable final Module module, @Nullable IAndroidTarget target) { if (module == null || target == null) { return null; } Project project = module.getProject(); AndroidPlatform platform = getPlatform(module); if (platform != null) { try { return platform.getSdkData().getTargetData(target).getLayoutLibrary(project); } catch (RenderingException e) { // Ignore. } catch (IOException e) { // Ditto } } return null; } /** Returns true if this service can render a non-rectangular shape */ public boolean isNonRectangular() { // Drawable images can have non-rectangular shapes; we need to ensure that we blank out the // background with full alpha return getFolderType() == ResourceFolderType.DRAWABLE; } /** Returns true if this service requires rendering into a transparent/alpha channel image */ public boolean requiresTransparency() { // Drawable images can have non-rectangular shapes; we need to ensure that we blank out the // background with full alpha return isNonRectangular(); } // ---- Implements IImageFactory ---- /** TODO: reuse image across subsequent render operations if the size is the same */ @SuppressWarnings("UndesirableClassUsage") // Don't need Retina for layoutlib rendering; will scale down anyway @Override public BufferedImage getImage(int width, int height) { return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); } /** * Notifies the render service that it is being used in design mode for this layout. * For example, that means that when rendering a ScrollView, it should measure the necessary * vertical space, and size the layout according to the needs rather than the available * device size. * <p> * We don't want to do this when for example offering thumbnail previews of the various * layouts. * * @param file the layout file, if any */ public void useDesignMode(@Nullable final PsiFile file) { if (file == null) { return; } String tagName = ApplicationManager.getApplication().runReadAction(new Computable<String>() { @Nullable @Override public String compute() { if (file instanceof XmlFile) { XmlTag root = ((XmlFile)file).getRootTag(); if (root != null) { return root.getName(); } } return null; } }); if (tagName != null) { // In multi configuration rendering, clip to screen bounds RenderPreviewMode currentMode = RenderPreviewMode.getCurrent(); if (currentMode != RenderPreviewMode.NONE) { return; } if (SCROLL_VIEW.equals(tagName)) { setRenderingMode(RenderingMode.V_SCROLL); setDecorations(false); } else if (HORIZONTAL_SCROLL_VIEW.equals(tagName)) { setRenderingMode(RenderingMode.H_SCROLL); setDecorations(false); } } } /** * Measure the children of the given parent element. * * @param parent the parent element whose children should be measured * @return a list of root view infos */ @Nullable public List<ViewInfo> measure(Element parent) { ILayoutPullParser modelParser = new DomPullParser(parent); RenderSession session = measure(modelParser); if (session != null) { Result result = session.getResult(); if (result != null && result.isSuccess()) { assert session.getRootViews().size() == 1; return session.getRootViews(); } } return null; } /** * Measure the children of the given parent tag, applying the given filter to the * pull parser's attribute values. * * @param parent the parent tag to measure children for * @param filter the filter to apply to the attribute values * @return a map from the children of the parent to new bounds of the children */ @Nullable public Map<XmlTag, ViewInfo> measureChildren(XmlTag parent, final AttributeFilter filter) { ILayoutPullParser modelParser = LayoutPsiPullParser.create(filter, parent, myLogger); Map<XmlTag, ViewInfo> map = Maps.newHashMap(); RenderSession session = measure(modelParser); if (session != null) { Result result = session.getResult(); if (result != null && result.isSuccess()) { assert session.getRootViews().size() == 1; ViewInfo root = session.getRootViews().get(0); List<ViewInfo> children = root.getChildren(); for (ViewInfo info : children) { Object cookie = info.getCookie(); if (cookie instanceof XmlTag) { map.put((XmlTag)cookie, info); } } } return map; } return null; } /** * Measure the given child in context, applying the given filter to the * pull parser's attribute values. * * @param tag the child to measure * @param filter the filter to apply to the attribute values * @return a view info, if found */ @Nullable public ViewInfo measureChild(XmlTag tag, final AttributeFilter filter) { XmlTag parent = tag.getParentTag(); if (parent != null) { Map<XmlTag, ViewInfo> map = measureChildren(parent, filter); if (map != null) { for (Map.Entry<XmlTag, ViewInfo> entry : map.entrySet()) { if (entry.getKey() == tag) { return entry.getValue(); } } } } return null; } @Nullable private RenderSession measure(ILayoutPullParser parser) { ResourceResolver resolver = getResourceResolver(); if (resolver == null) { // Abort the rendering if the resources are not found. return null; } myLayoutlibCallback.reset(); HardwareConfig hardwareConfig = myHardwareConfigHelper.getConfig(); final SessionParams params = new SessionParams( parser, RenderingMode.FULL_EXPAND, myModule /* projectKey */, hardwareConfig, resolver, myLayoutlibCallback, myMinSdkVersion.getApiLevel(), myTargetSdkVersion.getApiLevel(), myLogger); params.setLayoutOnly(); params.setForceNoDecor(); params.setExtendedViewInfoMode(true); params.setLocale(myLocale.toLocaleId()); ManifestInfo manifestInfo = ManifestInfo.get(myModule); try { params.setRtlSupport(manifestInfo.isRtlSupported()); } catch (Exception e) { // ignore. } try { myLayoutlibCallback.setLogger(myLogger); myLayoutlibCallback.setResourceResolver(resolver); return ApplicationManager.getApplication().runReadAction(new Computable<RenderSession>() { @Nullable @Override public RenderSession compute() { int retries = 0; while (retries < 10) { RenderSession session = myLayoutLib.createSession(params); Result result = session.getResult(); if (result.getStatus() != Result.Status.ERROR_TIMEOUT) { // Sometimes happens at startup; treat it as a timeout; typically a retry fixes it if (!result.isSuccess() && "The main Looper has already been prepared.".equals(result.getErrorMessage())) { retries++; continue; } return session; } retries++; } return null; } }); } catch (RuntimeException t) { // Exceptions from the bridge myLogger.error(null, t.getLocalizedMessage(), t, null); throw t; } } /** * The {@link AttributeFilter} allows a client of {@link #measureChildren} to modify the actual * XML values of the nodes being rendered, for example to force width and height values to * wrap_content when measuring preferred size. */ public interface AttributeFilter { /** * Returns the attribute value for the given node and attribute name. This filter * allows a client to adjust the attribute values that a node presents to the * layout library. * <p/> * Returns "" to unset an attribute. Returns null to return the unfiltered value. * * @param node the node for which the attribute value should be returned * @param namespace the attribute namespace * @param localName the attribute local name * @return an override value, or null to return the unfiltered value */ @Nullable String getAttribute(@NotNull XmlTag node, @Nullable String namespace, @NotNull String localName); } }