com.android.tools.idea.structure.DefaultSdksConfigurable.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.structure.DefaultSdksConfigurable.java

Source

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