/*
* 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.DefaultSdks;
import com.android.tools.idea.sdk.SdkMerger;
import com.android.tools.idea.sdk.SelectSdkDialog;
import com.google.common.annotations.VisibleForTesting;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.externalSystem.model.ExternalSystemException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.MessageDialogBuilder;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
import static com.android.tools.idea.sdk.DefaultSdks.isValidAndroidSdkPath;
public final class SdkSync {
private static final String ERROR_DIALOG_TITLE = "Sync Android SDKs";
private SdkSync() {
}
public static void syncIdeAndProjectAndroidHomes(@NotNull LocalProperties localProperties) {
syncIdeAndProjectAndroidHomes(localProperties, new FindValidSdkPathTask());
}
@VisibleForTesting
static void syncIdeAndProjectAndroidHomes(@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 androidHomePath = DefaultSdks.getDefaultAndroidHome();
final File projectAndroidHomePath = localProperties.getAndroidSdkPath();
if (androidHomePath != null) {
if (projectAndroidHomePath == 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, androidHomePath);
return;
}
if (!isValidAndroidSdkPath(projectAndroidHomePath)) {
// 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.
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
if (!ApplicationManager.getApplication().isUnitTestMode()) {
String format =
"The path\n'%1$s'\ndoes not refer to an Android SDK.\n\nAndroid Studio will use its default SDK instead:\n'%2$s'\n" +
"and will modify the project's local.properties file.";
Messages.showErrorDialog(String.format(format, projectAndroidHomePath, androidHomePath.getPath()), ERROR_DIALOG_TITLE);
}
setProjectSdk(localProperties, androidHomePath);
}
});
return;
}
}
else {
if (projectAndroidHomePath == null || !isValidAndroidSdkPath(projectAndroidHomePath)) {
// 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(selectedPath, localProperties);
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(projectAndroidHomePath, localProperties);
return;
}
if (!FileUtil.filesEqual(androidHomePath, projectAndroidHomePath)) {
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
// Prompt the user to choose between the SDK in the Studio and the one in local.properties.
ChooseSdkPathDialog dialog = new ChooseSdkPathDialog(androidHomePath, projectAndroidHomePath);
dialog.show();
switch (dialog.getExitCode()) {
case ChooseSdkPathDialog.USE_IDE_SDK_PATH:
setProjectSdk(localProperties, androidHomePath);
mergeIfNeeded(projectAndroidHomePath, androidHomePath);
break;
case ChooseSdkPathDialog.USE_PROJECT_SDK_PATH:
setIdeSdk(projectAndroidHomePath, localProperties);
mergeIfNeeded(androidHomePath, projectAndroidHomePath);
}
}
});
}
}
private static void setIdeSdk(@NotNull final File projectAndroidHomePath, @NotNull LocalProperties localProperties) {
// There is one case where DefaultSdks.setDefaultAndroidHome 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, projectAndroidHomePath);
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
DefaultSdks.setDefaultAndroidHome(projectAndroidHomePath);
}
});
}
});
}
private static void setProjectSdk(@NotNull LocalProperties localProperties, @NotNull File androidHomePath) {
if (FileUtil.filesEqual(localProperties.getAndroidSdkPath(), androidHomePath)) {
return;
}
localProperties.setAndroidSdkPath(androidHomePath);
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 = "The Android SDK at\n\n%1s\n\nhas packages not in your project's SDK at\n\n%2s\n\n" +
"Would you like to copy into the project SDK?";
int result = MessageDialogBuilder.yesNo("Merge SDKs", String.format(msg, sourceSdk.getPath(), destSdk.getPath()))
.yesText("Copy")
.noText("Do not copy")
.show();
if (result == Messages.YES) {
Task.Backgroundable task = new Task.Backgroundable(null, "Merging Android SDKs") {
@Override
public void run(@NotNull ProgressIndicator indicator) {
SdkMerger.mergeSdks(sourceSdk, destSdk, indicator);
}
};
ProgressManager.getInstance().run(task);
}
}
}
@VisibleForTesting
static class FindValidSdkPathTask {
@Nullable
File selectValidSdkPath() {
final AtomicReference<File> pathRef = new AtomicReference<File>();
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
findValidSdkPath(pathRef);
}
});
return pathRef.get();
}
private static void findValidSdkPath(@NotNull AtomicReference<File> pathRef) {
Sdk jdk = DefaultSdks.getDefaultJdk();
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?";
int result = Messages.showYesNoDialog(msg, ERROR_DIALOG_TITLE, null);
if (result == 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?";
int result = Messages.showYesNoDialog(String.format(format, path.getPath()), ERROR_DIALOG_TITLE, null);
if (result == Messages.YES) {
findValidSdkPath(pathRef);
}
return;
}
pathRef.set(path);
}
}
}