blob: fdac9e560b3be3e4321ab97633e3a075548b0420 [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.gradle.project;
import com.android.tools.idea.gradle.util.LocalProperties;
import com.android.tools.idea.sdk.IdeSdks;
import com.android.tools.idea.sdk.SdkMerger;
import com.android.tools.idea.sdk.SdkPaths;
import com.android.tools.idea.sdk.SdkPaths.ValidationResult;
import com.android.tools.idea.sdk.SelectSdkDialog;
import com.google.common.annotations.VisibleForTesting;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.ExternalSystemException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.MessageDialogBuilder;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Ref;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import static com.android.tools.idea.sdk.IdeSdks.isValidAndroidSdkPath;
import static com.android.tools.idea.sdk.SdkPaths.validateAndroidSdk;
import static com.intellij.openapi.util.io.FileUtil.filesEqual;
import static com.intellij.openapi.util.text.StringUtil.isEmpty;
import static com.intellij.util.ui.UIUtil.invokeAndWaitIfNeeded;
import static com.intellij.util.ui.UIUtil.invokeLaterIfNeeded;
import static org.jetbrains.android.AndroidPlugin.getGuiTestSuiteState;
import static org.jetbrains.android.AndroidPlugin.isGuiTestingMode;
public final class SdkSync {
private static final String ERROR_DIALOG_TITLE = "Sync Android SDKs";
private SdkSync() {
}
public static void syncIdeAndProjectAndroidSdks(@NotNull LocalProperties localProperties) {
syncIdeAndProjectAndroidSdks(localProperties, new FindValidSdkPathTask());
syncIdeAndProjectAndroidNdk(localProperties);
}
@VisibleForTesting
static void syncIdeAndProjectAndroidSdks(@NotNull final LocalProperties localProperties, @NotNull FindValidSdkPathTask findSdkPathTask) {
if (localProperties.hasAndroidDirProperty()) {
// if android.dir is specified, we don't sync SDKs. User is working with SDK sources.
return;
}
final File ideAndroidSdkPath = IdeSdks.getAndroidSdkPath();
final File projectAndroidSdkPath = localProperties.getAndroidSdkPath();
if (ideAndroidSdkPath != null) {
if (projectAndroidSdkPath == null) {
// If we have the IDE default SDK and we don't have a project SDK, update local.properties with default SDK path and exit.
setProjectSdk(localProperties, ideAndroidSdkPath);
return;
}
final ValidationResult validationResult = validateAndroidSdk(projectAndroidSdkPath, true);
if (!validationResult.success) {
// If we have the IDE default SDK and we don't have a valid project SDK, update local.properties with default SDK path and exit.
invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
if (!ApplicationManager.getApplication().isUnitTestMode()) {
String error = validationResult.message;
if (isEmpty(error)) {
error = String.format("The path \n'%1$s'\n" + "does not refer to a valid Android SDK.", projectAndroidSdkPath.getPath());
}
String format =
"%1$s\n\nAndroid Studio will use this Android SDK instead:\n'%2$s'\nand will modify the project's local.properties file.";
Messages.showErrorDialog(String.format(format, error, ideAndroidSdkPath.getPath()), ERROR_DIALOG_TITLE);
}
setProjectSdk(localProperties, ideAndroidSdkPath);
}
});
return;
}
}
else {
if (projectAndroidSdkPath == null || !isValidAndroidSdkPath(projectAndroidSdkPath)) {
// We don't have any SDK (IDE or project.)
File selectedPath = findSdkPathTask.selectValidSdkPath();
if (selectedPath == null) {
throw new ExternalSystemException("Unable to continue until an Android SDK is specified");
}
setIdeSdk(localProperties, selectedPath);
return;
}
// If we have a valid project SDK but we don't have IDE default SDK, update IDE with project SDK path and exit.
setIdeSdk(localProperties, projectAndroidSdkPath);
return;
}
if (!filesEqual(ideAndroidSdkPath, projectAndroidSdkPath)) {
final String msg = String.format("The project and Android Studio point to different Android SDKs.\n\n" +
"Android Studio's default SDK is in:\n" +
"%1$s\n\n" +
"The project's SDK (specified in local.properties) is in:\n" +
"%2$s\n\n" +
"To keep results consistent between IDE and command line builds, only one path can be used. " +
"Do you want to:\n\n" +
"[1] Use Android Studio's default SDK (modifies the project's local.properties file.)\n\n" +
"[2] Use the project's SDK (modifies Android Studio's default.)\n\n" +
"Note that switching SDKs could cause compile errors if the selected SDK doesn't have the " +
"necessary Android platforms or build tools.",
ideAndroidSdkPath.getPath(), projectAndroidSdkPath.getPath());
invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
int result =
MessageDialogBuilder.yesNo("Android SDK Manager", msg).yesText("Use Android Studio's SDK").noText("Use Project's SDK").show();
if (result == Messages.YES) {
// Use Android Studio's SDK
setProjectSdk(localProperties, ideAndroidSdkPath);
}
else {
// Use project's SDK
setIdeSdk(localProperties, projectAndroidSdkPath);
}
if (isGuiTestingMode() && !getGuiTestSuiteState().isSkipSdkMerge()) {
mergeIfNeeded(projectAndroidSdkPath, ideAndroidSdkPath);
}
}
});
}
}
@VisibleForTesting
static void syncIdeAndProjectAndroidNdk(@NotNull final LocalProperties localProperties) {
File projectAndroidNdkPath = localProperties.getAndroidNdkPath();
File ideAndroidNdkPath = IdeSdks.getAndroidNdkPath();
if (projectAndroidNdkPath != null) {
if (!SdkPaths.validateAndroidNdk(projectAndroidNdkPath, false).success) {
if (ideAndroidNdkPath != null) {
Logger.getInstance(SdkSync.class).warn(String.format("Replacing invalid NDK path %1$s with %2$s",
projectAndroidNdkPath, ideAndroidNdkPath));
setProjectNdk(localProperties, ideAndroidNdkPath);
}
else {
Logger.getInstance(SdkSync.class).warn(String.format("Removing invalid NDK path: %s", projectAndroidNdkPath));
setProjectNdk(localProperties, null);
}
}
}
else {
setProjectNdk(localProperties, ideAndroidNdkPath);
}
}
private static void setProjectNdk(@NotNull LocalProperties localProperties, @Nullable File ndkPath) {
localProperties.setAndroidNdkPath(ndkPath);
try {
localProperties.save();
}
catch (IOException e) {
String msg = String.format("Unable to save '%1$s'", localProperties.getFilePath().getPath());
throw new ExternalSystemException(msg, e);
}
}
private static void setIdeSdk(@NotNull LocalProperties localProperties, @NotNull final File projectAndroidSdkPath) {
// There is one case where DefaultSdks.setAndroidSdkPath will not update local.properties in the project. The conditions for this to
// happen are:
// 1. This is a fresh install of Android Studio and user does not set Android SDK
// 2. User imports a project that does not have a local.properties file
// Just to be on the safe side, we update local.properties.
setProjectSdk(localProperties, projectAndroidSdkPath);
invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
IdeSdks.setAndroidSdkPath(projectAndroidSdkPath, null);
}
});
}
});
}
private static void setProjectSdk(@NotNull LocalProperties localProperties, @NotNull File androidSdkPath) {
if (filesEqual(localProperties.getAndroidSdkPath(), androidSdkPath)) {
return;
}
localProperties.setAndroidSdkPath(androidSdkPath);
try {
localProperties.save();
}
catch (IOException e) {
String msg = String.format("Unable to save '%1$s'", localProperties.getFilePath().getPath());
throw new ExternalSystemException(msg, e);
}
}
private static void mergeIfNeeded(@NotNull final File sourceSdk, @NotNull final File destSdk) {
if (SdkMerger.hasMergeableContent(sourceSdk, destSdk)) {
String msg = String.format("The Android SDK at\n\n%1$s\n\nhas packages not in your project's SDK at\n\n%2$s\n\n" +
"Would you like to copy into the project SDK?", sourceSdk.getPath(), destSdk.getPath());
int result = MessageDialogBuilder.yesNo("Merge SDKs", msg).yesText("Copy").noText("Do not copy").show();
if (result == Messages.YES) {
new Task.Backgroundable(null, "Merging Android SDKs", false) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
SdkMerger.mergeSdks(sourceSdk, destSdk, indicator);
}
}.queue();
}
}
}
@VisibleForTesting
static class FindValidSdkPathTask {
@Nullable
File selectValidSdkPath() {
final Ref<File> pathRef = new Ref<File>();
invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
findValidSdkPath(pathRef);
}
});
return pathRef.get();
}
private static void findValidSdkPath(@NotNull Ref<File> pathRef) {
Sdk jdk = IdeSdks.getJdk();
String jdkPath = jdk != null ? jdk.getHomePath() : null;
SelectSdkDialog dialog = new SelectSdkDialog(jdkPath, null);
dialog.setModal(true);
if (!dialog.showAndGet()) {
String msg = "An Android SDK is needed to continue. Would you like to try again?";
if (Messages.showYesNoDialog(msg, ERROR_DIALOG_TITLE, null) == Messages.YES) {
findValidSdkPath(pathRef);
}
return;
}
final File path = new File(dialog.getAndroidHome());
if (!isValidAndroidSdkPath(path)) {
String format = "The path\n'%1$s'\ndoes not refer to a valid Android SDK. Would you like to try again?";
if (Messages.showYesNoDialog(String.format(format, path.getPath()), ERROR_DIALOG_TITLE, null) == Messages.YES) {
findValidSdkPath(pathRef);
}
return;
}
pathRef.set(path);
}
}
}