blob: 2cd3269f171920c0f6d24080e49b640fc6dbef97 [file] [log] [blame]
/*
* 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.LayoutLibrary;
import com.android.ide.common.rendering.api.Features;
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.util.Projects;
import com.android.tools.idea.rendering.multi.CompatibilityRenderTarget;
import com.android.tools.idea.sdk.wizard.SdkQuickfixWizard;
import com.android.tools.idea.structure.gradle.AndroidProjectSettingsService;
import com.android.utils.HtmlBuilder;
import com.google.common.collect.Lists;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService;
import com.intellij.openapi.ui.Messages;
import com.intellij.psi.PsiFile;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.maven.AndroidMavenUtil;
import org.jetbrains.android.sdk.AndroidPlatform;
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 java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import static com.android.SdkConstants.TAG_PREFERENCE_SCREEN;
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 {
public static final boolean NELE_ENABLED = Boolean.getBoolean("nele.enabled");
private static final Object RENDERING_LOCK = new Object();
@NotNull
private final AndroidFacet myFacet;
private final Object myCredential = new Object();
public RenderService(@NotNull AndroidFacet facet) {
myFacet = facet;
}
/**
* Returns the {@linkplain RenderService} for the given facet
*/
@NotNull
public static RenderService get(@NotNull AndroidFacet facet) {
return facet.getRenderService();
}
@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 = AndroidPlatform.getInstance(module);
if (platform != null) {
try {
return platform.getSdkData().getTargetData(target).getLayoutLibrary(project);
}
catch (RenderingException e) {
// Ignore.
}
catch (IOException e) {
// Ditto
}
}
return null;
}
public static boolean supportsCapability(@NotNull final Module module, @NotNull IAndroidTarget target,
@MagicConstant(flagsFromClass = Features.class) int capability) {
Project project = module.getProject();
AndroidPlatform platform = AndroidPlatform.getInstance(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;
}
/** Returns true if the given file can be rendered */
public static boolean canRender(@Nullable PsiFile file) {
return file != null && LayoutPullParserFactory.isSupported(file);
}
@NotNull
public RenderLogger createLogger() {
Module module = getModule();
return new RenderLogger(module.getName(), module, myCredential);
}
/**
* Creates a new {@link RenderService} associated with the given editor.
*
* @return a {@link RenderService} which can perform rendering services
*/
@Nullable
public RenderTask createTask(@Nullable final PsiFile psiFile,
@NotNull final Configuration configuration,
@NotNull final RenderLogger logger,
@Nullable final RenderContext renderContext) {
Module module = myFacet.getModule();
final Project project = module.getProject();
AndroidPlatform platform = getPlatform(module, logger);
if (platform == null) {
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;
}
if (psiFile != null && TAG_PREFERENCE_SCREEN.equals(AndroidPsiUtils.getRootTagName(psiFile)) && !layoutLib.supports(Features.PREFERENCES_RENDERING)) {
// This means that user is using an outdated version of layoutlib. A warning to update has already been
// presented in warnIfObsoleteLayoutLib(). Just log a plain message asking users to update.
logger.addMessage(RenderProblem.createPlain(ERROR, "This version of the rendering library does not support rendering Preferences. " +
"Update it using the SDK Manager"));
return null;
}
Device device = configuration.getDevice();
if (device == null) {
logger.addMessage(RenderProblem.createPlain(ERROR, "No device selected"));
return null;
}
RenderTask task = new RenderTask(this, configuration, logger, layoutLib, device, myCredential);
if (psiFile != null) {
task.setPsiFile(psiFile);
}
task.setRenderContext(renderContext);
return task;
}
@NotNull
public AndroidFacet getFacet() {
return myFacet;
}
public Module getModule() {
return myFacet.getModule();
}
public Project getProject() {
return getModule().getProject();
}
@Nullable
public AndroidPlatform getPlatform() {
return AndroidPlatform.getInstance(getModule());
}
@Nullable
private static AndroidPlatform getPlatform(@NotNull final Module module, @Nullable RenderLogger logger) {
AndroidPlatform platform = AndroidPlatform.getInstance(module);
if (platform == null && logger != 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() {
Project project = module.getProject();
ProjectSettingsService service = ProjectSettingsService.getInstance(project);
if (Projects.requiresAndroidModel(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 platform;
}
private static boolean ourWarnAboutObsoleteLayoutLibVersions = true;
protected static void warnIfObsoleteLayoutLib(@NotNull final Module module,
@NotNull RenderLogger logger,
@Nullable final RenderContext renderContext,
@NotNull IAndroidTarget target) {
if (!ourWarnAboutObsoleteLayoutLibVersions) {
return;
}
if (target instanceof CompatibilityRenderTarget) {
target = ((CompatibilityRenderTarget)target).getRenderTarget();
}
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 = 2;
}
break;
case 20: revision = 2; break;
case 19: revision = 4; break;
case 18: revision = 3; break;
case 17: revision = 3; break;
case 16: revision = 5; break;
case 15: revision = 5; break;
case 14: revision = 4; 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);
}
}
/**
* Runs a action that requires the rendering lock. Layoutlib is not thread safe so any rendering actions should be called using this
* method.
*/
public static void runRenderAction(@NotNull final Runnable runnable) throws Exception {
runRenderAction(new Callable<Void>() {
@Override
public Void call() throws Exception {
runnable.run();
return null;
}
});
}
/**
* Runs a action that requires the rendering lock. Layoutlib is not thread safe so any rendering actions should be called using this
* method.
*/
public static <T> T runRenderAction(@NotNull Callable<T> callable) throws Exception {
synchronized (RENDERING_LOCK) {
return callable.call();
}
}
}