/* * Copyright (C) 2014 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.gradle.project; import com.android.SdkConstants; import com.android.builder.model.AndroidProject; import com.android.sdklib.AndroidTargetHash; import com.android.sdklib.AndroidVersion; import com.android.sdklib.repository.FullRevision; import com.android.sdklib.repository.descriptors.IPkgDesc; import com.android.sdklib.repository.descriptors.PkgDesc; import com.android.tools.idea.gradle.GradleSyncState; import com.android.tools.idea.gradle.IdeaAndroidProject; import com.android.tools.idea.gradle.customizer.AbstractDependenciesModuleCustomizer; import com.android.tools.idea.gradle.facet.AndroidGradleFacet; import com.android.tools.idea.gradle.facet.JavaGradleFacet; import com.android.tools.idea.gradle.messages.AbstractNavigatable; import com.android.tools.idea.gradle.messages.Message; import com.android.tools.idea.gradle.messages.ProjectSyncMessages; import com.android.tools.idea.gradle.parser.GradleSettingsFile; import com.android.tools.idea.gradle.service.notification.hyperlink.InstallPlatformHyperlink; import com.android.tools.idea.gradle.service.notification.hyperlink.NotificationHyperlink; import com.android.tools.idea.gradle.service.notification.hyperlink.OpenAndroidSdkManagerHyperlink; import com.android.tools.idea.gradle.service.notification.hyperlink.OpenFileHyperlink; import com.android.tools.idea.gradle.structure.AndroidProjectSettingsService; import com.android.tools.idea.gradle.util.GradleUtil; import com.android.tools.idea.gradle.util.ProjectBuilder; import com.android.tools.idea.gradle.util.Projects; import com.android.tools.idea.gradle.variant.conflict.Conflict; import com.android.tools.idea.gradle.variant.conflict.ConflictSet; import com.android.tools.idea.gradle.variant.profiles.ProjectProfileSelectionDialog; import com.android.tools.idea.rendering.ProjectResourceRepository; import com.android.tools.idea.sdk.DefaultSdks; import com.android.tools.idea.sdk.Jdks; import com.android.tools.idea.sdk.VersionCheck; import com.android.tools.idea.sdk.wizard.SdkQuickfixWizard; import com.android.tools.idea.startup.AndroidStudioSpecificInitializer; import com.android.tools.idea.templates.TemplateManager; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.intellij.jarFinder.InternetAttachSourceProvider; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.externalSystem.util.ExternalSystemConstants; import com.intellij.openapi.module.*; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.projectRoots.SdkAdditionalData; import com.intellij.openapi.projectRoots.SdkModificator; import com.intellij.openapi.roots.*; import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable; import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.roots.libraries.LibraryTable; import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.StandardFileSystems; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.java.LanguageLevel; import com.intellij.util.SystemProperties; import com.intellij.util.io.URLUtil; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.sdk.AndroidSdkAdditionalData; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.gradle.util.GradleConstants; import java.io.File; import java.util.Collection; import java.util.List; import java.util.Set; import static com.android.tools.idea.gradle.messages.CommonMessageGroupNames.FAILED_TO_SET_UP_SDK; import static com.android.tools.idea.gradle.service.notification.errors.AbstractSyncErrorHandler.FAILED_TO_SYNC_GRADLE_PROJECT_ERROR_GROUP_FORMAT; import static com.android.tools.idea.gradle.util.Projects.hasErrors; import static com.android.tools.idea.gradle.variant.conflict.ConflictResolution.solveSelectionConflicts; import static com.android.tools.idea.gradle.variant.conflict.ConflictSet.findConflicts; import static com.intellij.notification.NotificationType.INFORMATION; import static org.jetbrains.android.sdk.AndroidSdkUtils.isAndroidSdk; public class PostProjectSetupTasksExecutor { private static boolean ourSdkVersionWarningShown; @NotNull private final Project myProject; private static final boolean DEFAULT_GENERATE_SOURCES_AFTER_SYNC = true; private volatile boolean myGenerateSourcesAfterSync = DEFAULT_GENERATE_SOURCES_AFTER_SYNC; @NotNull public static PostProjectSetupTasksExecutor getInstance(@NotNull Project project) { return ServiceManager.getService(project, PostProjectSetupTasksExecutor.class); } public PostProjectSetupTasksExecutor(@NotNull Project project) { myProject = project; } public void onProjectRestoreFromDisk() { ProjectSyncMessages messages = ProjectSyncMessages.getInstance(myProject); boolean checkJdkVersion = true; Collection<Sdk> invalidAndroidSdks = Sets.newHashSet(); ModuleManager moduleManager = ModuleManager.getInstance(myProject); for (Module module : moduleManager.getModules()) { AndroidFacet androidFacet = AndroidFacet.getInstance(module); if (androidFacet != null && androidFacet.getIdeaAndroidProject() != null) { Sdk sdk = ModuleRootManager.getInstance(module).getSdk(); if (sdk != null && !invalidAndroidSdks.contains(sdk) && isMissingAndroidLibrary(sdk)) { invalidAndroidSdks.add(sdk); } IdeaAndroidProject androidProject = androidFacet.getIdeaAndroidProject(); Collection<String> unresolved = androidProject.getDelegate().getUnresolvedDependencies(); messages.reportUnresolvedDependencies(unresolved, module); if (checkJdkVersion && !hasCorrectJdkVersion(module, androidProject)) { // we already displayed the error, no need to check each module. checkJdkVersion = false; } continue; } JavaGradleFacet javaFacet = JavaGradleFacet.getInstance(module); if (javaFacet != null && javaFacet.getJavaModel() != null) { List<String> unresolved = javaFacet.getJavaModel().getUnresolvedDependencyNames(); messages.reportUnresolvedDependencies(unresolved, module); } } if (hasErrors(myProject)) { addSdkLinkIfNecessary(); checkSdkVersion(myProject); return; } if (!invalidAndroidSdks.isEmpty()) { reinstallMissingPlatforms(invalidAndroidSdks); } findAndShowVariantConflicts(); addSdkLinkIfNecessary(); checkSdkVersion(myProject); TemplateManager.getInstance().refreshDynamicTemplateMenu(myProject); } private static boolean isMissingAndroidLibrary(@NotNull Sdk sdk) { if (isAndroidSdk(sdk)) { for (VirtualFile library : sdk.getRootProvider().getFiles(OrderRootType.CLASSES)) { // This code does not through the classes in the Android SDK. It iterates through a list of 3 files in the IDEA SDK: android.jar, // annotations.jar and res folder. if (!library.exists()) { return true; } } } return false; } private void reinstallMissingPlatforms(@NotNull Collection<Sdk> invalidAndroidSdks) { ProjectSyncMessages messages = ProjectSyncMessages.getInstance(myProject); List<AndroidVersion> versionsToInstall = Lists.newArrayList(); List<String> missingPlatforms = Lists.newArrayList(); for (Sdk sdk : invalidAndroidSdks) { SdkAdditionalData additionalData = sdk.getSdkAdditionalData(); if (additionalData instanceof AndroidSdkAdditionalData) { String platform = ((AndroidSdkAdditionalData)additionalData).getBuildTargetHashString(); if (platform != null) { missingPlatforms.add("'" + platform + "'"); AndroidVersion version = AndroidTargetHash.getPlatformVersion(platform); if (version != null) { versionsToInstall.add(version); } } } } if (!versionsToInstall.isEmpty()) { String group = String.format(FAILED_TO_SYNC_GRADLE_PROJECT_ERROR_GROUP_FORMAT, myProject.getName()); String text = "Missing Android platform(s) detected: " + Joiner.on(", ").join(missingPlatforms); Message msg = new Message(group, Message.Type.ERROR, text); messages.add(msg, new InstallPlatformHyperlink(versionsToInstall.toArray(new AndroidVersion[versionsToInstall.size()]))); } } public void onProjectSyncCompletion() { ModuleManager moduleManager = ModuleManager.getInstance(myProject); for (Module module : moduleManager.getModules()) { if (!hasCorrectJdkVersion(module)) { // we already displayed the error, no need to check each module. break; } } if (hasErrors(myProject)) { addSdkLinkIfNecessary(); checkSdkVersion(myProject); GradleSyncState.getInstance(myProject).syncEnded(); return; } attachSourcesToLibraries(); ensureAllModulesHaveValidSdks(); Projects.enforceExternalBuild(myProject); if (AndroidStudioSpecificInitializer.isAndroidStudio()) { // We remove modules not present in settings.gradle in Android Studio only. IDEA allows to have non-Gradle modules in Gradle projects. removeModulesNotInGradleSettingsFile(); } else { AndroidGradleProjectComponent.getInstance(myProject).checkForSupportedModules(); } findAndShowVariantConflicts(); checkSdkVersion(myProject); addSdkLinkIfNecessary(); ProjectResourceRepository.moduleRootsChanged(myProject); GradleSyncState.getInstance(myProject).syncEnded(); if (myGenerateSourcesAfterSync) { ProjectBuilder.getInstance(myProject).generateSourcesOnly(); } else { // set default value back. myGenerateSourcesAfterSync = DEFAULT_GENERATE_SOURCES_AFTER_SYNC; } TemplateManager.getInstance().refreshDynamicTemplateMenu(myProject); } private void ensureAllModulesHaveValidSdks() { Set<Sdk> androidSdks = Sets.newHashSet(); ModuleManager moduleManager = ModuleManager.getInstance(myProject); for (Module module : moduleManager.getModules()) { ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module); ModifiableRootModel model = moduleRootManager.getModifiableModel(); Sdk sdk = model.getSdk(); if (sdk != null) { if (isAndroidSdk(sdk)) { androidSdks.add(sdk); } model.dispose(); continue; } try { Sdk jdk = DefaultSdks.getDefaultJdk(); model.setSdk(jdk); } finally { model.commit(); } } for (Sdk sdk: androidSdks) { refreshLibrariesIn(sdk); } } // After a sync, the contents of an IDEA SDK does not get refreshed. This is an issue when an IDEA SDK is corrupt (e.g. missing libraries // like android.jar) and then it is restored by installing the missing platform from within the IDE (using a "quick fix.") After the // automatic project sync (triggered by the SDK restore) the contents of the SDK are not refreshed, and references to Android classes are // not found in editors. Removing and adding the libraries effectively refreshes the contents of the IDEA SDK, and references in editors // work again. private static void refreshLibrariesIn(@NotNull Sdk sdk) { VirtualFile[] libraries = sdk.getRootProvider().getFiles(OrderRootType.CLASSES); SdkModificator sdkModificator = sdk.getSdkModificator(); sdkModificator.removeRoots(OrderRootType.CLASSES); sdkModificator.commitChanges(); sdkModificator = sdk.getSdkModificator(); for (VirtualFile library : libraries) { sdkModificator.addRoot(library, OrderRootType.CLASSES); } sdkModificator.commitChanges(); } private void attachSourcesToLibraries() { LibraryTable libraryTable = ProjectLibraryTable.getInstance(myProject); for (Library library : libraryTable.getLibraries()) { if (library.getFiles(OrderRootType.SOURCES).length > 0) { // has sources already. continue; } for (VirtualFile classFile : library.getFiles(OrderRootType.CLASSES)) { if (!SdkConstants.EXT_JAR.equals(classFile.getExtension())) { // we only attach sources to jar files for now. continue; } VirtualFile sourceJar = findSourceJarFor(classFile); if (sourceJar != null) { Library.ModifiableModel model = library.getModifiableModel(); try { String url = AbstractDependenciesModuleCustomizer.pathToUrl(sourceJar.getPath()); model.addRoot(url, OrderRootType.SOURCES); } finally { model.commit(); } } } } } @Nullable private static VirtualFile findSourceJarFor(@NotNull VirtualFile jarFile) { String sourceFileName = jarFile.getNameWithoutExtension() + "-sources.jar"; // We need to get the real jar file. The one that we received is just a wrapper around a URL. Getting the parent from this file returns // null. File jarFilePath = getJarFromJarUrl(jarFile.getUrl()); if (jarFilePath == null) { return null; } VirtualFile realJarFile = VfsUtil.findFileByIoFile(jarFilePath, true); if (realJarFile == null) { // Unlikely to happen. At this point the jar file should exist. return null; } VirtualFile parent = realJarFile.getParent(); if (parent != null) { // Try finding sources in the same folder as the jar file. This is the layout of Maven repositories. VirtualFile sourceJar = parent.findChild(sourceFileName); if (sourceJar != null) { return sourceJar; } // Try the parent's parent. This is the layout of the repository cache in .gradle folder. parent = parent.getParent(); if (parent != null) { for (VirtualFile child : parent.getChildren()) { if (!child.isDirectory()) { continue; } sourceJar = child.findChild(sourceFileName); if (sourceJar != null) { return sourceJar; } } } } // Try IDEA's own cache. File librarySourceDirPath = InternetAttachSourceProvider.getLibrarySourceDir(); File sourceJar = new File(librarySourceDirPath, sourceFileName); return VfsUtil.findFileByIoFile(sourceJar, true); } @Nullable private static File getJarFromJarUrl(@NotNull String url) { // URLs for jar file start with "jar://" and end with "!/". if (!url.startsWith(StandardFileSystems.JAR_PROTOCOL_PREFIX)) { return null; } String path = url.substring(StandardFileSystems.JAR_PROTOCOL_PREFIX.length()); int index = path.lastIndexOf(URLUtil.JAR_SEPARATOR); if (index != -1) { path = path.substring(0, index); } return new File(FileUtil.toSystemDependentName(path)); } private void removeModulesNotInGradleSettingsFile() { GradleSettingsFile gradleSettingsFile = GradleSettingsFile.get(myProject); final List<Module> modulesToRemove = Lists.newArrayList(); final ModuleManager moduleManager = ModuleManager.getInstance(myProject); Module[] modules = moduleManager.getModules(); if (gradleSettingsFile == null) { // If there is no settings.gradle file, it means that the top-level module is the only module recognized by Gradle. if (modules.length == 1) { return; } boolean topLevelModuleFound = false; for (Module module : modules) { if (!topLevelModuleFound && isTopLevel(module)) { topLevelModuleFound = true; } else { modulesToRemove.add(module); } } } else { for (Module module : modules) { if (isNonGradleModule(module) || isOrphanGradleModule(module, gradleSettingsFile)) { modulesToRemove.add(module); } } } if (!modulesToRemove.isEmpty()) { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { ModifiableModuleModel moduleModel = moduleManager.getModifiableModel(); try { for (Module module : modulesToRemove) { removeDependencyLinks(module, moduleManager); moduleModel.disposeModule(module); } } finally { moduleModel.commit(); } } }); } } private static boolean isNonGradleModule(@NotNull Module module) { ModuleType moduleType = ModuleType.get(module); if (moduleType instanceof JavaModuleType) { String externalSystemId = module.getOptionValue(ExternalSystemConstants.EXTERNAL_SYSTEM_ID_KEY); return !GradleConstants.SYSTEM_ID.getId().equals(externalSystemId); } return false; } private static boolean isOrphanGradleModule(@NotNull Module module, @NotNull GradleSettingsFile settingsFile) { if (isTopLevel(module)) { return false; } AndroidGradleFacet facet = AndroidGradleFacet.getInstance(module); if (facet == null) { return true; } String gradleProjectPath = facet.getConfiguration().GRADLE_PROJECT_PATH; Iterable<String> allModules = settingsFile.getModules(); return !Iterables.contains(allModules, gradleProjectPath); } private static boolean isTopLevel(@NotNull Module module) { AndroidGradleFacet facet = AndroidGradleFacet.getInstance(module); if (facet == null) { // if this is the top-level module, it may not have the Gradle facet but it is still valid, because it represents the project. String moduleRootDirPath = new File(FileUtil.toSystemDependentName(module.getModuleFilePath())).getParent(); return moduleRootDirPath.equals(module.getProject().getBasePath()); } String gradleProjectPath = facet.getConfiguration().GRADLE_PROJECT_PATH; // top-level modules have Gradle path ":" return SdkConstants.GRADLE_PATH_SEPARATOR.equals(gradleProjectPath); } private static void removeDependencyLinks(@NotNull Module module, @NotNull ModuleManager moduleManager) { List<Module> dependents = moduleManager.getModuleDependentModules(module); for (Module dependent : dependents) { if (dependent.isDisposed()) { continue; } ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(dependent); ModifiableRootModel modifiableModel = moduleRootManager.getModifiableModel(); try { for (OrderEntry orderEntry : modifiableModel.getOrderEntries()) { if (orderEntry instanceof ModuleOrderEntry) { Module orderEntryModule = ((ModuleOrderEntry)orderEntry).getModule(); if (module.equals(orderEntryModule)) { modifiableModel.removeOrderEntry(orderEntry); } } } } finally { modifiableModel.commit(); } } } private void findAndShowVariantConflicts() { ConflictSet conflicts = findConflicts(myProject); List<Conflict> structureConflicts = conflicts.getStructureConflicts(); if (!structureConflicts.isEmpty() && SystemProperties.getBooleanProperty("enable.project.profiles", false)) { ProjectProfileSelectionDialog dialog = new ProjectProfileSelectionDialog(myProject, structureConflicts); dialog.show(); } List<Conflict> selectionConflicts = conflicts.getSelectionConflicts(); if (!selectionConflicts.isEmpty()) { boolean atLeastOneSolved = solveSelectionConflicts(selectionConflicts); if (atLeastOneSolved) { conflicts = findConflicts(myProject); } } conflicts.showSelectionConflicts(); } private void addSdkLinkIfNecessary() { ProjectSyncMessages messages = ProjectSyncMessages.getInstance(myProject); int sdkErrorCount = messages.getMessageCount(FAILED_TO_SET_UP_SDK); if (sdkErrorCount > 0) { // If we have errors due to platforms not being installed, we add an extra message that prompts user to open Android SDK manager and // install any missing platforms. String text = "Open Android SDK Manager and install all missing platforms."; Message hint = new Message(FAILED_TO_SET_UP_SDK, Message.Type.INFO, AbstractNavigatable.NOT_NAVIGATABLE, text); messages.add(hint, new OpenAndroidSdkManagerHyperlink()); } } private static void checkSdkVersion(@NotNull Project project) { if (project.isDisposed() || ourSdkVersionWarningShown) { return; } File androidHome = DefaultSdks.getDefaultAndroidHome(); if (androidHome != null && !VersionCheck.isCompatibleVersion(androidHome)) { InstallSdkToolsHyperlink hyperlink = new InstallSdkToolsHyperlink(VersionCheck.MIN_TOOLS_REV); String message = "Version " + VersionCheck.MIN_TOOLS_REV + " is available."; AndroidGradleNotification.getInstance(project).showBalloon("Android SDK Tools", message, INFORMATION, hyperlink); ourSdkVersionWarningShown = true; } } private boolean hasCorrectJdkVersion(@NotNull Module module) { AndroidFacet facet = AndroidFacet.getInstance(module); if (facet != null && facet.getIdeaAndroidProject() != null) { return hasCorrectJdkVersion(module, facet.getIdeaAndroidProject()); } return true; } private boolean hasCorrectJdkVersion(@NotNull Module module, @NotNull IdeaAndroidProject model) { AndroidProject androidProject = model.getDelegate(); String compileTarget = androidProject.getCompileTarget(); // TODO this is good for now, adjust this in the future to deal with 22, 23, etc. if ("android-L".equals(compileTarget) || "android-21".equals(compileTarget)) { Sdk jdk = DefaultSdks.getDefaultJdk(); if (jdk != null && !Jdks.isApplicableJdk(jdk, LanguageLevel.JDK_1_7)) { List<NotificationHyperlink> hyperlinks = Lists.newArrayList(); ProjectSettingsService service = ProjectSettingsService.getInstance(myProject); if (service instanceof AndroidProjectSettingsService) { hyperlinks.add(new OpenSdkSettingsHyperlink((AndroidProjectSettingsService)service)); } Message msg; String text = "compileSdkVersion " + compileTarget + " requires compiling with JDK 7"; VirtualFile buildFile = GradleUtil.getGradleBuildFile(module); if (buildFile != null) { hyperlinks.add(new OpenFileHyperlink(buildFile.getPath())); msg = new Message(myProject, "Project Configuration", Message.Type.ERROR, buildFile, -1, -1, text); } else { msg = new Message("Project Configuration", Message.Type.ERROR, AbstractNavigatable.NOT_NAVIGATABLE, text); } ProjectSyncMessages messages = ProjectSyncMessages.getInstance(myProject); messages.add(msg, hyperlinks.toArray(new NotificationHyperlink[hyperlinks.size()])); myProject.putUserData(Projects.HAS_WRONG_JDK, true); return false; } } return true; } public void setGenerateSourcesAfterSync(boolean generateSourcesAfterSync) { myGenerateSourcesAfterSync = generateSourcesAfterSync; } private static class OpenSdkSettingsHyperlink extends NotificationHyperlink { @NotNull private final AndroidProjectSettingsService mySettingsService; OpenSdkSettingsHyperlink(@NotNull AndroidProjectSettingsService settingsService) { super("open.sdk.settings", "Open SDK Settings"); mySettingsService = settingsService; } @Override protected void execute(@NotNull Project project) { mySettingsService.openSdkSettings(); } } private static class InstallSdkToolsHyperlink extends NotificationHyperlink { @NotNull private final FullRevision myVersion; InstallSdkToolsHyperlink(@NotNull FullRevision version) { super("install.build.tools", "Install Tools " + version); myVersion = version; } @Override protected void execute(@NotNull Project project) { List<IPkgDesc> requested = Lists.newArrayList(); if (myVersion.getMajor() == 23) { FullRevision minBuildToolsRev = new FullRevision(20, 0, 0); requested.add(PkgDesc.Builder.newPlatformTool(minBuildToolsRev).create()); } requested.add(PkgDesc.Builder.newTool(myVersion, myVersion).create()); SdkQuickfixWizard wizard = new SdkQuickfixWizard(project, null, requested); wizard.init(); if (wizard.showAndGet()) { GradleProjectImporter.getInstance().requestProjectSync(project, null); } } } }