blob: 9048d366454b9334d6a594cfed2bc4871a21debe [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.structure;
import com.android.sdklib.repository.FullRevision;
import com.android.sdklib.repository.descriptors.IPkgDesc;
import com.android.sdklib.repository.descriptors.PkgDesc;
import com.android.sdklib.repository.descriptors.PkgType;
import com.android.tools.idea.gradle.util.LocalProperties;
import com.android.tools.idea.sdk.*;
import com.android.tools.idea.sdk.SdkPaths.ValidationResult;
import com.android.tools.idea.sdk.wizard.SdkQuickfixWizard;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.options.BaseConfigurable;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.DetailsComponent;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.HyperlinkAdapter;
import com.intellij.ui.HyperlinkLabel;
import com.intellij.util.Function;
import com.intellij.util.ui.AsyncProcessIcon;
import org.jetbrains.android.actions.RunAndroidSdkManagerAction;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.HyperlinkEvent;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.util.List;
import static com.android.SdkConstants.NDK_DIR_PROPERTY;
import static com.android.tools.idea.sdk.SdkPaths.validateAndroidNdk;
import static com.android.tools.idea.sdk.SdkPaths.validateAndroidSdk;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.intellij.openapi.util.io.FileUtilRt.toSystemDependentName;
import static com.intellij.openapi.util.text.StringUtil.isEmpty;
import static com.intellij.openapi.vfs.VfsUtil.findFileByIoFile;
import static com.intellij.openapi.vfs.VfsUtilCore.virtualToIoFile;
import static org.jetbrains.android.sdk.AndroidSdkUtils.tryToChooseAndroidSdk;
/**
* Allows the user set global Android SDK and JDK locations that are used for Gradle-based Android projects.
*/
public class DefaultSdksConfigurable extends BaseConfigurable {
private static final String CHOOSE_VALID_JDK_DIRECTORY_ERR = "Please choose a valid JDK directory.";
private static final String CHOOSE_VALID_SDK_DIRECTORY_ERR = "Please choose a valid Android SDK directory.";
private static final String CHOOSE_VALID_NDK_DIRECTORY_ERR = "Please choose a valid Android NDK directory.";
private static final Logger LOG = Logger.getInstance(DefaultSdksConfigurable.class);
@Nullable private final AndroidProjectStructureConfigurable myHost;
@Nullable private final Project myProject;
// These paths are system-dependent.
private String myOriginalJdkHomePath;
private String myOriginalNdkHomePath;
private String myOriginalSdkHomePath;
private HyperlinkLabel myNdkDownloadHyperlinkLabel;
private HyperlinkLabel myNdkResetHyperlinkLabel;
private TextFieldWithBrowseButton mySdkLocationTextField;
private TextFieldWithBrowseButton myNdkLocationTextField;
private TextFieldWithBrowseButton myJdkLocationTextField;
private JPanel myWholePanel;
private JPanel myNdkDownloadPanel;
private AsyncProcessIcon myNdkCheckProcessIcon;
private DetailsComponent myDetailsComponent;
public DefaultSdksConfigurable(@Nullable AndroidProjectStructureConfigurable host, @Nullable Project project) {
myHost = host;
myProject = project;
myWholePanel.setPreferredSize(new Dimension(700, 500));
myDetailsComponent = new DetailsComponent();
myDetailsComponent.setContent(myWholePanel);
myDetailsComponent.setText("SDK Location");
// We can't update The IDE-level ndk directory. Due to that disabling the ndk directory option in the default Project Structure dialog.
if (myProject == null || myProject.isDefault()) {
myNdkLocationTextField.setEnabled(false);
}
final CardLayout layout = (CardLayout)myNdkDownloadPanel.getLayout();
layout.show(myNdkDownloadPanel, "loading");
final SdkState sdkState = SdkState.getInstance(AndroidSdkUtils.tryToChooseAndroidSdk());
sdkState.loadAsync(SdkState.DEFAULT_EXPIRATION_PERIOD_MS, false, null, new SdkLoadedCallback(true) {
@Override
public void doRun(@NotNull SdkPackages packages) {
if (!sdkState.getPackages().getRemotePkgInfos().get(PkgType.PKG_NDK).isEmpty()) {
layout.show(myNdkDownloadPanel, "link");
}
else {
myNdkDownloadPanel.setVisible(false);
}
}
}, new DispatchRunnable() {
@Override
public void doRun() {
myNdkDownloadPanel.setVisible(false);
}
}, false);
}
@Override
public void disposeUIResources() {
}
@Override
public void reset() {
myOriginalSdkHomePath = getDefaultSdkPath();
myOriginalNdkHomePath = getDefaultNdkPath();
myOriginalJdkHomePath = getDefaultJdkPath();
mySdkLocationTextField.setText(myOriginalSdkHomePath);
myNdkLocationTextField.setText(myOriginalNdkHomePath);
myJdkLocationTextField.setText(myOriginalJdkHomePath);
}
@Override
public void apply() throws ConfigurationException {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
IdeSdks.setJdkPath(getJdkLocation());
IdeSdks.setAndroidSdkPath(getSdkLocation(), myProject);
saveAndroidNdkPath();
if (!ApplicationManager.getApplication().isUnitTestMode()) {
RunAndroidSdkManagerAction.updateInWelcomePage(myDetailsComponent.getComponent());
}
}
});
}
private void saveAndroidNdkPath() {
if(myProject == null || myProject.isDefault()) {
return;
}
try {
LocalProperties localProperties = new LocalProperties(myProject);
localProperties.setAndroidNdkPath(getNdkLocation());
localProperties.save();
}
catch (IOException e) {
LOG.info(String.format("Unable to update local.properties file in project '%1$s'.", myProject.getName()), e);
String cause = e.getMessage();
if (isNullOrEmpty(cause)) {
cause = "[Unknown]";
}
String msg = String.format("Unable to update local.properties file in project '%1$s'.\n\n" +
"Cause: %2$s\n\n" +
"Please manually update the file's '%3$s' property value to \n" +
"'%4$s'\n" +
"and sync the project with Gradle files.", myProject.getName(), cause,
NDK_DIR_PROPERTY, getNdkLocation().getPath());
Messages.showErrorDialog(myProject, msg, "Android Ndk Update");
}
}
private void createUIComponents() {
myNdkCheckProcessIcon = new AsyncProcessIcon("NDK check progress");
createSdkLocationTextField();
createJdkLocationTextField();
createNdkLocationTextField();
createNdkDownloadLink();
createNdkResetLink();
}
private void createSdkLocationTextField() {
mySdkLocationTextField = createTextFieldWithBrowseButton("Choose Android SDK Location", CHOOSE_VALID_SDK_DIRECTORY_ERR,
new Function<File, ValidationResult>() {
@Override
public ValidationResult fun(File file) {
return validateAndroidSdk(file, false);
}
});
}
private void createNdkLocationTextField() {
myNdkLocationTextField = createTextFieldWithBrowseButton(
"Choose Android NDK Location", CHOOSE_VALID_NDK_DIRECTORY_ERR,
new Function<File, ValidationResult>() {
@Override
public ValidationResult fun(File file) {
return validateAndroidNdk(file, false);
}
});
}
private TextFieldWithBrowseButton createTextFieldWithBrowseButton(String title, final String errorMessagae, final Function<File,
ValidationResult> validation) {
final FileChooserDescriptor descriptor = createSingleFolderDescriptor(title, new Function<File, Void>() {
@Override
public Void fun(File file) {
ValidationResult validationResult = validation.fun(file);
if (!validationResult.success) {
String msg = validationResult.message;
if (isEmpty(msg)) {
msg = errorMessagae;
}
throw new IllegalArgumentException(msg);
}
return null;
}
});
final JTextField textField = new JTextField(10);
installValidationListener(textField);
return new TextFieldWithBrowseButton(textField, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
VirtualFile suggestedDir = null;
File ndkLocation = getNdkLocation();
if (ndkLocation.isDirectory()) {
suggestedDir = findFileByIoFile(ndkLocation, false);
}
VirtualFile chosen = FileChooser.chooseFile(descriptor, null, suggestedDir);
if (chosen != null) {
File f = virtualToIoFile(chosen);
textField.setText(f.getPath());
}
}
});
}
private void createNdkResetLink() {
myNdkResetHyperlinkLabel = new HyperlinkLabel();
myNdkResetHyperlinkLabel.setHyperlinkText("", "Select", " default NDK");
myNdkResetHyperlinkLabel.addHyperlinkListener(new HyperlinkAdapter() {
@Override
protected void hyperlinkActivated(HyperlinkEvent e) {
// known non-null since otherwise we won't show the link
//noinspection ConstantConditions
myNdkLocationTextField.setText(IdeSdks.getAndroidNdkPath().getPath());
}
});
}
private void createNdkDownloadLink() {
myNdkDownloadHyperlinkLabel = new HyperlinkLabel();
myNdkDownloadHyperlinkLabel.setHyperlinkText("", "Download", " Android NDK.");
myNdkDownloadHyperlinkLabel.addHyperlinkListener(new HyperlinkAdapter() {
@Override
protected void hyperlinkActivated(HyperlinkEvent e) {
List<IPkgDesc> requested = ImmutableList.of(PkgDesc.Builder.newNdk(FullRevision.NOT_SPECIFIED).create());
SdkQuickfixWizard wizard = new SdkQuickfixWizard(null, null, requested);
wizard.init();
if (wizard.showAndGet()) {
File ndk = IdeSdks.getAndroidNdkPath();
if (ndk != null) {
myNdkLocationTextField.setText(ndk.getPath());
}
validateState();
}
}
});
}
private void createJdkLocationTextField() {
JTextField textField = new JTextField(10);
myJdkLocationTextField = new TextFieldWithBrowseButton(textField, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
chooseJdkLocation();
}
});
installValidationListener(textField);
}
public void chooseJdkLocation() {
myJdkLocationTextField.getTextField().requestFocus();
VirtualFile suggestedDir = null;
File jdkLocation = getJdkLocation();
if (jdkLocation.isDirectory()) {
suggestedDir = findFileByIoFile(jdkLocation, false);
}
VirtualFile chosen = FileChooser.chooseFile(createSingleFolderDescriptor("Choose JDK Location", new Function<File, Void>() {
@Override
public Void fun(File file) {
if (!validateAndUpdateJdkPath(file)) {
throw new IllegalArgumentException(CHOOSE_VALID_JDK_DIRECTORY_ERR);
}
return null;
}
}), null, suggestedDir);
if (chosen != null) {
File f = virtualToIoFile(chosen);
myJdkLocationTextField.setText(f.getPath());
}
}
private void installValidationListener(@NotNull JTextField textField) {
if (myHost != null) {
textField.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
protected void textChanged(DocumentEvent e) {
myHost.requestValidation();
}
});
}
}
@NotNull
private static FileChooserDescriptor createSingleFolderDescriptor(@NotNull String title, @NotNull final Function<File, Void> validation) {
final FileChooserDescriptor descriptor = new FileChooserDescriptor(false, true, false, false, false, false) {
@Override
public void validateSelectedFiles(VirtualFile[] files) throws Exception {
for (VirtualFile virtualFile : files) {
File file = virtualToIoFile(virtualFile);
validation.fun(file);
}
}
};
if (SystemInfo.isMac) {
descriptor.withShowHiddenFiles(true);
}
descriptor.setTitle(title);
return descriptor;
}
@Override
public String getDisplayName() {
return "SDK Location";
}
@Override
public String getHelpTopic() {
return null;
}
@Nullable
@Override
public JComponent createComponent() {
return myDetailsComponent.getComponent();
}
@NotNull
public JComponent getContentPanel() {
return myWholePanel;
}
@Override
public boolean isModified() {
return !myOriginalSdkHomePath.equals(getSdkLocation().getPath())
|| !myOriginalNdkHomePath.equals(getNdkLocation().getPath())
|| !myOriginalJdkHomePath.equals(getJdkLocation().getPath());
}
/**
* Returns 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.
*
* @param create True if this method should attempt to create an SDK if one does not exist.
* @return null if an SDK is unavailable or creation failed.
*/
@Nullable
private static Sdk getFirstDefaultAndroidSdk(boolean create) {
List<Sdk> allAndroidSdks = IdeSdks.getEligibleAndroidSdks();
if (!allAndroidSdks.isEmpty()) {
return allAndroidSdks.get(0);
}
if (!create) {
return null;
}
AndroidSdkData sdkData = tryToChooseAndroidSdk();
if (sdkData == null) {
return null;
}
List<Sdk> sdks = IdeSdks.createAndroidSdkPerAndroidTarget(sdkData.getLocation());
return !sdks.isEmpty() ? sdks.get(0) : null;
}
/**
* @return what the IDE is using as the home path for the Android SDK for new projects.
*/
@NotNull
private static String getDefaultSdkPath() {
File path = IdeSdks.getAndroidSdkPath();
if (path != null) {
return path.getPath();
}
Sdk sdk = getFirstDefaultAndroidSdk(true);
if (sdk != null) {
String sdkHome = sdk.getHomePath();
if (sdkHome != null) {
return toSystemDependentName(sdkHome);
}
}
return "";
}
/**
* @return the appropriate NDK path for a given project, i.e the project's ndk path for a real project and the default NDK path default
* project.
*/
@NotNull
private String getDefaultNdkPath() {
if (myProject != null && !myProject.isDefault()) {
try {
File androidNdkPath = new LocalProperties(myProject).getAndroidNdkPath();
if (androidNdkPath != null) {
return androidNdkPath.getPath();
}
}
catch (IOException e) {
LOG.info(String.format("Unable to read local.properties file in project '%1$s'.", myProject.getName()), e);
}
} else {
File path = IdeSdks.getAndroidNdkPath();
if (path != null) {
return path.getPath();
}
}
return "";
}
/**
* @return what the IDE is using as the home path for the JDK.
*/
@NotNull
private static String getDefaultJdkPath() {
File javaHome = IdeSdks.getJdkPath();
return javaHome != null ? javaHome.getPath() : "";
}
@NotNull
private File getSdkLocation() {
String sdkLocation = mySdkLocationTextField.getText();
return new File(toSystemDependentName(sdkLocation));
}
@NotNull
private File getNdkLocation() {
String ndkLocation = myNdkLocationTextField.getText();
return new File(toSystemDependentName(ndkLocation));
}
@Override
@NotNull
public JComponent getPreferredFocusedComponent() {
return mySdkLocationTextField.getTextField();
}
public boolean validate() throws ConfigurationException {
String msg = validateAndroidSdkPath();
if (msg != null) {
throw new ConfigurationException(msg);
}
if (!validateAndUpdateJdkPath(getJdkLocation())) {
throw new ConfigurationException(CHOOSE_VALID_JDK_DIRECTORY_ERR);
}
msg = validateAndroidNdkPath();
if (msg != null) {
throw new ConfigurationException(msg);
}
return true;
}
@NotNull
public List<ProjectConfigurationError> validateState() {
List<ProjectConfigurationError> errors = Lists.newArrayList();
String msg = validateAndroidSdkPath();
if (msg != null) {
ProjectConfigurationError error = new ProjectConfigurationError(msg, mySdkLocationTextField.getTextField());
errors.add(error);
}
if (!validateAndUpdateJdkPath(getJdkLocation())) {
ProjectConfigurationError error =
new ProjectConfigurationError(CHOOSE_VALID_JDK_DIRECTORY_ERR, myJdkLocationTextField.getTextField());
errors.add(error);
}
msg = validateAndroidNdkPath();
if (msg != null) {
ProjectConfigurationError error = new ProjectConfigurationError(msg, myNdkLocationTextField.getTextField());
errors.add(error);
}
return errors;
}
/**
* @return the error message when the sdk path is not valid, {@code null} otherwise.
*/
@Nullable
private String validateAndroidSdkPath() {
ValidationResult validationResult = validateAndroidSdk(getSdkLocation(), false);
if (!validationResult.success) {
String msg = validationResult.message;
if (isEmpty(msg)) {
msg = CHOOSE_VALID_SDK_DIRECTORY_ERR;
}
return msg;
}
return null;
}
/**
* @return the error message when the ndk path is not valid, {@code null} otherwise.
*/
@Nullable
private String validateAndroidNdkPath() {
hideNdkQuickfixLink();
// As Ndk is required with for the projects with ndk modules, considering the empty value as legal.
if (!myNdkLocationTextField.getText().isEmpty()) {
ValidationResult validationResult = validateAndroidNdk(getNdkLocation(), false);
if (!validationResult.success) {
showNdkQuickfixLink();
String msg = validationResult.message;
if (isEmpty(msg)) {
msg = CHOOSE_VALID_NDK_DIRECTORY_ERR;
}
return msg;
}
}
else if (myNdkLocationTextField.isVisible()) {
showNdkQuickfixLink();
}
return null;
}
private void showNdkQuickfixLink() {
if (IdeSdks.getAndroidNdkPath() == null) {
myNdkDownloadPanel.setVisible(true);
}
else {
myNdkResetHyperlinkLabel.setVisible(true);
}
}
private void hideNdkQuickfixLink() {
myNdkResetHyperlinkLabel.setVisible(false);
myNdkDownloadPanel.setVisible(false);
}
@NotNull
private File getJdkLocation() {
String jdkLocation = myJdkLocationTextField.getText();
return new File(toSystemDependentName(jdkLocation));
}
private boolean validateAndUpdateJdkPath(@NotNull File file) {
if (JavaSdk.checkForJdk(file)) {
return true;
}
if (SystemInfo.isMac) {
File potentialPath = new File(file, IdeSdks.MAC_JDK_CONTENT_PATH);
if (potentialPath.isDirectory() && JavaSdk.checkForJdk(potentialPath)) {
myJdkLocationTextField.setText(potentialPath.getPath());
return true;
}
}
return false;
}
/**
* @return {@code true} if the configurable is needed: e.g. if we're missing a JDK or an Android SDK setting.
*/
public static boolean isNeeded() {
String jdkPath = getDefaultJdkPath();
String sdkPath = getDefaultSdkPath();
boolean validJdk = !jdkPath.isEmpty() && JavaSdk.checkForJdk(new File(jdkPath));
boolean validSdk = !sdkPath.isEmpty() && IdeSdks.isValidAndroidSdkPath(new File(sdkPath));
return !validJdk || !validSdk;
}
}