| /* |
| * 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); |
| } |
| } |
| } |