| /* |
| * Copyright (C) 2016 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 static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.ATTR_ID; |
| import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; |
| import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; |
| import static com.android.SdkConstants.CLASS_CONSTRAINT_LAYOUT; |
| import static com.android.SdkConstants.CLASS_FLEXBOX_LAYOUT; |
| import static com.android.SdkConstants.CLASS_VIEW; |
| import static com.android.SdkConstants.DOT_JAVA; |
| import static com.android.SdkConstants.PREFIX_RESOURCE_REF; |
| import static com.android.SdkConstants.VALUE_FILL_PARENT; |
| import static com.android.SdkConstants.VALUE_MATCH_PARENT; |
| import static com.android.SdkConstants.VALUE_TRUE; |
| import static com.android.SdkConstants.VALUE_WRAP_CONTENT; |
| import static com.android.ide.common.rendering.api.LayoutLog.TAG_RESOURCES_PREFIX; |
| import static com.android.ide.common.rendering.api.LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR; |
| import static com.android.tools.idea.rendering.RenderLogger.TAG_STILL_BUILDING; |
| import static com.android.tools.idea.res.IdeResourcesUtil.isViewPackageNeeded; |
| import static com.android.tools.lint.detector.api.Lint.editDistance; |
| import static com.android.tools.lint.detector.api.Lint.stripIdPrefix; |
| |
| import com.android.ide.common.rendering.api.AttributeFormat; |
| import com.android.ide.common.rendering.api.LayoutLog; |
| import com.android.ide.common.resources.ResourceResolver; |
| import com.android.layoutlib.bridge.impl.RenderSessionImpl; |
| import com.android.sdklib.IAndroidTarget; |
| import com.android.tools.idea.model.AndroidModel; |
| import com.android.tools.idea.model.AndroidModuleInfo; |
| import com.android.tools.idea.projectsystem.GoogleMavenArtifactId; |
| import com.android.tools.idea.psi.TagToClassMapper; |
| import com.android.tools.idea.rendering.classloading.ClassConverter; |
| import com.android.tools.idea.rendering.classloading.InconvertibleClassError; |
| import com.android.tools.idea.rendering.errors.ui.RenderErrorModel; |
| import com.android.tools.idea.sdk.AndroidSdks; |
| import com.android.tools.idea.ui.designer.EditorDesignSurface; |
| import com.android.utils.HtmlBuilder; |
| import com.android.xml.AndroidManifest; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Throwables; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Sets; |
| import com.intellij.compiler.impl.javaCompiler.javac.JavacConfiguration; |
| import com.intellij.lang.annotation.HighlightSeverity; |
| import com.intellij.openapi.actionSystem.DataContext; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ReadAction; |
| import com.intellij.openapi.compiler.CompilerManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.extensions.ExtensionPointName; |
| import com.intellij.openapi.fileTypes.StdFileTypes; |
| import com.intellij.openapi.ide.CopyPasteManager; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.options.ShowSettingsUtil; |
| import com.intellij.openapi.progress.EmptyProgressIndicator; |
| import com.intellij.openapi.progress.util.ProgressIndicatorUtils; |
| import com.intellij.openapi.project.DumbService; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.projectRoots.JavaSdk; |
| import com.intellij.openapi.projectRoots.JavaSdkVersion; |
| import com.intellij.openapi.projectRoots.ProjectJdkTable; |
| import com.intellij.openapi.projectRoots.Sdk; |
| import com.intellij.openapi.roots.ModuleRootManager; |
| import com.intellij.openapi.roots.ui.configuration.ClasspathEditor; |
| import com.intellij.openapi.roots.ui.configuration.ModulesConfigurator; |
| import com.intellij.openapi.roots.ui.configuration.ProjectStructureConfigurable; |
| import com.intellij.openapi.ui.Messages; |
| import com.intellij.openapi.util.Computable; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.util.SystemInfo; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.problems.WolfTheProblemSolver; |
| import com.intellij.psi.JavaPsiFacade; |
| import com.intellij.psi.PsiClass; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.SmartPsiElementPointer; |
| import com.intellij.psi.search.GlobalSearchScope; |
| import com.intellij.psi.util.CachedValue; |
| import com.intellij.psi.xml.XmlFile; |
| import com.intellij.psi.xml.XmlTag; |
| import java.awt.datatransfer.StringSelection; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import javax.swing.JEditorPane; |
| import javax.swing.event.HyperlinkEvent; |
| import javax.swing.event.HyperlinkListener; |
| import javax.swing.text.html.HTMLDocument; |
| import javax.swing.text.html.HTMLFrameHyperlinkEvent; |
| import org.jetbrains.android.dom.attrs.AttributeDefinition; |
| import org.jetbrains.android.dom.attrs.AttributeDefinitions; |
| import org.jetbrains.android.dom.manifest.Application; |
| import org.jetbrains.android.dom.manifest.Manifest; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.android.refactoring.MigrateToAndroidxUtil; |
| import org.jetbrains.android.sdk.AndroidPlatform; |
| import org.jetbrains.android.sdk.AndroidSdkAdditionalData; |
| import org.jetbrains.android.sdk.AndroidSdkType; |
| import org.jetbrains.android.sdk.AndroidTargetData; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerOptions; |
| |
| /** |
| * Class that finds {@link RenderErrorModel.Issue}s in a {@link RenderResult}. |
| */ |
| public class RenderErrorContributor { |
| private static final String RENDER_SESSION_IMPL_FQCN = RenderSessionImpl.class.getCanonicalName(); |
| private static final Key<CachedValue<Set<String>>> VIEWS_CACHE_KEY = |
| new Key<>(RenderErrorContributor.class.getName() + ".VIEWS_CACHE"); |
| |
| // These priorities can be used to promote certain issues to the top of the list |
| protected static final int HIGH_PRIORITY = 100; |
| @SuppressWarnings("unused") protected static final int MEDIUM_PRIORITY = 10; |
| @SuppressWarnings("unused") protected static final int LOW_PRIORITY = 10; |
| |
| protected static final Logger LOG = Logger.getInstance(RenderErrorContributor.class); |
| private static final String APP_COMPAT_REQUIRED_MSG = "You need to use a Theme.AppCompat"; |
| |
| private final List<RenderErrorModel.Issue> myIssues = new ArrayList<>(); |
| private final HtmlLinkManager myLinkManager; |
| private final HyperlinkListener myLinkHandler; |
| private final RenderResult myResult; |
| private final DataContext myDataContext; |
| private final EditorDesignSurface myDesignSurface; |
| |
| protected RenderErrorContributor(@Nullable EditorDesignSurface surface, @NotNull RenderResult result, @Nullable DataContext dataContext) { |
| myResult = result; |
| |
| myDesignSurface = surface; |
| myLinkManager = myResult.getLogger().getLinkManager(); |
| myLinkHandler = e -> { |
| if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { |
| JEditorPane pane = (JEditorPane)e.getSource(); |
| if (e instanceof HTMLFrameHyperlinkEvent) { |
| HTMLFrameHyperlinkEvent evt = (HTMLFrameHyperlinkEvent)e; |
| HTMLDocument doc = (HTMLDocument)pane.getDocument(); |
| doc.processHTMLFrameHyperlinkEvent(evt); |
| return; |
| } |
| |
| performClick(myResult, e.getDescription()); |
| } |
| }; |
| |
| myDataContext = dataContext; |
| } |
| |
| private static boolean isHiddenFrame(@NotNull StackTraceElement frame) { |
| String className = frame.getClassName(); |
| return |
| className.startsWith("sun.reflect.") || |
| className.equals("android.view.BridgeInflater") || |
| className.startsWith("com.android.tools.") || |
| className.startsWith("org.jetbrains."); |
| } |
| |
| private static boolean isInterestingFrame(@NotNull StackTraceElement frame) { |
| String className = frame.getClassName(); |
| return !(className.startsWith("android.") |
| || className.startsWith("org.jetbrains.") |
| || className.startsWith("com.android.") |
| || className.startsWith("java.") |
| || className.startsWith("javax.") |
| || className.startsWith("sun.")); |
| } |
| |
| private static boolean isFramework(@NotNull StackTraceElement frame) { |
| String className = frame.getClassName(); |
| return (className.startsWith("android.") |
| || className.startsWith("java.") |
| || className.startsWith("javax.") |
| || className.startsWith("sun.")); |
| } |
| |
| private static boolean isVisible(@NotNull StackTraceElement frame) { |
| String className = frame.getClassName(); |
| return !(isFramework(frame) || className.startsWith("sun.")); |
| } |
| |
| @NotNull |
| private static Collection<String> getAllViews(@Nullable final Module module) { |
| if (module == null) { |
| return Collections.emptyList(); |
| } |
| if (!ApplicationManager.getApplication().isReadAccessAllowed()) { |
| return ReadAction.compute(() -> getAllViews(module)); |
| } |
| |
| if (DumbService.getInstance(module.getProject()).isDumb()) { |
| // This method should not be called in dumb mode, but if it is, we can just return an empty list. This will disable the feature |
| // where we return suggestions for correcting views. |
| LOG.warn("getAllViews called in Dumb mode, no views will be returned"); |
| return Collections.emptyList(); |
| } |
| |
| return TagToClassMapper.getInstance(module).getClassMap(CLASS_VIEW).values().stream() |
| .map(PsiClass::getQualifiedName) |
| .collect(Collectors.toSet()); |
| } |
| |
| static boolean isBuiltByJdk7OrHigher(@NotNull Module module) { |
| Sdk sdk = ModuleRootManager.getInstance(module).getSdk(); |
| if (sdk == null) { |
| return false; |
| } |
| |
| AndroidSdks androidSdks = AndroidSdks.getInstance(); |
| if (androidSdks.isAndroidSdk(sdk)) { |
| AndroidSdkAdditionalData data = androidSdks.getAndroidSdkAdditionalData(sdk); |
| if (data != null) { |
| Sdk jdk = data.getJavaSdk(); |
| if (jdk != null) { |
| sdk = jdk; |
| } |
| } |
| } |
| return sdk.getSdkType() instanceof JavaSdk && |
| JavaSdk.getInstance().isOfVersionOrHigher(sdk, JavaSdkVersion.JDK_1_7); |
| } |
| |
| private static void collectProblemModules(@NotNull Module module, @NotNull Set<Module> visited, @NotNull Collection<Module> result) { |
| if (!visited.add(module)) { |
| return; |
| } |
| |
| if (isBuiltByJdk7OrHigher(module)) { |
| result.add(module); |
| } |
| |
| for (Module depModule : ModuleRootManager.getInstance(module).getDependencies(false)) { |
| collectProblemModules(depModule, visited, result); |
| } |
| } |
| |
| @NotNull |
| private static Set<String> getSdkNamesFromModules(@NotNull Collection<Module> modules) { |
| final Set<String> result = new HashSet<>(); |
| for (Module module : modules) { |
| final Sdk sdk = ModuleRootManager.getInstance(module).getSdk(); |
| |
| if (sdk != null) { |
| result.add(sdk.getName()); |
| } |
| } |
| return result; |
| } |
| |
| @NotNull |
| private static List<Module> getProblemModules(@NotNull Module root) { |
| final List<Module> result = new ArrayList<>(); |
| collectProblemModules(root, new HashSet<>(), result); |
| return result; |
| } |
| |
| private static void askAndRebuild(Project project) { |
| final int r = Messages.showYesNoDialog(project, "You have to rebuild project to see the fixed preview. Would you like to do it?", |
| "Rebuild Project", Messages.getQuestionIcon()); |
| if (r == Messages.YES) { |
| CompilerManager.getInstance(project).rebuild(null); |
| } |
| } |
| |
| /** |
| * Returns a new {@link RenderErrorModel.Issue.Builder} that will add the created issue to the issues list when |
| * {@link RenderErrorModel.Issue.Builder#build()} is called. |
| * The returned builder also is pre-configured with the default link handler. |
| */ |
| protected RenderErrorModel.Issue.Builder addIssue() { |
| return new RenderErrorModel.Issue.Builder() { |
| @NotNull |
| @Override |
| public RenderErrorModel.Issue build() { |
| RenderErrorModel.Issue built = super.build(); |
| myIssues.add(built); |
| return built; |
| } |
| }.setLinkHandler(myLinkHandler); |
| } |
| |
| private void reportMissingStyles(@NotNull RenderLogger logger) { |
| if (logger.seenTagPrefix(TAG_STILL_BUILDING)) { |
| addIssue() |
| .setSummary("Project Still Building: May cause rendering errors until the build is done") |
| .build(); |
| } |
| else if (logger.seenTagPrefix(TAG_RESOURCES_RESOLVE_THEME_ATTR)) { |
| addIssue() |
| .setSummary("Missing styles") |
| .setHtmlContent(new HtmlBuilder() |
| .addBold("Missing styles. Is the correct theme chosen for this layout?") |
| .newline() |
| .addIcon(HtmlBuilderHelper.getTipIconPath()) |
| .add( |
| "Use the Theme combo box above the layout to choose a different layout, or fix the theme style references.") |
| ) |
| .build(); |
| } |
| } |
| |
| private void reportMissingSize(@NotNull HtmlBuilder builder, |
| @NotNull RenderLogger logger, |
| @NotNull String fill, |
| @NotNull XmlTag tag, |
| @NotNull String id, |
| @NotNull String attribute) { |
| Module module = logger.getModule(); |
| if (module == null) { |
| return; |
| } |
| Project project = module.getProject(); |
| String wrapUrl = myLinkManager.createCommandLink(new SetAttributeFix(project, tag, attribute, ANDROID_URI, VALUE_WRAP_CONTENT)); |
| String fillUrl = myLinkManager.createCommandLink(new SetAttributeFix(project, tag, attribute, ANDROID_URI, fill)); |
| |
| builder.add(String.format("%1$s does not set the required %2$s attribute: ", id, attribute)) |
| .newline() |
| .addNbsps(4) |
| .addLink("Set to wrap_content", wrapUrl) |
| .add(", ") |
| .addLink("Set to " + fill, fillUrl) |
| .newline(); |
| } |
| |
| private void reportMissingSizeAttributes(@NotNull final RenderLogger logger, |
| @NotNull RenderTaskContext renderTaskContext, |
| @Nullable XmlFile psiFile) { |
| Module module = logger.getModule(); |
| if (module == null) { |
| return; |
| } |
| Project project = module.getProject(); |
| if (logger.isMissingSize()) { |
| HtmlBuilder builder = new HtmlBuilder(); |
| |
| // Emit hyperlink about missing attributes; the action will operate on all of them |
| builder.addBold("NOTE: One or more layouts are missing the layout_width or layout_height attributes. " + |
| "These are required in most layouts.").newline(); |
| final ResourceResolver resourceResolver = renderTaskContext.getConfiguration().getResourceResolver(); |
| if (psiFile == null) { |
| LOG.error("PsiFile is missing in RenderTask used in RenderErrorPanel!"); |
| return; |
| } |
| |
| // See whether we should offer match_parent instead of fill_parent |
| AndroidModuleInfo moduleInfo = AndroidModuleInfo.getInstance(module); |
| final String fill = moduleInfo == null |
| || moduleInfo.getBuildSdkVersion() == null |
| || moduleInfo.getBuildSdkVersion().getApiLevel() >= 8 |
| ? VALUE_MATCH_PARENT : VALUE_FILL_PARENT; |
| |
| ApplicationManager.getApplication() |
| .runReadAction(() -> AddMissingAttributesFix.findViewsMissingSizes(psiFile, resourceResolver).stream() |
| .map(SmartPsiElementPointer::getElement) |
| .filter(Objects::nonNull) |
| .filter(XmlTag::isValid) |
| .forEach(tag -> { |
| boolean missingWidth = |
| !AddMissingAttributesFix.definesWidth(tag, resourceResolver); |
| boolean missingHeight = |
| !AddMissingAttributesFix.definesHeight(tag, resourceResolver); |
| assert missingWidth || missingHeight; |
| |
| String id = tag.getAttributeValue(ATTR_ID); |
| if (id == null || id.isEmpty()) { |
| id = '<' + tag.getName() + '>'; |
| } |
| else { |
| id = '"' + stripIdPrefix(id) + '"'; |
| } |
| |
| if (missingWidth) { |
| reportMissingSize(builder, logger, fill, tag, id, |
| ATTR_LAYOUT_WIDTH); |
| } |
| if (missingHeight) { |
| reportMissingSize(builder, logger, fill, tag, id, |
| ATTR_LAYOUT_HEIGHT); |
| } |
| })); |
| |
| builder.newline() |
| .add("Or: ") |
| .addLink("Automatically add all missing attributes", |
| myLinkManager.createCommandLink(new AddMissingAttributesFix(project, psiFile, resourceResolver))).newline() |
| .newline().newline(); |
| |
| addIssue() |
| .setSeverity(HighlightSeverity.ERROR) |
| .setSummary("One or more layouts are missing the layout_width or layout_height attributes") |
| .setHtmlContent(builder) |
| .build(); |
| } |
| } |
| |
| private static void addHtmlForIssue164378(@NotNull Throwable throwable, |
| Module module, |
| HtmlLinkManager linkManager, |
| HtmlBuilder builder, |
| boolean addShowExceptionLink) { |
| builder.add("Rendering failed with a known bug. "); |
| if (module == null) { |
| // Unlikely, but just in case. |
| builder.add("Please rebuild the project and then clear the cache by clicking the refresh icon above the preview.").newline(); |
| return; |
| } |
| builder.addLink("Please try a ", "rebuild", ".", linkManager.createBuildProjectUrl()); |
| builder.newline().newline(); |
| if (!addShowExceptionLink) { |
| return; |
| } |
| ShowExceptionFix showExceptionFix = new ShowExceptionFix(module.getProject(), throwable); |
| builder.addLink("Show Exception", linkManager.createRunnableLink(showExceptionFix)); |
| } |
| |
| @VisibleForTesting |
| public void performClick(@NotNull RenderResult result, @NotNull String url) { |
| Module module = result.getModule(); |
| PsiFile file = result.getFile(); |
| |
| myLinkManager.handleUrl(url, module, file, myDataContext, result, myDesignSurface); |
| } |
| |
| private void reportRelevantCompilationErrors(@NotNull RenderLogger logger, @NotNull RenderTask renderTask) { |
| Module module = logger.getModule(); |
| if (module == null) { |
| return; |
| } |
| |
| Project project = module.getProject(); |
| WolfTheProblemSolver wolfgang = WolfTheProblemSolver.getInstance(project); |
| |
| if (!wolfgang.hasProblemFilesBeneath(module)) { |
| return; |
| } |
| |
| |
| HtmlBuilder builder = new HtmlBuilder(); |
| String summary = null; |
| if (logger.seenTagPrefix(TAG_RESOURCES_PREFIX)) { |
| // Do we have errors in the res/ files? |
| // See if it looks like we have aapt problems |
| boolean haveResourceErrors = wolfgang.hasProblemFilesBeneath(virtualFile -> virtualFile.getFileType() == StdFileTypes.XML); |
| if (haveResourceErrors) { |
| summary = "Resource errors"; |
| builder.addBold("This project contains resource errors, so aapt did not succeed, " + |
| "which can cause rendering failures. Fix resource problems first.") |
| .newline().newline(); |
| } |
| } |
| else if (renderTask.getLayoutlibCallback().isUsed()) { |
| boolean hasJavaErrors = wolfgang.hasProblemFilesBeneath(virtualFile -> virtualFile.getFileType() == StdFileTypes.JAVA); |
| if (hasJavaErrors) { |
| summary = "Compilation errors"; |
| builder.addBold("This project contains Java compilation errors, " + |
| "which can cause rendering failures for custom views. " + |
| "Fix compilation problems first.") |
| .newline().newline(); |
| } |
| } |
| |
| if (summary == null) { |
| return; |
| } |
| addIssue() |
| .setSeverity(HighlightSeverity.ERROR) |
| .setSummary(summary) |
| .setHtmlContent(builder) |
| .build(); |
| } |
| |
| private boolean reportSandboxError(@NotNull Throwable throwable, boolean newlineBefore, boolean newlineAfter) { |
| if (!(throwable instanceof SecurityException)) { |
| return false; |
| } |
| |
| HtmlBuilder builder = new HtmlBuilder(); |
| if (newlineBefore) { |
| builder.newline(); |
| } |
| builder.addLink("Turn off custom view rendering sandbox", myLinkManager.createDisableSandboxUrl()); |
| |
| String lastFailedPath = RenderSecurityManager.getLastFailedPath(); |
| if (lastFailedPath != null) { |
| builder.newline().newline() |
| .add("Diagnostic info for Studio bug report:").newline() |
| .add("Failed path: ").add(lastFailedPath).newline(); |
| String tempDir = System.getProperty("java.io.tmpdir"); |
| builder.add("Normal temp dir: ").add(tempDir).newline(); |
| File normalized = new File(tempDir); |
| builder.add("Normalized temp dir: ").add(normalized.getPath()).newline(); |
| try { |
| builder.add("Canonical temp dir: ").add(normalized.getCanonicalPath()).newline(); |
| } |
| catch (IOException e) { |
| // ignore |
| } |
| builder.add("os.name: ").add(SystemInfo.OS_NAME).newline() |
| .add("os.version: ").add(SystemInfo.OS_VERSION).newline() |
| .add("java.runtime.version: ").add(SystemInfo.JAVA_RUNTIME_VERSION); |
| } |
| |
| if (newlineAfter) { |
| builder.newline().newline(); |
| } |
| |
| reportThrowable(builder, throwable, false); |
| addRefreshAction(builder); |
| |
| addIssue() |
| .setSeverity(HighlightSeverity.ERROR) |
| .setSummary("Rendering sandbox error") |
| .setHtmlContent(builder) |
| .build(); |
| return true; |
| } |
| |
| /** |
| * Display the problem list encountered during a render. |
| * |
| * @return if the throwable was hidden. |
| */ |
| private boolean reportThrowable(@NotNull HtmlBuilder builder, |
| @NotNull final Throwable throwable, |
| boolean hideIfIrrelevant) { |
| StackTraceElement[] frames = throwable.getStackTrace(); |
| int end = -1; |
| boolean haveInterestingFrame = false; |
| for (int i = 0; i < frames.length; i++) { |
| StackTraceElement frame = frames[i]; |
| if (isInterestingFrame(frame)) { |
| haveInterestingFrame = true; |
| } |
| String className = frame.getClassName(); |
| if (className.equals(RENDER_SESSION_IMPL_FQCN)) { |
| end = i; |
| break; |
| } |
| } |
| |
| if (end == -1 || !haveInterestingFrame) { |
| // Not a recognized stack trace range: just skip it |
| if (hideIfIrrelevant) { |
| return true; |
| } |
| else { |
| // List just the top frames |
| for (int i = 0; i < frames.length; i++) { |
| StackTraceElement frame = frames[i]; |
| if (!isVisible(frame)) { |
| end = i; |
| if (end == 0) { |
| // Find end instead |
| for (int j = 0; j < frames.length; j++) { |
| frame = frames[j]; |
| String className = frame.getClassName(); |
| if (className.equals(RENDER_SESSION_IMPL_FQCN)) { |
| end = j; |
| break; |
| } |
| } |
| } |
| break; |
| } |
| } |
| } |
| } |
| |
| builder.addHtml(StringUtil.replace(throwable.toString(), "\n", "<BR/>")).newline(); |
| |
| boolean wasHidden = false; |
| int indent = 2; |
| File platformSource = null; |
| boolean platformSourceExists = true; |
| for (int i = 0; i < end; i++) { |
| StackTraceElement frame = frames[i]; |
| if (isHiddenFrame(frame)) { |
| wasHidden = true; |
| continue; |
| } |
| |
| String className = frame.getClassName(); |
| String methodName = frame.getMethodName(); |
| builder.addNbsps(indent); |
| builder.add("at ").add(className).add(".").add(methodName); |
| String fileName = frame.getFileName(); |
| if (fileName != null && !fileName.isEmpty()) { |
| int lineNumber = frame.getLineNumber(); |
| String location = fileName + ':' + lineNumber; |
| if (isInterestingFrame(frame)) { |
| if (wasHidden) { |
| builder.addNbsps(indent).add(" ...").newline(); |
| wasHidden = false; |
| } |
| String url = myLinkManager.createOpenStackUrl(className, methodName, fileName, lineNumber); |
| builder.add("(").addLink(location, url).add(")"); |
| } |
| else { |
| // Try to link to local documentation |
| String url = null; |
| if (isFramework(frame) && platformSourceExists) { // try to link to documentation, if available |
| if (platformSource == null) { |
| IAndroidTarget target = myResult.getRenderTask() != null ? |
| myResult.getRenderTask().getContext().getConfiguration().getRealTarget() : |
| null; |
| platformSource = target != null ? AndroidSdks.getInstance().findPlatformSources(target) : null; |
| platformSourceExists = platformSource != null; |
| } |
| |
| if (platformSourceExists) { |
| File classFile = new File(platformSource, frame.getClassName().replace('.', File.separatorChar) + DOT_JAVA); |
| if (!classFile.exists()) { |
| // Probably an innerclass like foo.bar.Outer.Inner; the above would look for foo/bar/Outer/Inner.java; try |
| // again at foo/bar/ |
| File parentFile = classFile.getParentFile(); |
| classFile = new File(parentFile.getParentFile(), parentFile.getName() + DOT_JAVA); |
| if (!classFile.exists()) { |
| classFile = null; // in theory we should keep trying this repeatedly for more deeply nested inner classes |
| } |
| } |
| if (classFile != null) { |
| url = HtmlLinkManager.createFilePositionUrl(classFile, lineNumber, 0); |
| } |
| } |
| } |
| if (url != null) { |
| builder.add("(").addLink(location, url).add(")"); |
| } |
| else { |
| builder.add("(").add(location).add(")"); |
| } |
| } |
| builder.newline(); |
| } |
| } |
| |
| builder.addLink("Copy stack to clipboard", myLinkManager.createRunnableLink(() -> { |
| String text = Throwables.getStackTraceAsString(throwable); |
| try { |
| CopyPasteManager.getInstance().setContents(new StringSelection(text)); |
| HtmlLinkManager.showNotification("Stack trace copied to clipboard"); |
| } |
| catch (Exception ignore) { |
| } |
| })); |
| return false; |
| } |
| |
| private void addRefreshAction(@NotNull HtmlBuilder builder) { |
| builder.newlineIfNecessary() |
| .newline() |
| .addIcon(HtmlBuilderHelper.getRefreshIconPath()) |
| .addLink("Tip: Try to ", "refresh", " the layout.", |
| myLinkManager.createRefreshRenderUrl()).newline(); |
| } |
| |
| private void reportRtlNotEnabled(@NotNull RenderLogger logger, @Nullable RenderTask task) { |
| ApplicationManager.getApplication().runReadAction(() -> { |
| Project project = logger.getProject(); |
| if (project == null || project.isDisposed()) { |
| return; |
| } |
| |
| Module module = logger.getModule(); |
| if (module == null) { |
| return; |
| } |
| |
| AndroidFacet facet = AndroidFacet.getInstance(module); |
| Manifest manifest = facet != null ? Manifest.getMainManifest(facet) : null; |
| Application application = manifest != null ? manifest.getApplication() : null; |
| if (application == null) { |
| return; |
| } |
| |
| final XmlTag applicationTag = application.getXmlTag(); |
| if (applicationTag == null) { |
| return; |
| } |
| |
| HtmlBuilder builder = new HtmlBuilder(); |
| builder.add("(") |
| .addLink("Add android:supportsRtl=\"true\" to the manifest", logger.getLinkManager().createRunnableLink(() -> { |
| new SetAttributeFix(project, applicationTag, AndroidManifest.ATTRIBUTE_SUPPORTS_RTL, ANDROID_URI, VALUE_TRUE).execute(); |
| |
| if (myDesignSurface != null) { |
| myDesignSurface.forceUserRequestedRefresh(); |
| } |
| })).add(")"); |
| |
| addIssue() |
| .setSeverity(HighlightSeverity.ERROR) |
| .setSummary("RTL support requires android:supportsRtl=\"true\" in the manifest") |
| .setHtmlContent(builder) |
| .build(); |
| }); |
| } |
| |
| private void reportTagResourceFormat(@NotNull RenderResult result, @NotNull RenderProblem message) { |
| Object clientData = message.getClientData(); |
| if (!(clientData instanceof String[])) { |
| return; |
| } |
| String[] strings = (String[])clientData; |
| if (strings.length != 2) { |
| return; |
| } |
| |
| RenderTask renderTask = result.getRenderTask(); |
| if (renderTask == null) { |
| return; |
| } |
| IAndroidTarget target = renderTask.getContext().getConfiguration().getRealTarget(); |
| if (target == null) { |
| return; |
| } |
| AndroidPlatform platform = renderTask.getContext().getPlatform(); |
| if (platform == null) { |
| return; |
| } |
| AndroidTargetData targetData = platform.getSdkData().getTargetData(target); |
| AttributeDefinitions definitionLookup = targetData.getPublicAttrDefs(result.getFile().getProject()); |
| String attributeName = strings[0]; |
| String currentValue = strings[1]; |
| AttributeDefinition definition = definitionLookup.getAttrDefByName(attributeName); |
| if (definition == null) { |
| return; |
| } |
| Set<AttributeFormat> formats = definition.getFormats(); |
| if (formats.contains(AttributeFormat.FLAGS) || formats.contains(AttributeFormat.ENUM)) { |
| String[] values = definition.getValues(); |
| if (values.length > 0) { |
| HtmlBuilder builder = new HtmlBuilder(); |
| builder.add("Change ").add(currentValue).add(" to: "); |
| boolean first = true; |
| for (String value : values) { |
| if (first) { |
| first = false; |
| } |
| else { |
| builder.add(", "); |
| } |
| builder.addLink(value, myLinkManager.createReplaceAttributeValueUrl(attributeName, currentValue, value)); |
| } |
| |
| addRefreshAction(builder); |
| addIssue() |
| //TODO: Review |
| .setSummary("Incorrect resource value format") |
| .setHtmlContent(builder) |
| .build(); |
| } |
| } |
| } |
| |
| private void reportOtherProblems(@NotNull RenderLogger logger, RenderTask task) { |
| List<RenderProblem> messages = logger.getMessages(); |
| |
| if (messages.isEmpty()) { |
| return; |
| } |
| |
| Set<String> seenTags = Sets.newHashSet(); |
| for (RenderProblem message : messages) { |
| String tag = message.getTag(); |
| if (tag != null && seenTags.contains(tag)) { |
| continue; |
| } |
| seenTags.add(tag); |
| |
| if (tag != null) { |
| if (LayoutLog.TAG_RESOURCES_FORMAT.equals(tag)) { |
| reportTagResourceFormat(myResult, message); |
| continue; |
| } |
| else if (LayoutLog.TAG_RTL_NOT_ENABLED.equals(tag)) { |
| reportRtlNotEnabled(logger, task); |
| continue; |
| } |
| else if (LayoutLog.TAG_RTL_NOT_SUPPORTED.equals(tag)) { |
| addIssue() |
| .setSeverity(HighlightSeverity.ERROR) |
| .setSummary("RTL support requires API level >= 17") |
| .setHtmlContent(new HtmlBuilder().addHtml(message.getHtml())) |
| .build(); |
| continue; |
| } |
| } |
| |
| HtmlBuilder builder = new HtmlBuilder(); |
| |
| String html = message.getHtml(); |
| Throwable throwable = message.getThrowable(); |
| |
| String summary = "Render problem"; |
| if (throwable != null) { |
| if (!reportSandboxError(throwable, false, true)) { |
| if (reportThrowable(builder, throwable, !html.isEmpty() || !message.isDefaultHtml())) { |
| // The error was hidden. |
| if (!html.isEmpty()) { |
| builder.getStringBuilder().append(html); |
| builder.newlineIfNecessary(); |
| } |
| |
| summary = throwable.getLocalizedMessage() != null ? |
| throwable.getLocalizedMessage() : |
| summary; |
| } |
| } |
| else { |
| // This was processed as a Sandbox error |
| continue; |
| } |
| } |
| else { |
| if (html.contains("has been edited more recently")) { |
| summary = "Build out-of-date"; |
| } |
| |
| builder.getStringBuilder().append(html); |
| builder.newlineIfNecessary(); |
| } |
| |
| addRefreshAction(builder); |
| addIssue() |
| .setSeverity(HighlightSeverity.ERROR) |
| .setSummary(summary) |
| .setHtmlContent(builder) |
| .build(); |
| } |
| } |
| |
| private boolean addTypoSuggestions(@NotNull HtmlBuilder builder, |
| @NotNull String actual, |
| @Nullable Collection<String> views, |
| boolean compareWithPackage) { |
| if (views == null || views.isEmpty()) { |
| return false; |
| } |
| |
| // Look for typos and try to match with custom views and android views |
| String actualBase = actual.substring(actual.lastIndexOf('.') + 1); |
| String match = compareWithPackage ? actual : actualBase; |
| int maxDistance = actualBase.length() >= 4 ? 2 : 1; |
| |
| for (String suggested : views) { |
| String suggestedBase = suggested.substring(suggested.lastIndexOf('.') + 1); |
| String matchWith = compareWithPackage ? suggested : suggestedBase; |
| if (Math.abs(actualBase.length() - suggestedBase.length()) > maxDistance) { |
| // The string lengths differ more than the allowed edit distance; |
| // no point in even attempting to compute the edit distance (requires |
| // O(n*m) storage and O(n*m) speed, where n and m are the string lengths) |
| continue; |
| } |
| |
| boolean sameBase = actualBase.equals(suggestedBase); |
| if (!compareWithPackage && sameBase) { |
| // This view is an exact match for one of the known views. |
| // That probably means it's a valid class, but the project needs to be built. |
| continue; |
| } |
| |
| if (compareWithPackage) { |
| if (!sameBase) { |
| // If they differ in the base name, handled by separate call with !compareWithPackage |
| continue; |
| } |
| else if (actualBase.equals(actual) && !actualBase.equals(suggested) && isViewPackageNeeded(suggested, -1)) { |
| // Custom view needs to be specified with a fully qualified path |
| builder.addLink(String.format("Change to %1$s", suggested), |
| myLinkManager.createReplaceTagsUrl(actual, suggested)); |
| builder.add(", "); |
| continue; |
| } |
| } |
| |
| if (compareWithPackage && Math.abs(match.length() - matchWith.length()) > maxDistance) { |
| continue; |
| } |
| |
| if (match.equals(matchWith)) { |
| // Exact match: Likely that we're looking for a valid package, but project has |
| // not yet been built |
| return true; |
| } |
| |
| if (editDistance(match, matchWith, maxDistance + 1) <= maxDistance) { |
| // Suggest this class as a typo for the given class |
| String labelClass = (suggestedBase.equals(actual) || actual.indexOf('.') != -1) ? suggested : suggestedBase; |
| builder.addLink(String.format("Change to %1$s", labelClass), |
| myLinkManager.createReplaceTagsUrl(actual, |
| // Only show full package name if class name |
| // is the same |
| (isViewPackageNeeded(suggested, -1) ? suggested : suggestedBase))); |
| builder.add(", "); |
| } |
| } |
| |
| return false; |
| } |
| |
| private void reportRenderingFidelityProblems(@NotNull RenderLogger logger) { |
| List<RenderProblem> fidelityWarnings = logger.getFidelityWarnings(); |
| if (fidelityWarnings.isEmpty()) { |
| return; |
| } |
| |
| HtmlBuilder builder = new HtmlBuilder(); |
| builder.add("The graphics preview in the layout editor may not be accurate:").newline(); |
| builder.beginList(); |
| int count = 0; |
| for (final RenderProblem warning : fidelityWarnings) { |
| builder.listItem(); |
| warning.appendHtml(builder.getStringBuilder()); |
| final Object clientData = warning.getClientData(); |
| if (clientData != null) { |
| builder.addLink(" (Ignore for this session)", myLinkManager.createRunnableLink(() -> { |
| RenderLogger.ignoreFidelityWarning(clientData); |
| if (myDesignSurface != null) { |
| myDesignSurface.forceUserRequestedRefresh(); |
| } |
| })); |
| } |
| builder.newline(); |
| count++; |
| // Only display the first 3 render fidelity issues |
| if (count == 3) { |
| @SuppressWarnings("ConstantConditions") |
| int remaining = fidelityWarnings.size() - count; |
| if (remaining > 0) { |
| builder.add("(").addHtml(Integer.toString(remaining)).add(" additional render fidelity issues hidden)"); |
| break; |
| } |
| } |
| } |
| builder.endList(); |
| builder.addLink("Ignore all fidelity warnings for this session", myLinkManager.createRunnableLink(() -> { |
| RenderLogger.ignoreAllFidelityWarnings(); |
| if (myDesignSurface != null) { |
| myDesignSurface.forceUserRequestedRefresh(); |
| } |
| })); |
| builder.newline(); |
| |
| addIssue() |
| .setSeverity(HighlightSeverity.WEAK_WARNING) |
| .setSummary("Layout fidelity warning") |
| .setHtmlContent(builder) |
| .build(); |
| } |
| |
| private void reportMissingClasses(@NotNull RenderLogger logger) { |
| Set<String> missingClasses = logger.getMissingClasses(); |
| if (missingClasses.isEmpty()) { |
| return; |
| } |
| |
| HtmlBuilder builder = new HtmlBuilder(); |
| if (missingClasses.contains("CalendarView")) { |
| builder.add("The ").addBold("CalendarView").add(" widget does not work correctly with this render target. " + |
| "As a workaround, try using the API 15 (Android 4.0.3) render target library by selecting it from the " + |
| "toolbar menu above."); |
| if (missingClasses.size() == 1) { |
| addIssue() |
| .setSeverity(HighlightSeverity.WARNING) |
| .setSummary("CalendarView does not work correctly with this render target") |
| .setHtmlContent(builder) |
| .build(); |
| return; |
| } |
| } |
| |
| boolean missingResourceClass = logger.isMissingResourceClass() && logger.getResourceClass() != null && logger.hasLoadedClasses(); |
| |
| builder.add("The following classes could not be found:"); |
| builder.beginList(); |
| |
| Collection<String> customViews = null; |
| Collection<String> androidViewClassNames = null; |
| Module module = logger.getModule(); |
| if (module != null) { |
| Ref<Collection<String>> viewsRef = new Ref<>(Collections.emptyList()); |
| // We yield to write actions here because UI responsiveness takes priority over typo suggestions. |
| ProgressIndicatorUtils.runWithWriteActionPriority(() -> viewsRef.set(getAllViews(module)), new EmptyProgressIndicator()); |
| Collection<String> views = viewsRef.get(); |
| if (!views.isEmpty()) { |
| customViews = Lists.newArrayListWithExpectedSize(Math.max(10, views.size() - 80)); // most will be framework views |
| androidViewClassNames = Lists.newArrayListWithExpectedSize(views.size()); |
| for (String fqcn : views) { |
| if (fqcn.startsWith("android.") && !isViewPackageNeeded(fqcn, -1)) { |
| androidViewClassNames.add(fqcn); |
| } |
| else { |
| customViews.add(fqcn); |
| } |
| } |
| } |
| } |
| |
| if (missingResourceClass) { |
| builder.listItem(); |
| builder.add(logger.getResourceClass()); |
| } |
| |
| boolean foundCustomView = false; |
| for (String className : missingClasses) { |
| builder.listItem(); |
| builder.add(className); |
| builder.add(" ("); |
| |
| foundCustomView |= addTypoSuggestions(builder, className, customViews, false); |
| addTypoSuggestions(builder, className, customViews, true); |
| addTypoSuggestions(builder, className, androidViewClassNames, false); |
| |
| if (myLinkManager == null) { |
| return; |
| } |
| |
| if (CLASS_CONSTRAINT_LAYOUT.isEquals(className)) { |
| builder.newline().addNbsps(3); |
| Project project = logger.getProject(); |
| boolean useAndroidX = project == null || MigrateToAndroidxUtil.isAndroidx(project); |
| GoogleMavenArtifactId artifact = useAndroidX ? |
| GoogleMavenArtifactId.ANDROIDX_CONSTRAINT_LAYOUT : |
| GoogleMavenArtifactId.CONSTRAINT_LAYOUT; |
| builder.addLink("Add constraint-layout library dependency to the project", |
| myLinkManager.createAddDependencyUrl(artifact)); |
| builder.add(", "); |
| } |
| if (CLASS_FLEXBOX_LAYOUT.equals(className)) { |
| builder.newline().addNbsps(3); |
| builder.addLink("Add flexbox layout library dependency to the project", |
| myLinkManager.createAddDependencyUrl(GoogleMavenArtifactId.FLEXBOX_LAYOUT)); |
| builder.add(", "); |
| } |
| |
| builder.addLink("Fix Build Path", myLinkManager.createEditClassPathUrl()); |
| |
| //DesignSurface surface = renderTask.getDesignSurface(); |
| //if (surface != null && surface.getType() == LAYOUT_EDITOR) { |
| builder.add(", "); |
| builder.addLink("Edit XML", myLinkManager.createShowTagUrl(className)); |
| //} |
| |
| // Offer to create the class, but only if it looks like a custom view |
| // TODO: Check to see if it looks like it's the name of a custom view and the |
| // user didn't realize a FQN is required here |
| if (className.indexOf('.') != -1) { |
| builder.add(", "); |
| builder.addLink("Create Class", myLinkManager.createNewClassUrl(className)); |
| } |
| builder.add(")"); |
| } |
| builder.endList(); |
| |
| builder.addIcon(HtmlBuilderHelper.getTipIconPath()); |
| builder.addLink("Tip: Try to ", "build", " the project.", |
| myLinkManager.createBuildProjectUrl()); |
| addRefreshAction(builder); |
| if (foundCustomView) { |
| builder.newline() |
| .add("One or more missing custom views were found in the project, but does not appear to have been compiled yet."); |
| } |
| builder.newline().newline(); |
| |
| addIssue() |
| .setSeverity(HighlightSeverity.ERROR) |
| .setSummary("Missing classes") |
| .setHtmlContent(builder) |
| .build(); |
| } |
| |
| private void reportBrokenClasses(@NotNull RenderLogger logger) { |
| Map<String, Throwable> brokenClasses = logger.getBrokenClasses(); |
| if (brokenClasses.isEmpty()) { |
| return; |
| } |
| |
| HtmlBuilder builder = new HtmlBuilder(); |
| final Module module = logger.getModule(); |
| |
| for (Throwable throwable : brokenClasses.values()) { |
| if (RenderLogger.isIssue164378(throwable)) { |
| addHtmlForIssue164378(throwable, module, myLinkManager, builder, false); |
| break; |
| } |
| } |
| |
| builder.add("The following classes could not be instantiated:"); |
| |
| boolean listContainsElements = false; |
| Throwable firstThrowable = null; |
| builder.beginList(); |
| for (Map.Entry<String, Throwable> entry : brokenClasses.entrySet()) { |
| String className = entry.getKey(); |
| Throwable throwable = entry.getValue(); |
| |
| if (throwable != null && throwable.getMessage() != null && throwable.getMessage().startsWith(APP_COMPAT_REQUIRED_MSG)) { |
| // This is already handled by #reportAppCompatRequired |
| continue; |
| } |
| |
| listContainsElements = true; |
| builder.listItem() |
| .add(className) |
| .add(" (") |
| .addLink("Open Class", myLinkManager.createOpenClassUrl(className)); |
| if (throwable != null && module != null) { |
| builder.add(", "); |
| ShowExceptionFix detailsFix = new ShowExceptionFix(module.getProject(), throwable); |
| builder.addLink("Show Exception", myLinkManager.createRunnableLink(detailsFix)); |
| } |
| builder.add(", ") |
| .addLink("Clear Cache", myLinkManager.createClearCacheUrl()) |
| .add(")"); |
| |
| if (firstThrowable == null && throwable != null) { |
| firstThrowable = throwable; |
| } |
| } |
| |
| if (!listContainsElements) { |
| return; |
| } |
| |
| builder.endList() |
| .addIcon(HtmlBuilderHelper.getTipIconPath()) |
| .addLink("Tip: Use ", "View.isInEditMode()", " in your custom views to skip code or show sample data when shown in the IDE.", |
| "http://developer.android.com/reference/android/view/View.html#isInEditMode()") |
| .newline().newline() |
| .add("If this is an unexpected error you can also try to ") |
| .addLink("", "build the project", ", then ", myLinkManager.createBuildProjectUrl()) |
| .addLink("manually ", "refresh the layout", ".", myLinkManager.createRefreshRenderUrl()); |
| |
| if (firstThrowable != null) { |
| builder.newline().newline() |
| .addHeading("Exception Details", HtmlBuilderHelper.getHeaderFontColor()).newline(); |
| reportThrowable(builder, firstThrowable, false); |
| reportSandboxError(firstThrowable, true, false); |
| } |
| builder.newline().newline(); |
| |
| addIssue() |
| .setSeverity(HighlightSeverity.ERROR, HIGH_PRIORITY) |
| .setSummary("Failed to instantiate one or more classes") |
| .setHtmlContent(builder) |
| .build(); |
| } |
| |
| private void reportInstantiationProblems(@NotNull final RenderLogger logger) { |
| Map<String, Throwable> classesWithIncorrectFormat = logger.getClassesWithIncorrectFormat(); |
| if (classesWithIncorrectFormat.isEmpty()) { |
| return; |
| } |
| |
| HtmlBuilder builder = new HtmlBuilder(); |
| builder |
| .add("Preview might be incorrect: unsupported class version.").newline() |
| .addIcon(HtmlBuilderHelper.getTipIconPath()) |
| .add("Tip: "); |
| |
| builder.add("You need to run the IDE with the highest JDK version that you are compiling custom views with. "); |
| |
| int highest = ClassConverter.findHighestMajorVersion(classesWithIncorrectFormat.values()); |
| if (highest > 0 && highest > ClassConverter.getCurrentClassVersion()) { |
| String required = ClassConverter.classVersionToJdk(highest); |
| builder.add("One or more views have been compiled with JDK ") |
| .add(required) |
| .add(", but you are running the IDE on JDK ") |
| .add(ClassConverter.getCurrentJdkVersion()) |
| .add(". "); |
| } |
| else { |
| builder.add("For example, if you are compiling with sourceCompatibility 1.7, you must run the IDE with JDK 1.7. "); |
| } |
| builder.add("Running on a higher JDK is necessary such that these classes can be run in the layout renderer. " + |
| "(Or, extract your custom views into a library which you compile with a lower JDK version.)") |
| .newline().newline() |
| .addLink("If you have just accidentally built your code with a later JDK, try to ", "build", " the project.", |
| myLinkManager.createBuildProjectUrl()) |
| .newline().newline() |
| .add("Classes with incompatible format:"); |
| |
| builder.beginList(); |
| List<String> names = Lists.newArrayList(classesWithIncorrectFormat.keySet()); |
| Collections.sort(names); |
| for (String className : names) { |
| builder.listItem(); |
| builder.add(className); |
| //noinspection ThrowableResultOfMethodCallIgnored |
| Throwable throwable = classesWithIncorrectFormat.get(className); |
| if (throwable instanceof InconvertibleClassError) { |
| InconvertibleClassError error = (InconvertibleClassError)throwable; |
| builder.add(" (Compiled with ") |
| .add(ClassConverter.classVersionToJdk(error.getMajor())) |
| .add(")"); |
| } |
| } |
| builder.endList(); |
| |
| Module module = logger.getModule(); |
| if (module == null) { |
| return; |
| } |
| final List<Module> problemModules = getProblemModules(module); |
| if (!problemModules.isEmpty()) { |
| builder.add("The following modules are built with incompatible JDK:").newline(); |
| for (Iterator<Module> it = problemModules.iterator(); it.hasNext(); ) { |
| Module problemModule = it.next(); |
| builder.add(problemModule.getName()); |
| if (it.hasNext()) { |
| builder.add(", "); |
| } |
| } |
| builder.newline(); |
| } |
| |
| AndroidFacet facet = AndroidFacet.getInstance(logger.getModule()); |
| if (facet != null && !AndroidModel.isRequired(facet)) { |
| Project project = logger.getModule().getProject(); |
| builder |
| .addLink("Rebuild project with '-target 1.6'", myLinkManager.createRunnableLink(new RebuildWith16Fix(project))) |
| .newline(); |
| |
| if (!problemModules.isEmpty()) { |
| builder |
| .addLink("Change Java SDK to 1.6", myLinkManager.createRunnableLink(new SwitchTo16Fix(project, problemModules))) |
| .newline(); |
| } |
| } |
| |
| addIssue() |
| .setSeverity(HighlightSeverity.WARNING) |
| .setSummary("Some classes have an unsupported version") |
| .setHtmlContent(builder) |
| .build(); |
| } |
| |
| private void reportUnknownFragments(@NotNull final RenderLogger logger) { |
| List<String> fragmentNames = logger.getMissingFragments(); |
| if (fragmentNames == null || fragmentNames.isEmpty()) { |
| return; |
| } |
| |
| HtmlBuilder builder = new HtmlBuilder(); |
| builder.add("A ").addHtml("<code>").add("<fragment>").addHtml("</code>").add(" tag allows a layout file to dynamically include " + |
| "different layouts at runtime. ") |
| .add("At layout editing time the specific layout to be used is not known. You can choose which layout you would " + |
| "like previewed while editing the layout."); |
| builder.beginList(); |
| |
| // TODO: Add link to not warn any more for this session |
| |
| for (final String className : fragmentNames) { |
| builder.listItem(); |
| boolean isIdentified = className != null && !className.isEmpty(); |
| boolean isActivityKnown = isIdentified && !className.startsWith(PREFIX_RESOURCE_REF); |
| if (isIdentified) { |
| builder.add("<fragment ").addBold(className).add(" ...>"); |
| } |
| else { |
| builder.add("<fragment>"); |
| } |
| builder.add(" ("); |
| |
| if (isActivityKnown) { |
| final Module module = logger.getModule(); |
| ApplicationManager.getApplication().runReadAction(() -> { |
| // TODO: Look up layout references in the given layout, if possible |
| // Find activity class |
| // Look for R references in the layout |
| assert module != null; |
| Project project = module.getProject(); |
| GlobalSearchScope scope = GlobalSearchScope.allScope(project); |
| PsiClass clz = DumbService.getInstance(project).isDumb() ? |
| null : |
| JavaPsiFacade.getInstance(project).findClass(className, scope); |
| String layoutName = myResult.getFile().getName(); |
| boolean separate = false; |
| if (clz != null) { |
| // TODO: Should instead find all R.layout elements |
| // HACK AHEAD! |
| String matchText = clz.getText(); |
| final Pattern LAYOUT_FIELD_PATTERN = Pattern.compile("R\\.layout\\.([a-z0-9_]+)"); |
| Matcher matcher = LAYOUT_FIELD_PATTERN.matcher(matchText); |
| Set<String> layouts = Sets.newTreeSet(); |
| int index = 0; |
| while (true) { |
| if (matcher.find(index)) { |
| layouts.add(matcher.group(1)); |
| index = matcher.end(); |
| } |
| else { |
| break; |
| } |
| } |
| for (String layout : layouts) { |
| if (layout.equals(layoutName)) { // Don't include self |
| continue; |
| } |
| if (separate) { |
| builder.add(", "); |
| } |
| builder.addLink("Use @layout/" + layout, myLinkManager.createAssignLayoutUrl(className, layout)); |
| separate = true; |
| } |
| } |
| |
| if (separate) { |
| builder.add(", "); |
| } |
| builder.addLink("Pick Layout...", myLinkManager.createPickLayoutUrl(className)); |
| }); |
| } |
| else { |
| builder.addLink("Choose Fragment Class...", myLinkManager.createAssignFragmentUrl(className)); |
| } |
| builder.add(")"); |
| } |
| builder.endList() |
| .newline() |
| // TODO: URLs |
| .addLink("Do not warn about <fragment> tags in this session", myLinkManager.createIgnoreFragmentsUrl()) |
| .newline(); |
| |
| addIssue() |
| .setSeverity(HighlightSeverity.ERROR) |
| .setSummary("Unknown fragments") |
| .setHtmlContent(builder) |
| .build(); |
| } |
| |
| /** |
| * Support lib classes will fail to instantiate if the preview is not using the right theme. |
| */ |
| private void reportAppCompatRequired(@NotNull RenderLogger logger) { |
| Map<String, Throwable> brokenClasses = logger.getBrokenClasses(); |
| |
| if (brokenClasses.isEmpty()) { |
| return; |
| } |
| |
| brokenClasses.values().stream() |
| .filter(Objects::nonNull) |
| .filter(t -> t.getMessage() != null && t.getMessage().startsWith(APP_COMPAT_REQUIRED_MSG)) |
| .findAny() |
| .ifPresent(t -> addIssue() |
| .setSeverity(HighlightSeverity.ERROR, HIGH_PRIORITY + 1) // Reported above broken classes |
| .setSummary("Using the design library requires using Theme.AppCompat or a descendant") |
| .setHtmlContent(new HtmlBuilder() |
| .add("Select ").addItalic("Theme.AppCompat").add(" or a descendant in the theme selector.")) |
| .build()); |
| } |
| |
| public Collection<RenderErrorModel.Issue> reportIssues() { |
| RenderLogger logger = myResult.getLogger(); |
| RenderTask renderTask = myResult.getRenderTask(); |
| |
| reportMissingStyles(logger); |
| reportAppCompatRequired(logger); |
| if (renderTask != null) { |
| reportRelevantCompilationErrors(logger, renderTask); |
| reportMissingSizeAttributes(logger, renderTask.getContext(), renderTask.getXmlFile()); |
| reportMissingClasses(logger); |
| } |
| reportBrokenClasses(logger); |
| reportInstantiationProblems(logger); |
| reportOtherProblems(logger, renderTask); |
| reportUnknownFragments(logger); |
| reportRenderingFidelityProblems(logger); |
| |
| return getIssues(); |
| } |
| |
| protected RenderResult getResult() { |
| return myResult; |
| } |
| |
| protected HtmlLinkManager getLinkManager() { |
| return myLinkManager; |
| } |
| |
| protected Collection<RenderErrorModel.Issue> getIssues() { |
| return Collections.unmodifiableCollection(myIssues); |
| } |
| |
| private static class RebuildWith16Fix implements Runnable { |
| private final Project myProject; |
| |
| private RebuildWith16Fix(Project project) { |
| myProject = project; |
| } |
| |
| @Override |
| public void run() { |
| final JpsJavaCompilerOptions settings = JavacConfiguration.getOptions(myProject, JavacConfiguration.class); |
| if (!settings.ADDITIONAL_OPTIONS_STRING.isEmpty()) { |
| settings.ADDITIONAL_OPTIONS_STRING += ' '; |
| } |
| settings.ADDITIONAL_OPTIONS_STRING += "-target 1.6"; |
| CompilerManager.getInstance(myProject).rebuild(null); |
| } |
| } |
| |
| private static class SwitchTo16Fix implements Runnable { |
| final List<Module> myProblemModules; |
| private final Project myProject; |
| |
| private SwitchTo16Fix(Project project, List<Module> problemModules) { |
| myProject = project; |
| myProblemModules = problemModules; |
| } |
| |
| @Override |
| public void run() { |
| final Set<String> sdkNames = getSdkNamesFromModules(myProblemModules); |
| if (sdkNames.size() == 1) { |
| final Sdk sdk = ProjectJdkTable.getInstance().findJdk(sdkNames.iterator().next()); |
| if (sdk != null && sdk.getSdkType() instanceof AndroidSdkType) { |
| final ProjectStructureConfigurable config = ProjectStructureConfigurable.getInstance(myProject); |
| if (ShowSettingsUtil.getInstance().editConfigurable(myProject, config, () -> config.select(sdk, true))) { |
| askAndRebuild(myProject); |
| } |
| return; |
| } |
| } |
| |
| final String moduleToSelect = !myProblemModules.isEmpty() |
| ? myProblemModules.iterator().next().getName() |
| : null; |
| if (ModulesConfigurator.showDialog(myProject, moduleToSelect, ClasspathEditor.getName())) { |
| askAndRebuild(myProject); |
| } |
| } |
| } |
| |
| public static class Provider { |
| static final ExtensionPointName<Provider> EP_NAME = |
| new ExtensionPointName<>("com.android.rendering.renderErrorContributor"); |
| |
| public boolean isApplicable(Project project) { |
| return true; |
| } |
| |
| public RenderErrorContributor getContributor(@Nullable EditorDesignSurface surface, @NotNull RenderResult result, @Nullable DataContext dataContext) { |
| return new RenderErrorContributor(surface, result, dataContext); |
| } |
| } |
| } |