Java tutorial
/* * 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.DispatchRunnable; import com.android.tools.idea.sdk.IdeSdks; import com.android.tools.idea.sdk.SdkPaths.ValidationResult; import com.android.tools.idea.sdk.SdkState; 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 DispatchRunnable() { @Override public void doRun() { 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; } }