com.android.tools.idea.gradle.util.GradleUtil.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.gradle.util.GradleUtil.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.gradle.util;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.builder.model.*;
import com.android.ide.common.repository.GradleVersion;
import com.android.tools.idea.IdeInfo;
import com.android.tools.idea.gradle.dsl.model.GradleBuildModel;
import com.android.tools.idea.gradle.dsl.model.android.AndroidModel;
import com.android.tools.idea.gradle.project.AndroidGradleNotification;
import com.android.tools.idea.gradle.project.facet.gradle.GradleFacet;
import com.android.tools.idea.gradle.project.model.AndroidModuleModel;
import com.android.tools.idea.gradle.project.model.NdkModuleModel;
import com.android.tools.idea.project.AndroidProjectInfo;
import com.android.tools.idea.sdk.IdeSdks;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.impl.ApplicationImpl;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.ProjectSystemId;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import icons.AndroidIcons;
import org.gradle.tooling.internal.consumer.DefaultGradleConnector;
import org.gradle.tooling.model.UnsupportedMethodException;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings;
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings;
import org.jetbrains.plugins.gradle.settings.GradleSettings;
import org.jetbrains.plugins.gradle.util.GradleConstants;

import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.util.*;

import static com.android.SdkConstants.*;
import static com.android.builder.model.AndroidProject.PROJECT_TYPE_APP;
import static com.android.builder.model.AndroidProject.PROJECT_TYPE_INSTANTAPP;
import static com.android.tools.idea.gradle.project.model.AndroidModuleModel.getTestArtifacts;
import static com.android.tools.idea.gradle.util.BuildMode.ASSEMBLE_TRANSLATE;
import static com.android.tools.idea.gradle.util.GradleBuilds.ENABLE_TRANSLATION_JVM_ARG;
import static com.android.tools.idea.gradle.util.Projects.getBaseDirPath;
import static com.android.tools.idea.startup.GradleSpecificInitializer.GRADLE_DAEMON_TIMEOUT_MS;
import static com.google.common.base.Splitter.on;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.intellij.notification.NotificationType.ERROR;
import static com.intellij.notification.NotificationType.WARNING;
import static com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction;
import static com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil.getExecutionSettings;
import static com.intellij.openapi.ui.Messages.getQuestionIcon;
import static com.intellij.openapi.util.io.FileUtil.filesEqual;
import static com.intellij.openapi.util.io.FileUtil.join;
import static com.intellij.openapi.util.text.StringUtil.isEmptyOrSpaces;
import static com.intellij.openapi.util.text.StringUtil.isNotEmpty;
import static com.intellij.openapi.vfs.VfsUtil.findFileByIoFile;
import static com.intellij.openapi.vfs.VfsUtilCore.virtualToIoFile;
import static com.intellij.util.ArrayUtil.toStringArray;
import static com.intellij.util.SystemProperties.getUserHome;
import static com.intellij.util.containers.ContainerUtil.getFirstItem;
import static com.intellij.util.ui.UIUtil.invokeAndWaitIfNeeded;
import static org.gradle.wrapper.WrapperExecutor.DISTRIBUTION_URL_PROPERTY;
import static org.jetbrains.plugins.gradle.settings.DistributionType.LOCAL;

/**
 * Utilities related to Gradle.
 */
public final class GradleUtil {
    public static final ProjectSystemId GRADLE_SYSTEM_ID = GradleConstants.SYSTEM_ID;

    @NonNls
    public static final String BUILD_DIR_DEFAULT_NAME = "build";
    @NonNls
    public static final String GRADLEW_PROPERTIES_PATH = join(FD_GRADLE_WRAPPER, FN_GRADLE_WRAPPER_PROPERTIES);

    private static final Logger LOG = Logger.getInstance(GradleUtil.class);

    /**
     * Finds characters that shouldn't be used in the Gradle path.
     * <p/>
     * I was unable to find any specification for Gradle paths. In my experiments, Gradle only failed with slashes. This list may grow if
     * we find any other unsupported characters.
     */
    private static final CharMatcher ILLEGAL_GRADLE_PATH_CHARS_MATCHER = CharMatcher.anyOf("\\/");

    private GradleUtil() {
    }

    @NotNull
    public static Collection<File> getGeneratedSourceFolders(@NotNull BaseArtifact artifact) {
        try {
            Collection<File> folders = artifact.getGeneratedSourceFolders();
            // JavaArtifactImpl#getGeneratedSourceFolders returns null even though BaseArtifact#getGeneratedSourceFolders is marked as @NonNull.
            // See https://code.google.com/p/android/issues/detail?id=216236
            //noinspection ConstantConditions
            return folders != null ? folders : Collections.emptyList();
        } catch (UnsupportedMethodException e) {
            // Model older than 1.2.
        }
        return Collections.emptyList();
    }

    @NotNull
    public static Dependencies getDependencies(@NotNull BaseArtifact artifact,
            @Nullable GradleVersion modelVersion) {
        return artifact.getDependencies();
    }

    public static boolean androidModelSupportsInstantApps(@NotNull GradleVersion modelVersion) {
        return modelVersion.compareIgnoringQualifiers("2.3.0") >= 0;
    }

    public static void clearStoredGradleJvmArgs(@NotNull Project project) {
        GradleSettings settings = GradleSettings.getInstance(project);
        String existingJvmArgs = settings.getGradleVmOptions();
        settings.setGradleVmOptions(null);
        if (!isEmptyOrSpaces(existingJvmArgs)) {
            invokeAndWaitIfNeeded((Runnable) () -> {
                String jvmArgs = existingJvmArgs.trim();
                String msg = String.format(
                        "Starting with version 1.3, Android Studio no longer supports IDE-specific Gradle JVM arguments.\n\n"
                                + "Android Studio will now remove any stored Gradle JVM arguments.\n\n"
                                + "Would you like to copy these JVM arguments:\n%1$s\n"
                                + "to the project's gradle.properties file?\n\n"
                                + "(Any existing JVM arguments in the gradle.properties file will be overwritten.)",
                        jvmArgs);

                int result = Messages.showYesNoDialog(project, msg, "Gradle Settings", getQuestionIcon());
                if (result == Messages.YES) {
                    try {
                        GradleProperties gradleProperties = new GradleProperties(project);
                        gradleProperties.setJvmArgs(jvmArgs);
                        gradleProperties.save();
                    } catch (IOException e) {
                        String err = String.format(
                                "Failed to copy JVM arguments '%1$s' to the project's gradle.properties file.",
                                existingJvmArgs);
                        LOG.info(err, e);

                        String cause = e.getMessage();
                        if (isNotEmpty(cause)) {
                            err += String.format("<br>\nCause: %1$s", cause);
                        }

                        AndroidGradleNotification.getInstance(project).showBalloon("Gradle Settings", err, ERROR);
                    }
                } else {
                    String text = String.format(
                            "JVM arguments<br>\n'%1$s'<br>\nwere not copied to the project's gradle.properties file.",
                            existingJvmArgs);
                    AndroidGradleNotification.getInstance(project).showBalloon("Gradle Settings", text, WARNING);
                }
            });
        }
    }

    public static boolean isSupportedGradleVersion(@NotNull GradleVersion gradleVersion) {
        GradleVersion supported = GradleVersion.parse(GRADLE_MINIMUM_VERSION);
        return supported.compareTo(gradleVersion) <= 0;
    }

    /**
     * This is temporary, until the model returns more outputs per artifact.
     * Deprecating since the model 0.13 provides multiple outputs per artifact if split apks are enabled.
     */
    @Deprecated
    @NotNull
    public static AndroidArtifactOutput getOutput(@NotNull AndroidArtifact artifact) {
        Collection<AndroidArtifactOutput> outputs = artifact.getOutputs();
        assert !outputs.isEmpty();
        AndroidArtifactOutput output = getFirstItem(outputs);
        assert output != null;
        return output;
    }

    @NotNull
    public static Icon getModuleIcon(@NotNull Module module) {
        AndroidModuleModel androidModel = AndroidModuleModel.get(module);
        if (androidModel != null) {
            int projectType = androidModel.getProjectType();
            if (projectType == PROJECT_TYPE_APP || projectType == PROJECT_TYPE_INSTANTAPP) {
                return AndroidIcons.AppModule;
            }
            return AndroidIcons.LibraryModule;
        }
        return AndroidProjectInfo.getInstance(module.getProject()).requiresAndroidModel() ? AllIcons.Nodes.PpJdk
                : AllIcons.Nodes.Module;
    }

    @Nullable
    public static AndroidProject getAndroidProject(@NotNull Module module) {
        AndroidModuleModel gradleModel = AndroidModuleModel.get(module);
        return gradleModel != null ? gradleModel.getAndroidProject() : null;
    }

    @Nullable
    public static NativeAndroidProject getNativeAndroidProject(@NotNull Module module) {
        NdkModuleModel ndkModuleModel = NdkModuleModel.get(module);
        return ndkModuleModel != null ? ndkModuleModel.getAndroidProject() : null;
    }

    /**
     * Returns the Gradle "logical" path (using colons as separators) if the given module represents a Gradle project or sub-project.
     *
     * @param module the given module.
     * @return the Gradle path for the given module, or {@code null} if the module does not represent a Gradle project or sub-project.
     */
    @Nullable
    public static String getGradlePath(@NotNull Module module) {
        GradleFacet facet = GradleFacet.getInstance(module);
        return facet != null ? facet.getConfiguration().GRADLE_PROJECT_PATH : null;
    }

    /**
     * Returns whether the given module is the module corresponding to the project root (i.e. gradle path of ":") and has no source roots.
     * <p/>
     * The default Android Studio projects create an empty module at the root level. In theory, users could add sources to that module, but
     * we expect that most don't and keep that as a module simply to tie together other modules.
     */
    public static boolean isRootModuleWithNoSources(@NotNull Module module) {
        if (ModuleRootManager.getInstance(module).getSourceRoots().length == 0) {
            String gradlePath = getGradlePath(module);
            if (gradlePath == null || gradlePath.equals(":")) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the library dependencies in the given variant. This method checks dependencies in the main and test (as currently selected
     * in the UI) artifacts. The dependency lookup is not transitive (only direct dependencies are returned.)
     */
    @NotNull
    public static List<AndroidLibrary> getDirectLibraryDependencies(@NotNull Variant variant,
            @NotNull AndroidModuleModel androidModel) {
        List<AndroidLibrary> libraries = Lists.newArrayList();

        GradleVersion modelVersion = androidModel.getModelVersion();

        AndroidArtifact mainArtifact = variant.getMainArtifact();
        Dependencies dependencies = getDependencies(mainArtifact, modelVersion);
        libraries.addAll(dependencies.getLibraries());

        for (BaseArtifact testArtifact : getTestArtifacts(variant)) {
            dependencies = getDependencies(testArtifact, modelVersion);
            libraries.addAll(dependencies.getLibraries());
        }
        return libraries;
    }

    @Nullable
    public static Module findModuleByGradlePath(@NotNull Project project, @NotNull String gradlePath) {
        ModuleManager moduleManager = ModuleManager.getInstance(project);
        for (Module module : moduleManager.getModules()) {
            GradleFacet gradleFacet = GradleFacet.getInstance(module);
            if (gradleFacet != null) {
                if (gradlePath.equals(gradleFacet.getConfiguration().GRADLE_PROJECT_PATH)) {
                    return module;
                }
            }
        }
        return null;
    }

    @NotNull
    public static List<String> getPathSegments(@NotNull String gradlePath) {
        return on(GRADLE_PATH_SEPARATOR).omitEmptyStrings().splitToList(gradlePath);
    }

    /**
     * Returns the build.gradle file in the given module. This method first checks if the Gradle model has the path of the build.gradle
     * file for the given module. If it doesn't find it, it tries to find a build.gradle inside the module's root directory.
     *
     * @param module the given module.
     * @return the build.gradle file in the given module, or {@code null} if it cannot be found.
     */
    @Nullable
    public static VirtualFile getGradleBuildFile(@NotNull Module module) {
        GradleFacet gradleFacet = GradleFacet.getInstance(module);
        if (gradleFacet != null && gradleFacet.getGradleModuleModel() != null) {
            return gradleFacet.getGradleModuleModel().getBuildFile();
        }
        // At the time we're called, module.getModuleFile() may be null, but getModuleFilePath returns the path where it will be created.
        File moduleFilePath = new File(module.getModuleFilePath());
        File parentFile = moduleFilePath.getParentFile();
        return parentFile != null ? getGradleBuildFile(parentFile) : null;
    }

    /**
     * Returns the build.gradle file that is expected right in the directory at the given path. For example, if the directory path is
     * '~/myProject/myModule', this method will look for the file '~/myProject/myModule/build.gradle'.
     * <p>
     * <b>Note:</b> Only use this method if you do <b>not</b> have a reference to a {@link Module}. Otherwise use
     * {@link #getGradleBuildFile(Module)}.
     * </p>
     *
     * @param dirPath the given directory path.
     * @return the build.gradle file in the directory at the given path, or {@code null} if there is no build.gradle file in the given
     * directory path.
     */
    @Nullable
    public static VirtualFile getGradleBuildFile(@NotNull File dirPath) {
        File gradleBuildFilePath = getGradleBuildFilePath(dirPath);
        return findFileByIoFile(gradleBuildFilePath, true);
    }

    /**
     * Returns the path of a build.gradle file in the directory at the given path. For example, if the directory path is
     * '~/myProject/myModule', this method will return the path '~/myProject/myModule/build.gradle'. Please note that a build.gradle file
     * may not exist at the returned path.
     * <p>
     * <b>Note:</b> Only use this method if you do <b>not</b> have a reference to a {@link Module}. Otherwise use
     * {@link #getGradleBuildFile(Module)}.
     * </p>
     *
     * @param dirPath the given directory path.
     * @return the path of a build.gradle file in the directory at the given path.
     */
    @NotNull
    public static File getGradleBuildFilePath(@NotNull File dirPath) {
        return new File(dirPath, FN_BUILD_GRADLE);
    }

    @Nullable
    public static VirtualFile getGradleSettingsFile(@NotNull File dirPath) {
        File gradleSettingsFilePath = getGradleSettingsFilePath(dirPath);
        return findFileByIoFile(gradleSettingsFilePath, true);
    }

    @NotNull
    public static File getGradleSettingsFilePath(@NotNull File dirPath) {
        return new File(dirPath, FN_SETTINGS_GRADLE);
    }

    @Nullable
    public static GradleExecutionSettings getOrCreateGradleExecutionSettings(@NotNull Project project,
            boolean useEmbeddedGradle) {
        GradleExecutionSettings executionSettings = getGradleExecutionSettings(project);
        if (IdeInfo.getInstance().isAndroidStudio() && useEmbeddedGradle) {
            if (executionSettings == null) {
                File gradlePath = EmbeddedDistributionPaths.getInstance().findEmbeddedGradleDistributionPath();
                assert gradlePath != null && gradlePath.isDirectory();
                executionSettings = new GradleExecutionSettings(gradlePath.getPath(), null, LOCAL, null, false);
                File jdkPath = IdeSdks.getInstance().getJdkPath();
                if (jdkPath != null) {
                    executionSettings.setJavaHome(jdkPath.getPath());
                }
            }
        }
        return executionSettings;
    }

    @Nullable
    public static GradleExecutionSettings getGradleExecutionSettings(@NotNull Project project) {
        GradleProjectSettings projectSettings = getGradleProjectSettings(project);
        if (projectSettings == null) {
            File baseDirPath = getBaseDirPath(project);
            String msg = String.format(
                    "Unable to obtain Gradle project settings for project '%1$s', located at '%2$s'",
                    project.getName(), baseDirPath.getPath());
            LOG.info(msg);
            return null;
        }

        try {
            GradleExecutionSettings settings = getExecutionSettings(project,
                    projectSettings.getExternalProjectPath(), GRADLE_SYSTEM_ID);
            if (settings != null) {
                // By setting the Gradle daemon timeout to -1, we don't allow IDEA to set it to 1 minute. Gradle daemons need to be reused as
                // much as possible. The default timeout is 3 hours.
                settings.setRemoteProcessIdleTtlInMs(GRADLE_DAEMON_TIMEOUT_MS);
            }
            return settings;
        } catch (IllegalArgumentException e) {
            LOG.info("Failed to obtain Gradle execution settings", e);
            return null;
        }
    }

    @Nullable
    private static GradleProjectSettings getGradleProjectSettings(@NotNull Project project) {
        return GradleProjectSettingsFinder.getInstance().findGradleProjectSettings(project);
    }

    @VisibleForTesting
    @Nullable
    static String getGradleInvocationJvmArg(@Nullable BuildMode buildMode) {
        if (ASSEMBLE_TRANSLATE == buildMode) {
            return AndroidGradleSettings.createJvmArg(ENABLE_TRANSLATION_JVM_ARG, true);
        }
        return null;
    }

    public static void stopAllGradleDaemonsAndRestart() {
        DefaultGradleConnector.close();
        Application application = ApplicationManager.getApplication();
        if (application instanceof ApplicationImpl) {
            ((ApplicationImpl) application).restart(true);
        } else {
            application.restart();
        }
    }

    /**
     * Converts a Gradle project name into a system dependent path relative to root project. Please note this is the default mapping from a
     * Gradle "logical" path to a physical path. Users can override this mapping in settings.gradle and this mapping may not always be
     * accurate.
     * <p/>
     * E.g. ":module" becomes "module" and ":directory:module" is converted to "directory/module"
     */
    @NotNull
    public static String getDefaultPhysicalPathFromGradlePath(@NotNull String gradlePath) {
        List<String> segments = getPathSegments(gradlePath);
        return join(toStringArray(segments));
    }

    /**
     * Obtains the default path for the module (Gradle sub-project) with the given name inside the given directory.
     */
    @NotNull
    public static File getModuleDefaultPath(@NotNull VirtualFile parentDir, @NotNull String gradlePath) {
        assert gradlePath.length() > 0;
        String relativePath = getDefaultPhysicalPathFromGradlePath(gradlePath);
        return new File(virtualToIoFile(parentDir), relativePath);
    }

    /**
     * Tests if the Gradle path is valid and return index of the offending character or -1 if none.
     */
    public static int isValidGradlePath(@NotNull String gradlePath) {
        return ILLEGAL_GRADLE_PATH_CHARS_MATCHER.indexIn(gradlePath);
    }

    /**
     * Checks if the project already has a module with given Gradle path.
     */
    public static boolean hasModule(@Nullable Project project, @NotNull String gradlePath,
            boolean checkProjectFolder) {
        if (project == null) {
            return false;
        }
        for (Module module : ModuleManager.getInstance(project).getModules()) {
            if (gradlePath.equals(getGradlePath(module))) {
                return true;
            }
        }
        if (checkProjectFolder) {
            File location = getModuleDefaultPath(project.getBaseDir(), gradlePath);
            if (location.isFile()) {
                return true;
            } else if (location.isDirectory()) {
                File[] children = location.listFiles();
                return children == null || children.length > 0;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    /**
     * Determines version of the Android gradle plugin (and model) used by the project. The result can be absent if there are no android
     * modules in the project or if the last sync has failed.
     */
    @Nullable
    public static GradleVersion getAndroidGradleModelVersionInUse(@NotNull Project project) {
        Set<String> foundInLibraries = Sets.newHashSet();
        Set<String> foundInApps = Sets.newHashSet();
        for (Module module : ModuleManager.getInstance(project).getModules()) {

            AndroidModuleModel androidModel = AndroidModuleModel.get(module);
            if (androidModel != null) {
                AndroidProject androidProject = androidModel.getAndroidProject();
                String modelVersion = androidProject.getModelVersion();
                if (androidModel.getProjectType() == PROJECT_TYPE_APP) {
                    foundInApps.add(modelVersion);
                } else {
                    foundInLibraries.add(modelVersion);
                }
            }
        }

        String found = null;

        // Prefer the version in app.
        if (foundInApps.size() == 1) {
            found = getOnlyElement(foundInApps);
        } else if (foundInApps.isEmpty() && foundInLibraries.size() == 1) {
            found = getOnlyElement(foundInLibraries);
        }

        return found != null ? GradleVersion.tryParse(found) : null;
    }

    public static void attemptToUseEmbeddedGradle(@NotNull Project project) {
        if (IdeInfo.getInstance().isAndroidStudio()) {
            GradleWrapper gradleWrapper = GradleWrapper.find(project);
            if (gradleWrapper != null) {
                String gradleVersion = null;
                try {
                    Properties properties = gradleWrapper.getProperties();
                    String url = properties.getProperty(DISTRIBUTION_URL_PROPERTY);
                    gradleVersion = getGradleWrapperVersionOnlyIfComingForGradleDotOrg(url);
                } catch (IOException e) {
                    LOG.warn("Failed to read file " + gradleWrapper.getPropertiesFilePath().getPath());
                }
                if (gradleVersion != null && isCompatibleWithEmbeddedGradleVersion(gradleVersion)
                        && !GradleLocalCache.getInstance().containsGradleWrapperVersion(gradleVersion, project)) {
                    File embeddedGradlePath = EmbeddedDistributionPaths.getInstance()
                            .findEmbeddedGradleDistributionPath();
                    if (embeddedGradlePath != null) {
                        GradleProjectSettings gradleSettings = getGradleProjectSettings(project);
                        if (gradleSettings != null) {
                            gradleSettings.setDistributionType(LOCAL);
                            gradleSettings.setGradleHome(embeddedGradlePath.getPath());
                        }
                    }
                }
            }
        }
    }

    @VisibleForTesting
    @Nullable
    static String getGradleWrapperVersionOnlyIfComingForGradleDotOrg(@Nullable String url) {
        if (url != null) {
            int foundIndex = url.indexOf("://");
            if (foundIndex != -1) {
                String protocol = url.substring(0, foundIndex);
                if (protocol.equals("http") || protocol.equals("https")) {
                    String expectedPrefix = protocol + "://services.gradle.org/distributions/gradle-";
                    if (url.startsWith(expectedPrefix)) {
                        // look for "-" before "bin" or "all"
                        foundIndex = url.indexOf('-', expectedPrefix.length());
                        if (foundIndex != -1) {
                            String version = url.substring(expectedPrefix.length(), foundIndex);
                            if (isNotEmpty(version)) {
                                return version;
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    // Currently, the latest Gradle version is 2.2.1, and we consider 2.2 and 2.2.1 as compatible.
    private static boolean isCompatibleWithEmbeddedGradleVersion(@NotNull String gradleVersion) {
        return gradleVersion.equals(GRADLE_MINIMUM_VERSION) || gradleVersion.equals(GRADLE_LATEST_VERSION);
    }

    /**
     * Returns {@code true} if the main artifact of the given Android model depends on the given artifact, which consists of a group id and an
     * artifact id, such as {@link SdkConstants#APPCOMPAT_LIB_ARTIFACT}.
     *
     * @param androidModel the Android model to check
     * @param artifact     the artifact
     * @return {@code true} if the project depends on the given artifact (including transitively)
     */
    public static boolean dependsOn(@NonNull AndroidModuleModel androidModel, @NonNull String artifact) {
        Dependencies dependencies = androidModel.getSelectedMainCompileDependencies();
        return dependsOn(dependencies, artifact);
    }

    @Nullable
    public static GradleVersion getModuleDependencyVersion(@NonNull AndroidModuleModel androidModel,
            @NonNull String artifact) {
        Dependencies dependencies = androidModel.getSelectedMainCompileDependencies();
        for (AndroidLibrary library : dependencies.getLibraries()) {
            String version = getDependencyVersion(library, artifact, true);
            if (version != null) {
                return GradleVersion.tryParse(version);
            }
        }
        return null;
    }

    /**
     * Returns {@code true} if the androidTest artifact of the given Android model depends on the given artifact, which consists of a group id
     * and an artifact id, such as {@link SdkConstants#APPCOMPAT_LIB_ARTIFACT}.
     *
     * @param androidModel the Android model to check
     * @param artifact     the artifact
     * @return {@code true} if the project depends on the given artifact (including transitively)
     */
    public static boolean dependsOnAndroidTest(@NonNull AndroidModuleModel androidModel, @NonNull String artifact) {
        Dependencies dependencies = androidModel.getSelectedAndroidTestCompileDependencies();
        if (dependencies == null) {
            return false;
        }
        return dependsOn(dependencies, artifact);
    }

    /**
     * Returns {@code true} if the given dependencies include the given artifact, which consists of a group id and an artifact id, such as
     * {@link SdkConstants#APPCOMPAT_LIB_ARTIFACT}.
     *
     * @param dependencies the Gradle dependencies object to check
     * @param artifact     the artifact
     * @return {@code true} if the dependencies include the given artifact (including transitively)
     */
    private static boolean dependsOn(@NonNull Dependencies dependencies, @NonNull String artifact) {
        for (AndroidLibrary library : dependencies.getLibraries()) {
            if (dependsOn(library, artifact, true)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns {@code true} if the given library depends on the given artifact, which consists a group id and an artifact id, such as
     * {@link SdkConstants#APPCOMPAT_LIB_ARTIFACT}.
     *
     * @param library      the Gradle library to check
     * @param artifact     the artifact
     * @param transitively if {@code false}, checks only direct dependencies, otherwise checks transitively
     * @return {@code true} if the project depends on the given artifact
     */
    public static boolean dependsOn(@NonNull AndroidLibrary library, @NonNull String artifact,
            boolean transitively) {
        return getDependencyVersion(library, artifact, transitively) != null;
    }

    private static String getDependencyVersion(@NonNull AndroidLibrary library, @NonNull String artifact,
            boolean transitively) {
        MavenCoordinates resolvedCoordinates = library.getResolvedCoordinates();
        //noinspection ConstantConditions
        if (resolvedCoordinates != null) {
            if (artifact.endsWith(resolvedCoordinates.getArtifactId()) && artifact
                    .equals(resolvedCoordinates.getGroupId() + ':' + resolvedCoordinates.getArtifactId())) {
                return resolvedCoordinates.getVersion();
            }
        }

        if (transitively) {
            for (AndroidLibrary dependency : library.getLibraryDependencies()) {
                String version = getDependencyVersion(dependency, artifact, true);
                if (version != null) {
                    return version;
                }
            }
        }
        return null;
    }

    public static boolean hasCause(@NotNull Throwable e, @NotNull Class<?> causeClass) {
        // We want to ignore class loader difference, that's why we just compare fully-qualified class names here.
        String causeClassName = causeClass.getName();
        for (Throwable ex = e; ex != null; ex = ex.getCause()) {
            if (causeClassName.equals(ex.getClass().getName())) {
                return true;
            }
        }
        return false;
    }

    @Nullable
    public static File getGradleUserSettingsFile() {
        String homePath = getUserHome();
        if (homePath == null) {
            return null;
        }
        return new File(homePath, join(DOT_GRADLE, FN_GRADLE_PROPERTIES));
    }

    public static void setBuildToolsVersion(@NotNull Project project, @NotNull String version) {
        List<GradleBuildModel> modelsToUpdate = Lists.newArrayList();

        for (Module module : ModuleManager.getInstance(project).getModules()) {
            AndroidFacet facet = AndroidFacet.getInstance(module);
            if (facet != null) {
                GradleBuildModel buildModel = GradleBuildModel.get(module);
                if (buildModel != null) {
                    AndroidModel android = buildModel.android();
                    if (android != null && !version.equals(android.buildToolsVersion().value())) {
                        android.setBuildToolsVersion(version);
                        modelsToUpdate.add(buildModel);
                    }
                }
            }
        }

        if (!modelsToUpdate.isEmpty()) {
            runWriteCommandAction(project, () -> {
                for (GradleBuildModel buildModel : modelsToUpdate) {
                    buildModel.applyChanges();
                }
            });
        }
    }

    @Nullable
    public static AndroidLibrary findLibrary(@NotNull File bundleDir, @NotNull Variant variant,
            @Nullable GradleVersion modelVersion) {
        AndroidArtifact artifact = variant.getMainArtifact();
        Dependencies dependencies = getDependencies(artifact, modelVersion);
        for (AndroidLibrary library : dependencies.getLibraries()) {
            AndroidLibrary result = findLibrary(library, bundleDir);
            if (result != null) {
                return result;
            }
        }

        return null;
    }

    @Nullable
    public static AndroidLibrary findLibrary(@NotNull AndroidLibrary library, @NotNull File bundleDir) {
        if (filesEqual(bundleDir, library.getFolder())) {
            return library;
        }

        for (AndroidLibrary dependency : library.getLibraryDependencies()) {
            AndroidLibrary result = findLibrary(dependency, bundleDir);
            if (result != null) {
                return result;
            }
        }

        return null;
    }
}