blob: b720e87136a00d850e24492d1ed4e2389cbd3c4d [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.sdk;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.repository.descriptors.PkgType;
import com.android.sdklib.repository.local.LocalPkgInfo;
import com.google.common.collect.Lists;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.projectRoots.*;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.util.SystemProperties;
import org.jetbrains.android.actions.RunAndroidSdkManagerAction;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import static com.android.tools.idea.gradle.util.Projects.requiresAndroidModel;
import static com.android.tools.idea.sdk.SdkPaths.validateAndroidSdk;
import static com.android.tools.idea.startup.AndroidStudioSpecificInitializer.isAndroidStudio;
import static com.intellij.ide.impl.NewProjectUtil.applyJdkToProject;
import static com.intellij.openapi.projectRoots.JavaSdk.checkForJdk;
import static com.intellij.openapi.util.io.FileUtil.*;
import static org.jetbrains.android.sdk.AndroidSdkData.getSdkData;
import static org.jetbrains.android.sdk.AndroidSdkUtils.*;
public final class IdeSdks {
@NonNls public static final String MAC_JDK_CONTENT_PATH = "/Contents/Home";
@NonNls private static final String ANDROID_SDK_PATH_KEY = "android.sdk.path";
private IdeSdks() {
}
/**
* @return what the IDE is using as the home path for the Android SDK for new projects.
*/
@Nullable
public static File getAndroidSdkPath() {
// We assume that every time new android sdk path is applied, all existing ide android sdks are removed and replaced by newly
// created ide android sdks for the platforms downloaded for the new android sdk. So, we bring the first ide android sdk configured
// at the moment and deduce android sdk path from it.
String sdkHome = null;
Sdk sdk = getFirstAndroidSdk();
if (sdk != null) {
sdkHome = sdk.getHomePath();
}
if (sdkHome != null) {
return new File(toSystemDependentName(sdkHome));
}
// There is a possible case that android sdk which path was applied previously (setAndroidSdkPath()) didn't have any
// platforms downloaded. Hence, no ide android sdk was created and we can't deduce android sdk location from it.
// Hence, we fallback to the explicitly stored android sdk path here.
PropertiesComponent component = PropertiesComponent.getInstance(ProjectManager.getInstance().getDefaultProject());
String sdkPath = component.getValue(ANDROID_SDK_PATH_KEY);
if (sdkPath != null) {
File candidate = new File(sdkPath);
if (isValidAndroidSdkPath(candidate)) {
return candidate;
}
}
return null;
}
public static File getAndroidNdkPath() {
AndroidSdkData data = AndroidSdkUtils.tryToChooseAndroidSdk();
if (data == null) {
return null;
}
LocalPkgInfo[] ndk = data.getLocalSdk().getPkgsInfos(PkgType.PKG_NDK);
if (ndk.length == 0) {
return null;
}
return ndk[0].getLocalDir();
}
@Nullable
public static File getJdkPath() {
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 jdkPath = jdk.getHomePath();
if (jdkPath != null) {
return new File(toSystemDependentName(jdkPath));
}
}
}
else {
for (Sdk sdk : androidSdks) {
AndroidSdkAdditionalData data = getAndroidSdkAdditionalData(sdk);
assert data != null;
Sdk jdk = data.getJavaSdk();
if (jdk != null) {
String jdkHomePath = jdk.getHomePath();
if (jdkHomePath != null) {
return new File(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;
}
/**
* Must run inside a WriteAction
*/
public static void setJdkPath(@NotNull File path) {
if (checkForJdk(path)) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
File canonicalPath = resolvePath(path);
Sdk chosenJdk = null;
if (isAndroidStudio()) {
// Delete all JDKs in Android Studio. We want to have only one.
List<Sdk> jdks = ProjectJdkTable.getInstance().getSdksOfType(JavaSdk.getInstance());
for (final Sdk jdk : jdks) {
ProjectJdkTable.getInstance().removeJdk(jdk);
}
}
else {
for (Sdk jdk : ProjectJdkTable.getInstance().getSdksOfType(JavaSdk.getInstance())) {
if (pathsEqual(jdk.getHomePath(), canonicalPath.getPath())) {
chosenJdk = jdk;
break;
}
}
}
if (chosenJdk == null) {
if (canonicalPath.isDirectory()) {
chosenJdk = createJdk(canonicalPath);
if (chosenJdk == null) {
// Unlikely to happen
throw new IllegalStateException("Failed to create IDEA JDK from '" + path.getPath() + "'");
}
updateAndroidSdks(chosenJdk);
ProjectManager projectManager = ApplicationManager.getApplication().getComponent(ProjectManager.class);
Project[] openProjects = projectManager.getOpenProjects();
for (Project project : openProjects) {
applyJdkToProject(project, chosenJdk);
}
}
else {
throw new IllegalStateException("The resolved path '" + canonicalPath.getPath() + "' was not found");
}
}
}
}
@NotNull
public static List<Sdk> setAndroidSdkPath(@NotNull File path, @Nullable Project currentProject) {
return setAndroidSdkPath(path, null, currentProject);
}
/**
* Iterates through all Android SDKs and makes them point to the given JDK.
*/
private static void updateAndroidSdks(@NotNull Sdk jdk) {
for (Sdk sdk : getAllAndroidSdks()) {
AndroidSdkAdditionalData oldData = getAndroidSdkAdditionalData(sdk);
if (oldData == null) {
continue;
}
oldData.setJavaSdk(jdk);
SdkModificator modificator = sdk.getSdkModificator();
modificator.setSdkAdditionalData(oldData);
modificator.commitChanges();
}
}
/**
* Sets the path of Android Studio's 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)
*/
@NotNull
public static List<Sdk> setAndroidSdkPath(@NotNull File path, @Nullable Sdk javaSdk, @Nullable Project currentProject) {
if (isValidAndroidSdkPath(path)) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
// There is a possible case that no platform is downloaded for the android sdk which path is given as an argument
// to the current method. Hence, no ide android sdk is configured and our further android sdk lookup
// (check project jdk table for the configured ide android sdk and deduce the path from it) wouldn't work. So, we save
// given path as well in order to be able to fallback to it later if there is still no android sdk configured within the ide.
if (currentProject != null && !currentProject.isDisposed()) {
String sdkPath = toCanonicalPath(path.getAbsolutePath());
PropertiesComponent.getInstance(currentProject).setValue(ANDROID_SDK_PATH_KEY, sdkPath);
if (!currentProject.isDefault()) {
// Store default sdk path for default project as well in order to be able to re-use it for another ide projects if necessary.
PropertiesComponent component = PropertiesComponent.getInstance(ProjectManager.getInstance().getDefaultProject());
component.setValue(ANDROID_SDK_PATH_KEY, sdkPath);
}
}
// 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 = getSdkData(path);
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);
// Parse out the new SDK. We'll need its targets to set up IntelliJ SDKs for each.
AndroidSdkData sdkData = getSdkData(resolved, true);
if (sdkData != null) {
// Iterate over all current existing IJ Android SDKs
for (Sdk sdk : getAllAndroidSdks()) {
if (sdk.getName().startsWith(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 = createAndroidSdkPerAndroidTarget(resolved, javaSdk);
afterAndroidSdkPathUpdate(resolved);
return sdks;
}
return Collections.emptyList();
}
private static void afterAndroidSdkPathUpdate(@NotNull File androidSdkPath) {
ProjectManager projectManager = ApplicationManager.getApplication().getComponent(ProjectManager.class);
Project[] openProjects = projectManager.getOpenProjects();
if (openProjects.length == 0) {
return;
}
AndroidSdkEventListener[] eventListeners = AndroidSdkEventListener.EP_NAME.getExtensions();
for (Project project : openProjects) {
if (!requiresAndroidModel(project)) {
continue;
}
for (AndroidSdkEventListener listener : eventListeners) {
listener.afterSdkPathChange(androidSdkPath, project);
}
}
}
/**
* @return {@code true} if the given Android SDK path points to a valid Android SDK.
*/
public static boolean isValidAndroidSdkPath(@NotNull File path) {
return validateAndroidSdk(path, false).success;
}
@NotNull
public static List<Sdk> createAndroidSdkPerAndroidTarget(@NotNull File androidSdkPath) {
List<Sdk> sdks = createAndroidSdkPerAndroidTarget(androidSdkPath, 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> createAndroidSdkPerAndroidTarget(@NotNull File androidSdkPath, @Nullable Sdk javaSdk) {
AndroidSdkData sdkData = getSdkData(androidSdkPath);
if (sdkData == null) {
return Collections.emptyList();
}
IAndroidTarget[] targets = sdkData.getTargets();
if (targets.length == 0) {
return Collections.emptyList();
}
List<Sdk> sdks = Lists.newArrayList();
Sdk ideSdk = javaSdk != null ? javaSdk : getJdk();
for (IAndroidTarget target : targets) {
if (target.isPlatform() && !doesIdeAndroidSdkExist(target)) {
String name = chooseNameForNewLibrary(target);
Sdk sdk = createNewAndroidPlatform(target, sdkData.getLocation().getPath(), name, ideSdk, 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 doesIdeAndroidSdkExist(@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) {
AndroidPlatform androidPlatform = AndroidPlatform.getInstance(sdk);
assert androidPlatform != null;
return androidPlatform.getTarget();
}
@NotNull
private static File resolvePath(@NotNull File path) {
try {
String resolvedPath = 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 getJdk() {
return getJdk(null);
}
@Nullable
public static Sdk getJdk(@Nullable JavaSdkVersion preferredVersion) {
List<Sdk> androidSdks = getEligibleAndroidSdks();
if (!androidSdks.isEmpty()) {
Sdk androidSdk = androidSdks.get(0);
AndroidSdkAdditionalData data = getAndroidSdkAdditionalData(androidSdk);
assert data != null;
Sdk jdk = data.getJavaSdk();
if (isJdkCompatible(jdk, preferredVersion)) {
return jdk;
}
}
JavaSdk javaSdk = JavaSdk.getInstance();
List<Sdk> jdks = ProjectJdkTable.getInstance().getSdksOfType(javaSdk);
if (!jdks.isEmpty()) {
for (Sdk jdk : jdks) {
if (isJdkCompatible(jdk, preferredVersion)) {
return jdk;
}
}
}
List<File> jdkPaths = getPotentialJdkPaths();
for (File jdkPath : jdkPaths) {
if (checkForJdk(jdkPath)) {
Sdk jdk = createJdk(jdkPath);
return isJdkCompatible(jdk, preferredVersion) ? jdk : null;
}
// On Linux, the returned path is the folder that contains all JDKs, instead of a specific JDK.
if (SystemInfo.isLinux) {
for (File child : notNullize(jdkPath.listFiles())) {
if (child.isDirectory() && checkForJdk(child)) {
Sdk jdk = Jdks.createJdk(child.getPath());
if (isJdkCompatible(jdk, preferredVersion)) {
return jdk;
}
}
}
}
}
return null;
}
/**
* Find all potential folders that may contain Java SDKs.
* Those folders are guaranteed to exist but they may not be valid Java homes.
*/
@NotNull
private static List<File> getPotentialJdkPaths() {
JavaSdk javaSdk = JavaSdk.getInstance();
List<String> jdkPaths = Lists.newArrayList(javaSdk.suggestHomePaths());
jdkPaths.add(SystemProperties.getJavaHome());
List<File> virtualFiles = Lists.newArrayListWithCapacity(jdkPaths.size());
for (String jdkPath : jdkPaths) {
if (jdkPath != null) {
File javaHome = new File(jdkPath);
if (javaHome.isDirectory()) {
virtualFiles.add(javaHome);
}
}
}
return virtualFiles;
}
private static boolean isJdkCompatible(@Nullable Sdk jdk, @Nullable JavaSdkVersion preferredVersion) {
if (jdk == null) {
return false;
}
if (preferredVersion == null) {
return true;
}
return JavaSdk.getInstance().isOfVersionOrHigher(jdk, preferredVersion);
}
/**
* 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 : getAllAndroidSdks()) {
if (sdk.getName().startsWith(SDK_NAME_PREFIX) && AndroidPlatform.getInstance(sdk) != 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 File homeDirectory) {
return Jdks.createJdk(homeDirectory.getPath());
}
public interface AndroidSdkEventListener {
ExtensionPointName<AndroidSdkEventListener> EP_NAME = ExtensionPointName.create("com.android.ide.sdkEventListener");
/**
* Notification that the path of the IDE's Android SDK path has changed.
*
* @param sdkPath the new Android SDK path.
* @param project one of the projects currently open in the IDE.
*/
void afterSdkPathChange(@NotNull File sdkPath, @NotNull Project project);
}
}