/* * 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; import com.android.builder.model.*; import com.android.sdklib.AndroidVersion; import com.android.tools.lint.detector.api.LintUtils; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.java.LanguageLevel; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.Serializable; import java.util.*; import static com.android.builder.model.AndroidProject.FD_GENERATED; import static com.android.builder.model.AndroidProject.FD_INTERMEDIATES; import static com.android.tools.idea.gradle.customizer.android.ContentRootModuleCustomizer.EXCLUDED_OUTPUT_FOLDER_NAMES; /** * Contains Android-Gradle related state necessary for configuring an IDEA project based on a user-selected build variant. */ public class IdeaAndroidProject implements Serializable { @NotNull private final String myModuleName; @NotNull private final VirtualFile myRootDir; @NotNull private final AndroidProject myDelegate; @NotNull private String mySelectedVariantName; @Nullable private Boolean myOverridesManifestPackage; @Nullable private AndroidVersion myMinSdkVersion; @NotNull private Map<String, BuildTypeContainer> myBuildTypesByName = Maps.newHashMap(); @NotNull private Map<String, ProductFlavorContainer> myProductFlavorsByName = Maps.newHashMap(); @NotNull private Map<String, Variant> myVariantsByName = Maps.newHashMap(); @NotNull private Set<File> myExtraGeneratedSourceFolders = Sets.newHashSet(); /** * Creates a new {@link IdeaAndroidProject}. * * @param moduleName the name of the IDEA module, created from {@code delegate}. * @param rootDir the root directory of the imported Android-Gradle project. * @param delegate imported Android-Gradle project. * @param selectedVariantName name of the selected build variant. */ public IdeaAndroidProject(@NotNull String moduleName, @NotNull File rootDir, @NotNull AndroidProject delegate, @NotNull String selectedVariantName) { myModuleName = moduleName; VirtualFile found = VfsUtil.findFileByIoFile(rootDir, true); // the module's root directory can never be null. assert found != null; myRootDir = found; myDelegate = delegate; populateBuildTypesByName(); populateProductFlavorsByName(); populateVariantsByName(); setSelectedVariantName(selectedVariantName); } private void populateBuildTypesByName() { for (BuildTypeContainer container : myDelegate.getBuildTypes()) { String name = container.getBuildType().getName(); myBuildTypesByName.put(name, container); } } private void populateProductFlavorsByName() { for (ProductFlavorContainer container : myDelegate.getProductFlavors()) { String name = container.getProductFlavor().getName(); myProductFlavorsByName.put(name, container); } } private void populateVariantsByName() { for (Variant variant : myDelegate.getVariants()) { myVariantsByName.put(variant.getName(), variant); } } @Nullable public BuildTypeContainer findBuildType(@NotNull String name) { return myBuildTypesByName.get(name); } @NotNull public Set<String> getBuildTypes() { return myBuildTypesByName.keySet(); } @NotNull public Set<String> getProductFlavors() { return myProductFlavorsByName.keySet(); } @Nullable public ProductFlavorContainer findProductFlavor(@NotNull String name) { return myProductFlavorsByName.get(name); } @Nullable public AndroidArtifact findInstrumentationTestArtifactInSelectedVariant() { Variant variant = getSelectedVariant(); return findInstrumentationTestArtifact(variant); } @Nullable public static AndroidArtifact findInstrumentationTestArtifact(@NotNull Variant variant) { Collection<AndroidArtifact> extraAndroidArtifacts = variant.getExtraAndroidArtifacts(); for (AndroidArtifact extraArtifact : extraAndroidArtifacts) { if (extraArtifact.getName().equals(AndroidProject.ARTIFACT_ANDROID_TEST)) { return extraArtifact; } } return null; } @NotNull public String getModuleName() { return myModuleName; } /** * @return the root directory of the imported Android-Gradle project. The returned path belongs to the IDEA module containing the * build.gradle file. */ @NotNull public VirtualFile getRootDir() { return myRootDir; } /** * @return the imported Android-Gradle project. */ @NotNull public AndroidProject getDelegate() { return myDelegate; } /** * @return the selected build variant. */ @NotNull public Variant getSelectedVariant() { Variant selected = myVariantsByName.get(mySelectedVariantName); assert selected != null; return selected; } /** * Updates the name of the selected build variant. If the given name does not belong to an existing variant, this method will pick up * the first variant, in alphabetical order. * * @param name the new name. */ public void setSelectedVariantName(@NotNull String name) { Collection<String> variantNames = getVariantNames(); String newVariantName; if (variantNames.contains(name)) { newVariantName = name; } else { List<String> sorted = Lists.newArrayList(variantNames); Collections.sort(sorted); // AndroidProject has always at least 2 variants (debug and release.) newVariantName = sorted.get(0); } mySelectedVariantName = newVariantName; // force lazy recompute myOverridesManifestPackage = null; myMinSdkVersion = null; } @NotNull public Collection<String> getVariantNames() { return myVariantsByName.keySet(); } @NotNull public Collection<String> getBuildTypeNames() { return myBuildTypesByName.keySet(); } @NotNull public Collection<String> getProductFlavorNames() { return myProductFlavorsByName.keySet(); } @Nullable public LanguageLevel getJavaLanguageLevel() { JavaCompileOptions compileOptions = myDelegate.getJavaCompileOptions(); String sourceCompatibility = compileOptions.getSourceCompatibility(); return LanguageLevel.parse(sourceCompatibility); } /** * Returns the package name used for the current variant in the given project. */ @NotNull public String computePackageName() { return getSelectedVariant().getMainArtifact().getApplicationId(); } public boolean isLibrary() { return getDelegate().isLibrary(); } /** * Returns whether this project fully overrides the manifest package (with applicationId in the * default config or one of the product flavors) in the current variant. * * @return true if the manifest package is overridden */ public boolean overridesManifestPackage() { if (myOverridesManifestPackage == null) { myOverridesManifestPackage = getDelegate().getDefaultConfig().getProductFlavor().getApplicationId() != null; Variant variant = getSelectedVariant(); List<String> flavors = variant.getProductFlavors(); for (String flavor : flavors) { ProductFlavorContainer productFlavor = findProductFlavor(flavor); assert productFlavor != null; if (productFlavor.getProductFlavor().getApplicationId() != null) { myOverridesManifestPackage = true; break; } } // The build type can specify a suffix, but it will be merged with the manifest // value if not specified in a flavor/default config, so only flavors count } return myOverridesManifestPackage.booleanValue(); } private static final AndroidVersion NOT_SPECIFIED = new AndroidVersion(0, null); /** * Returns the {@code }minSdkVersion} specified by the user (in the default config or product flavors). * This is normally the merged value, but for example when using preview platforms, the Gradle plugin * will set minSdkVersion and targetSdkVersion to match the level of the compileSdkVersion; in this case * we want tools like lint's API check to continue to look for the intended minSdkVersion specified in * the build.gradle file * * @return the {@link AndroidVersion} to use for this Gradle project, or null if not specified */ @Nullable public AndroidVersion getConfigMinSdkVersion() { if (myMinSdkVersion == null) { ApiVersion minSdkVersion = getSelectedVariant().getMergedFlavor().getMinSdkVersion(); if (minSdkVersion != null && minSdkVersion.getCodename() != null) { ApiVersion defaultConfigVersion = getDelegate().getDefaultConfig().getProductFlavor().getMinSdkVersion(); if (defaultConfigVersion != null) { minSdkVersion = defaultConfigVersion; } List<String> flavors = getSelectedVariant().getProductFlavors(); for (String flavor : flavors) { ProductFlavorContainer productFlavor = findProductFlavor(flavor); assert productFlavor != null; ApiVersion flavorVersion = productFlavor.getProductFlavor().getMinSdkVersion(); if (flavorVersion != null) { minSdkVersion = flavorVersion; break; } } } if (minSdkVersion != null) { myMinSdkVersion = LintUtils.convertVersion(minSdkVersion, null); } else { myMinSdkVersion = NOT_SPECIFIED; } } return myMinSdkVersion != NOT_SPECIFIED ? myMinSdkVersion : null; } /** * Registers the path of a source folder that has been incorrectly generated outside of the default location (${buildDir}/generated.) * * @param folderPath the path of the generated source folder. */ public void registerExtraGeneratedSourceFolder(@NotNull File folderPath) { myExtraGeneratedSourceFolders.add(folderPath); } /** * Indicates whether the given path should be manually excluded in the IDE, to minimize file indexing. * <p> * This method returns {@code false} if: * <ul> * <li>the given path does not belong to a folder</li> * <li>the path belongs to the "generated sources" root folder (${buildDir}/generated)</li> * <li>the path belongs to the standard output folders (${buildDir}/intermediates and ${buildDir}/outputs)</li> * <li>or if the path belongs to a generated source folder that has been placed at the wrong location (e.g. by a 3rd-party Gradle * plug-in)</li> * </ul> * </p> * * @param path the given path * @return {@code true} if the path should be manually excluded in the IDE, {@code false otherwise}. */ public boolean shouldManuallyExclude(@NotNull File path) { if (!path.isDirectory()) { return false; } String name = path.getName(); if (FD_INTERMEDIATES.equals(name) || EXCLUDED_OUTPUT_FOLDER_NAMES.contains(name)) { // already excluded. return false; } boolean hasGeneratedFolders = FD_GENERATED.equals(name) || containsExtraGeneratedSourceFolder(path); return !hasGeneratedFolders; } private boolean containsExtraGeneratedSourceFolder(@NotNull File folderPath) { if (!folderPath.isDirectory()) { return false; } for (File generatedSourceFolder : myExtraGeneratedSourceFolders) { if (FileUtil.isAncestor(folderPath, generatedSourceFolder, false)) { return true; } } return false; } /** * @return the paths of generated sources placed at the wrong location (not in ${build}/generated.) */ @NotNull public File[] getExtraGeneratedSourceFolders() { return myExtraGeneratedSourceFolders.toArray(new File[myExtraGeneratedSourceFolders.size()]); } }