com.android.tools.idea.npw.NewProjectWizardDynamic.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.npw.NewProjectWizardDynamic.java

Source

/*
 * Copyright (C) 2014 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.npw;

import com.android.SdkConstants;
import com.android.repository.io.FileOpUtils;
import com.android.tools.idea.IdeInfo;
import com.android.tools.idea.gradle.plugin.AndroidPluginGeneration;
import com.android.tools.idea.gradle.project.importing.GradleProjectImporter;
import com.android.tools.idea.gradle.project.sync.GradleSyncListener;
import com.android.tools.idea.gradle.util.GradleWrapper;
import com.android.tools.idea.npw.cpp.ConfigureCppSupportPath;
import com.android.tools.idea.npw.deprecated.ConfigureAndroidProjectPath;
import com.android.tools.idea.npw.deprecated.ConfigureAndroidProjectStep;
import com.android.tools.idea.npw.deprecated.NewFormFactorModulePath;
import com.android.tools.idea.sdk.AndroidSdks;
import com.android.tools.idea.sdk.IdeSdks;
import com.android.tools.idea.templates.*;
import com.android.tools.idea.wizard.WizardConstants;
import com.android.tools.idea.wizard.dynamic.DynamicWizard;
import com.android.tools.idea.wizard.dynamic.DynamicWizardHost;
import com.android.tools.idea.wizard.dynamic.ScopedStateStore;
import com.android.tools.idea.wizard.template.TemplateWizard;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;

import static com.android.tools.idea.wizard.WizardConstants.*;

/**
 * Presents a wizard to the user to create a new project.
 */
public class NewProjectWizardDynamic extends DynamicWizard {
    private static final String ERROR_MSG_TITLE = "Error in New Project Wizard";
    private Project myProject;

    public NewProjectWizardDynamic(@Nullable Project project, @Nullable Module module) {
        super(project, module, "New Project");
        setTitle("Create New Project");
    }

    public NewProjectWizardDynamic(@Nullable Project project, @Nullable Module module,
            @NotNull DynamicWizardHost host) {
        super(project, module, "New Project", host);
        setTitle("Create New Project");
    }

    @Override
    public void init() {
        checkSdk();
        addPaths();
        initState();
        super.init();
    }

    protected void checkSdk() {
        if (!AndroidSdkUtils.isAndroidSdkAvailable() || !TemplateManager.templatesAreValid()) {
            String title = "SDK problem";
            String msg = "<html>Your Android SDK is missing, out of date, or is missing templates.<br>"
                    + "You can configure your SDK via <b>Configure | Project Defaults | Project Structure | SDKs</b></html>";
            Messages.showErrorDialog(msg, title);
            throw new IllegalStateException("Android SDK missing");
        }
    }

    /**
     * Add the steps for this wizard
     */
    private void addPaths() {
        addPath(new ConfigureAndroidProjectPath(getDisposable()));
        for (NewFormFactorModulePath path : NewFormFactorModulePath
                .getAvailableFormFactorModulePaths(getDisposable())) {
            addPath(path);
        }
        addPath(new ConfigureCppSupportPath(getDisposable()));
    }

    /**
     * Populate our state store with some common configuration items, such as the SDK location and the Gradle configuration.
     */
    private void initState() {
        initState(getState(), AndroidPluginGeneration.ORIGINAL.getLatestKnownVersion());
    }

    static void initState(@NotNull ScopedStateStore state, @NotNull String gradlePluginVersion) {
        state.put(GRADLE_PLUGIN_VERSION_KEY, gradlePluginVersion);
        state.put(GRADLE_VERSION_KEY, SdkConstants.GRADLE_LATEST_VERSION);
        state.put(IS_GRADLE_PROJECT_KEY, true);
        state.put(IS_NEW_PROJECT_KEY, true);
        state.put(TARGET_FILES_KEY, new HashSet<>());
        state.put(FILES_TO_OPEN_KEY, new ArrayList<>());
        state.put(USE_PER_MODULE_REPOS_KEY, false);
        state.put(ALSO_CREATE_IAPK_KEY, true);

        // For now, our definition of low memory is running in a 32-bit JVM. In this case, we have to be careful about the amount of memory we
        // request for the Gradle build.
        state.put(WizardConstants.IS_LOW_MEMORY_KEY, SystemInfo.is32Bit);

        try {
            state.put(DEBUG_KEYSTORE_SHA_1_KEY,
                    KeystoreUtils.sha1(KeystoreUtils.getOrCreateDefaultDebugKeystore()));
        } catch (Exception exception) {
            LOG.warn("Could not create debug keystore", exception);
            state.put(DEBUG_KEYSTORE_SHA_1_KEY, "");
        }

        String mavenUrl = System.getProperty(TemplateWizard.MAVEN_URL_PROPERTY);

        if (mavenUrl != null) {
            state.put(MAVEN_URL_KEY, mavenUrl);
        }

        AndroidSdkData data = AndroidSdks.getInstance().tryToChooseAndroidSdk();

        if (data != null) {
            File sdkLocation = data.getLocation();
            state.put(SDK_DIR_KEY, sdkLocation.getPath());

            String espressoVersion = RepositoryUrlManager.get().getLibraryRevision(
                    SupportLibrary.ESPRESSO_CORE.getGroupId(), SupportLibrary.ESPRESSO_CORE.getArtifactId(), null,
                    false, sdkLocation, FileOpUtils.create());

            if (espressoVersion != null) {
                state.put(ESPRESSO_VERSION_KEY, espressoVersion);
            }
        }
    }

    @Override
    protected String getWizardActionDescription() {
        return String.format("Create %1$s", getState().get(APPLICATION_NAME_KEY));
    }

    @Override
    public void performFinishingActions() {
        ApplicationManager.getApplication().invokeLater(this::runFinish);
    }

    private void runFinish() {
        if (ApplicationManager.getApplication().isUnitTestMode()) {
            return;
        }
        GradleProjectImporter projectImporter = GradleProjectImporter.getInstance();
        String rootPath = getState().get(PROJECT_LOCATION_KEY);
        if (rootPath == null) {
            LOG.error("No root path specified for project");
            return;
        }
        File rootLocation = new File(rootPath);
        File wrapperPropertiesFilePath = GradleWrapper.getDefaultPropertiesFilePath(rootLocation);
        try {
            GradleWrapper.get(wrapperPropertiesFilePath).updateDistributionUrl(SdkConstants.GRADLE_LATEST_VERSION);
        } catch (IOException e) {
            // Unlikely to happen. Continue with import, the worst-case scenario is that sync fails and the error message has a "quick fix".
            LOG.warn("Failed to update Gradle wrapper file", e);
        }

        String projectName = getState().get(APPLICATION_NAME_KEY);
        if (projectName == null) {
            projectName = "Unnamed Project";
        }

        // Pick the highest language level of all the modules/form factors.
        // We have to pick the language level up front while creating the project rather than
        // just reacting to it during sync, because otherwise the user gets prompted with
        // a changing-language-level-requires-reopening modal dialog box and have to reload
        // the project
        LanguageLevel initialLanguageLevel = null;
        for (FormFactor factor : FormFactor.values()) {
            Object version = getState().get(FormFactorUtils.getLanguageLevelKey(factor));
            if (version != null) {
                LanguageLevel level = LanguageLevel.parse(version.toString());
                if (level != null && (initialLanguageLevel == null || level.isAtLeast(initialLanguageLevel))) {
                    initialLanguageLevel = level;
                }
            }
        }

        // This is required for Android plugin in IDEA
        if (!IdeInfo.getInstance().isAndroidStudio()) {
            final Sdk jdk = IdeSdks.getInstance().getJdk();
            if (jdk != null) {
                ApplicationManager.getApplication()
                        .runWriteAction(() -> ProjectRootManager.getInstance(myProject).setProjectSdk(jdk));
            }
        }
        try {
            GradleSyncListener listener = new PostStartupGradleSyncListener(() -> {
                Iterable<File> targetFiles = myState.get(TARGET_FILES_KEY);
                assert targetFiles != null;

                TemplateUtils.reformatAndRearrange(myProject, targetFiles);

                Collection<File> filesToOpen = myState.get(FILES_TO_OPEN_KEY);
                assert filesToOpen != null;

                TemplateUtils.openEditors(myProject, filesToOpen, true);
            });

            GradleProjectImporter.Request request = new GradleProjectImporter.Request();
            request.setLanguageLevel(initialLanguageLevel).setProject(myProject);
            projectImporter.importProject(projectName, rootLocation, request, listener);
        } catch (IOException | ConfigurationException e) {
            Messages.showErrorDialog(e.getMessage(), ERROR_MSG_TITLE);
            LOG.error(e);
        }
    }

    @NotNull
    @Override
    protected String getProgressTitle() {
        return "Creating project...";
    }

    @Override
    public void doFinishAction() {
        if (!checkFinish())
            return;

        final String projectLocation = myState.get(PROJECT_LOCATION_KEY);
        assert projectLocation != null;

        boolean couldEnsureLocationExists = WriteCommandAction.runWriteCommandAction(getProject(),
                new Computable<Boolean>() {
                    @Override
                    public Boolean compute() {
                        // We generally assume that the path has passed a fair amount of prevalidation checks
                        // at the project configuration step before. Write permissions check can be tricky though in some cases,
                        // e.g., consider an unmounted device in the middle of wizard execution or changed permissions.
                        // Anyway, it seems better to check that we were really able to create the target location and are able to
                        // write to it right here when the wizard is about to close, than running into some internal IDE errors
                        // caused by these problems downstream
                        // Note: this change was originally caused by http://b.android.com/219851, but then
                        // during further discussions that a more important bug was in path validation in the old wizards,
                        // where File.canWrite() always returned true as opposed to the correct Files.isWritable(), which is
                        // already used in new wizard's PathValidator.
                        // So the change below is therefore a more narrow case than initially supposed (however it still needs to be handled)
                        try {
                            if (VfsUtil.createDirectoryIfMissing(projectLocation) != null
                                    && FileOpUtils.create().canWrite(new File(projectLocation))) {
                                return true;
                            }
                        } catch (Exception e) {
                            LOG.error(String.format("Exception thrown when creating target project location: %1$s",
                                    projectLocation), e);
                        }
                        return false;
                    }
                });
        if (!couldEnsureLocationExists) {
            Messages.showErrorDialog(
                    String.format(
                            "Could not ensure the target project location exists and is accessible:\n\n%1$s\n\n"
                                    + "Please try to specify another path.",
                            projectLocation),
                    "Error Creating Project");
            navigateToNamedStep(ConfigureAndroidProjectStep.STEP_NAME, true);
            myHost.shakeWindow();
            return;
        }

        // Create project in the dispatch thread. (super.doFinishAction also calls doFinish, but in other thread.)
        try {
            doFinish();
        } catch (IOException e) {
            LOG.error(e);
        }
    }

    @Override
    protected void doFinish() throws IOException {
        final String projectLocation = myState.get(PROJECT_LOCATION_KEY);
        final String name = myState.get(APPLICATION_NAME_KEY);
        assert projectLocation != null && name != null;

        myProject = UIUtil
                .invokeAndWaitIfNeeded(() -> ProjectManager.getInstance().createProject(name, projectLocation));
        super.doFinish();
    }

    @Nullable
    @Override
    public Project getProject() {
        return myProject;
    }
}