/*
* 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.structure;
import com.android.tools.idea.sdk.DefaultSdks;
import com.google.common.collect.Lists;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.PathChooserDialog;
import com.intellij.openapi.options.BaseConfigurable;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.DetailsComponent;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.DocumentAdapter;
import com.intellij.util.Function;
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 java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.List;
import static com.intellij.openapi.util.io.FileUtilRt.toSystemDependentName;
/**
* Allows the user set global Android SDK and JDK locations that are used for Gradle-based Android projects.
*/
public class DefaultSdksConfigurable extends BaseConfigurable implements ValidationAwareConfigurable {
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 final ConfigurableHost myHost;
// These paths are system-dependent.
@NotNull private String myOriginalJdkHomePath;
@NotNull private String myOriginalSdkHomePath;
private TextFieldWithBrowseButton mySdkLocationTextField;
private TextFieldWithBrowseButton myJdkLocationTextField;
private JPanel myWholePanel;
private DetailsComponent myDetailsComponent;
public DefaultSdksConfigurable(@Nullable ConfigurableHost host) {
myHost = host;
myWholePanel.setPreferredSize(new Dimension(700, 500));
myDetailsComponent = new DetailsComponent();
myDetailsComponent.setContent(myWholePanel);
myDetailsComponent.setText("SDK Location");
}
@Override
public void disposeUIResources() {
}
@Override
public void reset() {
myOriginalSdkHomePath = getDefaultSdkPath();
myOriginalJdkHomePath = getDefaultJdkPath();
mySdkLocationTextField.setText(myOriginalSdkHomePath);
myJdkLocationTextField.setText(myOriginalJdkHomePath);
}
@Override
public void apply() throws ConfigurationException {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
DefaultSdks.setDefaultAndroidHome(getSdkLocation());
DefaultSdks.setDefaultJavaHome(getJdkLocation());
if (!ApplicationManager.getApplication().isUnitTestMode()) {
RunAndroidSdkManagerAction.updateInWelcomePage(myDetailsComponent.getComponent());
}
}
});
}
private void createUIComponents() {
createSdkLocationTextField();
createJdkLocationTextField();
}
private void createSdkLocationTextField() {
final FileChooserDescriptor descriptor = createSingleFolderDescriptor("Choose Android SDK Location", new Function<File, Void>() {
@Override
public Void fun(File file) {
if (!DefaultSdks.isValidAndroidSdkPath(file)) {
throw new IllegalArgumentException(CHOOSE_VALID_SDK_DIRECTORY_ERR);
}
return null;
}
});
JTextField textField = new JTextField(10);
mySdkLocationTextField = new TextFieldWithBrowseButton(textField, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
VirtualFile suggestedDir = null;
File sdkLocation = getSdkLocation();
if (sdkLocation.isDirectory()) {
suggestedDir = VfsUtil.findFileByIoFile(sdkLocation, false);
}
VirtualFile chosen = FileChooser.chooseFile(descriptor, null, suggestedDir);
if (chosen != null) {
File f = VfsUtilCore.virtualToIoFile(chosen);
mySdkLocationTextField.setText(f.getPath());
}
}
});
installValidationListener(textField);
}
private void createJdkLocationTextField() {
final FileChooserDescriptor descriptor = createSingleFolderDescriptor("Choose JDK Location", new Function<File, Void>() {
@Override
public Void fun(File file) {
if (!JavaSdk.checkForJdk(file)) {
throw new IllegalArgumentException(CHOOSE_VALID_JDK_DIRECTORY_ERR);
}
return null;
}
});
JTextField textField = new JTextField(10);
myJdkLocationTextField = new TextFieldWithBrowseButton(textField, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
VirtualFile suggestedDir = null;
File jdkLocation = getJdkLocation();
if (jdkLocation.isDirectory()) {
suggestedDir = VfsUtil.findFileByIoFile(jdkLocation, false);
}
VirtualFile chosen = FileChooser.chooseFile(descriptor, null, suggestedDir);
if (chosen != null) {
File f = VfsUtilCore.virtualToIoFile(chosen);
myJdkLocationTextField.setText(f.getPath());
}
}
});
installValidationListener(textField);
}
private void installValidationListener(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 = VfsUtilCore.virtualToIoFile(virtualFile);
validation.fun(file);
}
}
};
if (SystemInfo.isMac) {
descriptor.putUserData(PathChooserDialog.NATIVE_MAC_CHOOSER_SHOW_HIDDEN_FILES, Boolean.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()) || !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 = DefaultSdks.getEligibleAndroidSdks();
if (!allAndroidSdks.isEmpty()) {
return allAndroidSdks.get(0);
}
if (!create) {
return null;
}
AndroidSdkData sdkData = AndroidSdkUtils.tryToChooseAndroidSdk();
if (sdkData == null) {
return null;
}
List<Sdk> sdks = DefaultSdks.createAndroidSdksForAllTargets(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 = DefaultSdks.getDefaultAndroidHome();
if (path != null) {
return path.getPath();
}
Sdk sdk = getFirstDefaultAndroidSdk(true);
if (sdk != null) {
String sdkHome = sdk.getHomePath();
if (sdkHome != null) {
return FileUtil.toSystemDependentName(sdkHome);
}
}
return "";
}
/**
* @return what the IDE is using as the home path for the JDK.
*/
@NotNull
private static String getDefaultJdkPath() {
File javaHome = DefaultSdks.getDefaultJavaHome();
return javaHome != null ? javaHome.getPath() : "";
}
@NotNull
private File getJdkLocation() {
String jdkLocation = myJdkLocationTextField.getText();
return new File(toSystemDependentName(jdkLocation));
}
@NotNull
private File getSdkLocation() {
String sdkLocation = mySdkLocationTextField.getText();
return new File(toSystemDependentName(sdkLocation));
}
@Override
@NotNull
public JComponent getPreferredFocusedComponent() {
return mySdkLocationTextField.getTextField();
}
public boolean validate() throws ConfigurationException {
if (!DefaultSdks.isValidAndroidSdkPath(getSdkLocation())) {
throw new ConfigurationException(CHOOSE_VALID_SDK_DIRECTORY_ERR);
}
if (!JavaSdk.checkForJdk(getJdkLocation())) {
throw new ConfigurationException(CHOOSE_VALID_JDK_DIRECTORY_ERR);
}
return true;
}
@Override
@NotNull
public List<ProjectConfigurationError> validateState() {
List<ProjectConfigurationError> errors = Lists.newArrayList();
if (!DefaultSdks.isValidAndroidSdkPath(getSdkLocation())) {
ProjectConfigurationError error =
new ProjectConfigurationError(CHOOSE_VALID_SDK_DIRECTORY_ERR, mySdkLocationTextField.getTextField());
errors.add(error);
}
if (!JavaSdk.checkForJdk(getJdkLocation())) {
ProjectConfigurationError error =
new ProjectConfigurationError(CHOOSE_VALID_JDK_DIRECTORY_ERR, myJdkLocationTextField.getTextField());
errors.add(error);
}
return errors;
}
/**
* @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() && DefaultSdks.isValidAndroidSdkPath(new File(sdkPath));
return !validJdk || !validSdk;
}
}