/* * 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.sdk; import com.android.SdkConstants; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.tools.idea.gradle.project.GradleProjectImporter; import com.android.tools.idea.gradle.util.LocalProperties; import com.android.tools.idea.gradle.util.Projects; import com.android.tools.idea.startup.AndroidStudioSpecificInitializer; import com.android.tools.idea.startup.ExternalAnnotationsSupport; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.projectRoots.*; import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.SystemProperties; import com.intellij.util.ui.UIUtil; import org.jetbrains.android.actions.RunAndroidSdkManagerAction; import org.jetbrains.android.sdk.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.List; import static org.jetbrains.android.sdk.AndroidSdkUtils.chooseNameForNewLibrary; import static org.jetbrains.android.sdk.AndroidSdkUtils.createNewAndroidPlatform; public final class DefaultSdks { private static final Logger LOG = Logger.getInstance(DefaultSdks.class); private static final String ERROR_DIALOG_TITLE = "Project SDK Update"; private DefaultSdks() { } /** * @return what the IDE is using as the home path for the Android SDK for new projects. */ @Nullable public static File getDefaultAndroidHome() { String sdkHome = null; Sdk sdk = getFirstAndroidSdk(); if (sdk != null) { sdkHome = sdk.getHomePath(); } if (sdkHome != null) { return new File(FileUtil.toSystemDependentName(sdkHome)); } return null; } @Nullable public static File getDefaultJavaHome() { List<Sdk> androidSdks = getEligibleAndroidSdks(); if (androidSdks.isEmpty()) { // This happens when user has a fresh installation of Android Studio without an Android SDK, but with a JDK. Android Studio should // populate the text field with the existing JDK. Sdk jdk = Jdks.chooseOrCreateJavaSdk(); if (jdk != null) { String jdkHomePath = jdk.getHomePath(); if (jdkHomePath != null) { return new File(FileUtil.toSystemDependentName(jdkHomePath)); } } } else { for (Sdk sdk : androidSdks) { AndroidSdkAdditionalData data = (AndroidSdkAdditionalData)sdk.getSdkAdditionalData(); assert data != null; Sdk jdk = data.getJavaSdk(); if (jdk != null) { String jdkHomePath = jdk.getHomePath(); if (jdkHomePath != null) { return new File(FileUtil.toSystemDependentName(jdkHomePath)); } } } } return null; } /** * @return the first SDK it finds that matches our default naming convention. There will be several SDKs so named, one for each build * target installed in the SDK; which of those this method returns is not defined. */ @Nullable private static Sdk getFirstAndroidSdk() { List<Sdk> allAndroidSdks = getEligibleAndroidSdks(); if (!allAndroidSdks.isEmpty()) { return allAndroidSdks.get(0); } return null; } public static void setDefaultJavaHome(@NotNull File path) { // Set up a list of SDKs we don't need any more. At the end we'll delete them. List<Sdk> sdksToDelete = Lists.newArrayList(); if (JavaSdk.checkForJdk(path)) { File canonicalPath = resolvePath(path); // Try to set this path into the "default" JDK associated with the IntelliJ SDKs. Sdk defaultJdk = getDefaultJdk(); if (defaultJdk != null) { setJdkPath(defaultJdk, canonicalPath); // Flip through the IntelliJ SDKs and make sure they point to this JDK. updateAllSdks(defaultJdk); } else { // We didn't have a JDK set at all. Try to create one. VirtualFile virtualPath = VfsUtil.findFileByIoFile(canonicalPath, true); if (virtualPath != null) { defaultJdk = createJdk(virtualPath); } } if (AndroidStudioSpecificInitializer.isAndroidStudio()) { // Now iterate through all the JDKs and delete any that aren't the default one. List<Sdk> jdks = ProjectJdkTable.getInstance().getSdksOfType(JavaSdk.getInstance()); if (defaultJdk != null) { for (Sdk jdk : jdks) { if (jdk.getName() != defaultJdk.getName()) { sdksToDelete.add(defaultJdk); } else { // This may actually be a different copy of the SDK than what we obtained from the JDK. Set its path to be sure. setJdkPath(jdk, canonicalPath); } } } } for (final Sdk sdk : sdksToDelete) { ProjectJdkTable.getInstance().removeJdk(sdk); } } } public static List<Sdk> setDefaultAndroidHome(@NotNull File path) { return setDefaultAndroidHome(path, null); } /** * Sets the given JDK's home path to the given path, and resets all of its content roots. */ private static void setJdkPath(@NotNull Sdk sdk, @NotNull File path) { SdkModificator sdkModificator = sdk.getSdkModificator(); sdkModificator.setHomePath(path.getPath()); sdkModificator.removeAllRoots(); ExternalAnnotationsSupport.attachJdkAnnotations(sdkModificator); sdkModificator.commitChanges(); JavaSdk.getInstance().setupSdkPaths(sdk); } /** * Iterates through all Android SDKs and makes them point to the given JDK. */ private static void updateAllSdks(@NotNull Sdk jdk) { for (Sdk sdk : AndroidSdkUtils.getAllAndroidSdks()) { AndroidSdkAdditionalData oldData = (AndroidSdkAdditionalData)sdk.getSdkAdditionalData(); if (oldData == null) { continue; } oldData.setJavaSdk(jdk); SdkModificator modificator = sdk.getSdkModificator(); modificator.setSdkAdditionalData(oldData); modificator.commitChanges(); } } /** * Sets the path of Android Studio's default Android SDK. This method should be called in a write action. It is assumed that the given * path has been validated by {@link #isValidAndroidSdkPath(File)}. This method will fail silently if the given path is not valid. * * * @param path the path of the Android SDK. * @see com.intellij.openapi.application.Application#runWriteAction(Runnable) */ public static List<Sdk> setDefaultAndroidHome(@NotNull File path, @Nullable Sdk javaSdk) { if (isValidAndroidSdkPath(path)) { assert ApplicationManager.getApplication().isWriteAccessAllowed(); // Since removing SDKs is *not* asynchronous, we force an update of the SDK Manager. // If we don't force this update, AndroidSdkUtils will still use the old SDK until all SDKs are properly deleted. AndroidSdkData oldSdkData = AndroidSdkData.getSdkData(path); AndroidSdkUtils.setSdkData(oldSdkData); // Set up a list of SDKs we don't need any more. At the end we'll delete them. List<Sdk> sdksToDelete = Lists.newArrayList(); File resolved = resolvePath(path); String resolvedPath = resolved.getPath(); // Parse out the new SDK. We'll need its targets to set up IntelliJ SDKs for each. AndroidSdkData sdkData = AndroidSdkData.getSdkData(resolvedPath); if (sdkData != null) { // Iterate over all current existing IJ Android SDKs for (Sdk sdk : AndroidSdkUtils.getAllAndroidSdks()) { if (sdk.getName().startsWith(AndroidSdkUtils.SDK_NAME_PREFIX)) { sdksToDelete.add(sdk); } } } for (Sdk sdk : sdksToDelete) { ProjectJdkTable.getInstance().removeJdk(sdk); } // If there are any API targets that we haven't created IntelliJ SDKs for yet, fill those in. List<Sdk> sdks = createAndroidSdksForAllTargets(resolved, javaSdk); // Update the local.properties files for any open projects. updateLocalPropertiesAndSync(resolved); return sdks; } return Collections.emptyList(); } /** * @return {@code true} if the given Android SDK path points to a valid Android SDK. */ public static boolean isValidAndroidSdkPath(@NotNull File path) { return AndroidSdkType.validateAndroidSdk(path.getPath()).getFirst(); } @NotNull public static List<Sdk> createAndroidSdksForAllTargets(@NotNull File androidHome) { List<Sdk> sdks = createAndroidSdksForAllTargets(androidHome, null); RunAndroidSdkManagerAction.updateInWelcomePage(null); return sdks; } /** * Creates a set of IntelliJ SDKs (one for each build target) corresponding to the Android SDK in the given directory, if SDKs with the * default naming convention and each individual build target do not already exist. If IntelliJ SDKs do exist, they are not updated. */ @NotNull private static List<Sdk> createAndroidSdksForAllTargets(@NotNull File androidHome, @Nullable Sdk javaSdk) { AndroidSdkData sdkData = AndroidSdkData.getSdkData(androidHome); if (sdkData == null) { return Collections.emptyList(); } IAndroidTarget[] targets = sdkData.getTargets(); if (targets.length == 0) { return Collections.emptyList(); } List<Sdk> sdks = Lists.newArrayList(); Sdk defaultJdk = javaSdk != null ? javaSdk : getDefaultJdk(); for (IAndroidTarget target : targets) { if (target.isPlatform() && !doesDefaultAndroidSdkExist(target)) { String name = chooseNameForNewLibrary(target); Sdk sdk = createNewAndroidPlatform(target, sdkData.getLocation().getPath(), name, defaultJdk, true); sdks.add(sdk); } } return sdks; } /** * @return {@code true} if an IntelliJ SDK with the default naming convention already exists for the given Android build target. */ private static boolean doesDefaultAndroidSdkExist(@NotNull IAndroidTarget target) { for (Sdk sdk : getEligibleAndroidSdks()) { IAndroidTarget platformTarget = getTarget(sdk); AndroidVersion version = target.getVersion(); AndroidVersion existingVersion = platformTarget.getVersion(); if (existingVersion.equals(version)) { return true; } } return false; } @NotNull private static IAndroidTarget getTarget(@NotNull Sdk sdk) { AndroidSdkAdditionalData data = (AndroidSdkAdditionalData)sdk.getSdkAdditionalData(); assert data != null; AndroidPlatform androidPlatform = data.getAndroidPlatform(); assert androidPlatform != null; return androidPlatform.getTarget(); } private static void updateLocalPropertiesAndSync(@NotNull final File sdkHomePath) { ProjectManager projectManager = ApplicationManager.getApplication().getComponent(ProjectManager.class); Project[] openProjects = projectManager.getOpenProjects(); if (openProjects.length == 0) { return; } final List<String> projectsToUpdateNames = Lists.newArrayList(); List<Pair<Project, LocalProperties>> localPropertiesToUpdate = Lists.newArrayList(); for (Project project : openProjects) { if (!Projects.isGradleProject(project)) { continue; } try { LocalProperties localProperties = new LocalProperties(project); if (!FileUtil.filesEqual(sdkHomePath, localProperties.getAndroidSdkPath())) { localPropertiesToUpdate.add(Pair.create(project, localProperties)); projectsToUpdateNames.add("'" + project.getName() + "'"); } } catch (IOException e) { // Exception thrown when local.properties file exists but cannot be read (e.g. no writing permissions.) logAndShowErrorWhenUpdatingLocalProperties(project, e, "read", sdkHomePath); } } if (!localPropertiesToUpdate.isEmpty()) { if (!ApplicationManager.getApplication().isUnitTestMode()) { UIUtil.invokeAndWaitIfNeeded(new Runnable() { @Override public void run() { String format = "The local.properties files in projects %1$s will be modified with the path of Android Studio's default Android SDK:\n" + "'%2$s'"; Messages.showErrorDialog(String.format(format, projectsToUpdateNames, sdkHomePath), "Sync Android SDKs"); } }); } GradleProjectImporter projectImporter = GradleProjectImporter.getInstance(); for (Pair<Project, LocalProperties> toUpdate : localPropertiesToUpdate) { Project project = toUpdate.getFirst(); try { LocalProperties localProperties = toUpdate.getSecond(); if (!FileUtil.filesEqual(sdkHomePath, localProperties.getAndroidSdkPath())) { localProperties.setAndroidSdkPath(sdkHomePath); localProperties.save(); } } catch (IOException e) { logAndShowErrorWhenUpdatingLocalProperties(project, e, "update", sdkHomePath); // No point in syncing project if local.properties is pointing to the wrong SDK. continue; } if (ApplicationManager.getApplication().isUnitTestMode()) { // Don't sync in tests. For now. continue; } projectImporter.requestProjectSync(project, null); } } } private static void logAndShowErrorWhenUpdatingLocalProperties(@NotNull Project project, @NotNull Exception error, @NotNull String action, @NotNull File sdkHomePath) { LOG.info(error); String msg = String.format("Unable to %1$s local.properties file in project '%2$s'.\n\n" + "Cause: %3$s\n\n" + "Please manually update the file's '%4$s' property value to \n" + "'%5$s'\n" + "and sync the project with Gradle files.", action, project.getName(), getMessage(error), SdkConstants.SDK_DIR_PROPERTY, sdkHomePath.getPath()); Messages.showErrorDialog(project, msg, ERROR_DIALOG_TITLE); } @NotNull private static String getMessage(@NotNull Exception e) { String cause = e.getMessage(); if (Strings.isNullOrEmpty(cause)) { cause = "[Unknown]"; } return cause; } @NotNull private static File resolvePath(@NotNull File path) { try { String resolvedPath = FileUtil.resolveShortWindowsName(path.getPath()); return new File(resolvedPath); } catch (IOException e) { //file doesn't exist yet } return path; } /** * @return the JDK with the default naming convention, creating one if it is not set up. */ @Nullable public static Sdk getDefaultJdk() { List<Sdk> androidSdks = getEligibleAndroidSdks(); if (!androidSdks.isEmpty()) { Sdk androidSdk = androidSdks.get(0); AndroidSdkAdditionalData data = (AndroidSdkAdditionalData)androidSdk.getSdkAdditionalData(); assert data != null; Sdk jdk = data.getJavaSdk(); if (jdk != null) { return jdk; } } List<Sdk> jdks = ProjectJdkTable.getInstance().getSdksOfType(JavaSdk.getInstance()); if (!jdks.isEmpty()) { return jdks.get(0); } final Collection<String> jdkPaths = JavaSdk.getInstance().suggestHomePaths(); VirtualFile javaHome = null; for (String jdkPath : jdkPaths) { javaHome = jdkPath != null ? LocalFileSystem.getInstance().findFileByPath(jdkPath) : null; if (javaHome != null) { break; } } if (javaHome == null) { javaHome = LocalFileSystem.getInstance().findFileByPath(SystemProperties.getJavaHome()); } return javaHome != null ? createJdk(javaHome) : null; } /** * Filters through all Android SDKs and returns only those that have our special name prefix and which have additional data and a * platform. */ @NotNull public static List<Sdk> getEligibleAndroidSdks() { List<Sdk> sdks = Lists.newArrayList(); for (Sdk sdk : AndroidSdkUtils.getAllAndroidSdks()) { SdkAdditionalData sdkData = sdk.getSdkAdditionalData(); if (sdkData instanceof AndroidSdkAdditionalData) { AndroidSdkAdditionalData androidSdkData = (AndroidSdkAdditionalData)sdkData; if (sdk.getName().startsWith(AndroidSdkUtils.SDK_NAME_PREFIX) && androidSdkData.getAndroidPlatform() != null) { sdks.add(sdk); } } } return sdks; } /** * Creates an IntelliJ SDK for the JDK at the given location and returns it, or {@code null} if it could not be created successfully. */ @Nullable private static Sdk createJdk(@NotNull VirtualFile homeDirectory) { Sdk newSdk = SdkConfigurationUtil.setupSdk(ProjectJdkTable.getInstance().getAllJdks(), homeDirectory, JavaSdk.getInstance(), true, null, AndroidSdkUtils.DEFAULT_JDK_NAME); if (newSdk != null) { SdkConfigurationUtil.addSdk(newSdk); } return newSdk; } }