| /* |
| * Copyright (C) 2019 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.templates |
| |
| import com.android.AndroidProjectTypes.PROJECT_TYPE_DYNAMIC_FEATURE |
| import com.android.SdkConstants.FD_TEST |
| import com.android.SdkConstants.FD_UNIT_TEST |
| import com.android.sdklib.AndroidTargetHash |
| import com.android.sdklib.AndroidVersion |
| import com.android.sdklib.AndroidVersion.VersionCodes.P |
| import com.android.sdklib.SdkVersionInfo.HIGHEST_KNOWN_STABLE_API |
| import com.android.sdklib.SdkVersionInfo.LOWEST_ACTIVE_API |
| import com.android.tools.idea.configurations.ConfigurationManager |
| import com.android.tools.idea.gradle.npw.project.GradleAndroidModuleTemplate |
| import com.android.tools.idea.gradle.util.DynamicAppUtils |
| import com.android.tools.idea.gradle.util.GradleUtil |
| import com.android.tools.idea.model.AndroidModuleInfo |
| import com.android.tools.idea.model.MergedManifestManager |
| import com.android.tools.idea.npw.ThemeHelper |
| import com.android.tools.idea.npw.platform.AndroidVersionsInfo |
| import com.android.tools.idea.projectsystem.AndroidModulePaths |
| import com.android.tools.idea.util.toIoFile |
| import com.android.tools.idea.wizard.template.ApiTemplateData |
| import com.android.tools.idea.wizard.template.ApiVersion |
| import com.android.tools.idea.wizard.template.BaseFeature |
| import com.android.tools.idea.wizard.template.FormFactor |
| import com.android.tools.idea.wizard.template.Language |
| import com.android.tools.idea.wizard.template.ModuleTemplateData |
| import com.android.tools.idea.wizard.template.PackageName |
| import com.android.tools.idea.wizard.template.ThemeData |
| import com.android.tools.idea.wizard.template.ThemesData |
| import com.intellij.openapi.application.ApplicationManager |
| import com.intellij.openapi.module.Module |
| import com.intellij.openapi.project.Project |
| import com.intellij.openapi.project.guessProjectDir |
| import com.intellij.openapi.util.io.FileUtil |
| import com.intellij.openapi.vfs.VfsUtilCore |
| import org.jetbrains.android.facet.AndroidFacet |
| import org.jetbrains.android.facet.AndroidRootUtil |
| import org.jetbrains.android.facet.SourceProviderManager |
| import org.jetbrains.android.refactoring.isAndroidx |
| import org.jetbrains.android.sdk.AndroidPlatform |
| import java.io.File |
| |
| /** |
| * Helper method for converting two paths relative to one another into a String path, since this |
| * ends up being a common pattern when creating values to put into our template's data model. |
| * |
| * Note: Use [FileUtil.getRelativePath] (String, String, Char) instead of [FileUtil.getRelativePath] (File, File), because the second |
| * version will use [File.getParent] if base directory is not yet created (when adding a new module, the directory is created later) |
| */ |
| private fun getRelativePath(base: File, file: File): String? = |
| FileUtil.getRelativePath(FileUtil.toSystemIndependentName(base.path), FileUtil.toSystemIndependentName(file.path), '/') |
| |
| /** |
| * Builder for [ModuleTemplateData]. |
| * |
| * Extracts information from various data sources. |
| */ |
| class ModuleTemplateDataBuilder(val projectTemplateDataBuilder: ProjectTemplateDataBuilder, val isNew: Boolean) { |
| var srcDir: File? = null |
| var resDir: File? = null |
| var manifestDir: File? = null |
| var testDir: File? = null |
| var unitTestDir: File? = null |
| var aidlDir: File? = null |
| var rootDir: File? = null |
| var hasApplicationTheme: Boolean = true |
| var name: String? = null |
| var isLibrary: Boolean? = null |
| var packageName: PackageName? = null |
| var formFactor: FormFactor? = null |
| var themesData: ThemesData? = null |
| var baseFeature: BaseFeature? = null |
| var apis: ApiTemplateData? = null |
| |
| /** |
| * Adds common module roots template values like [rootDir], [srcDir], etc |
| * @param paths Project paths |
| * @param packageName Package Name for the module |
| */ |
| fun setModuleRoots(paths: AndroidModulePaths, projectPath: String, moduleName: String, packageName: String) { |
| val moduleRoot = paths.moduleRoot!! |
| |
| // Register the resource directories associated with the active source provider |
| rootDir = File(FileUtil.toSystemIndependentName(moduleRoot.absolutePath)) |
| srcDir = paths.getSrcDirectory(packageName) |
| testDir = paths.getTestDirectory(packageName) |
| unitTestDir = paths.getUnitTestDirectory(packageName) |
| resDir = paths.resDirectories.firstOrNull() |
| manifestDir = paths.manifestDirectory |
| aidlDir = paths.getAidlDirectory(packageName) |
| |
| projectTemplateDataBuilder.topOut = File(projectPath) |
| name = moduleName.trimStart(':') // The templates already add an initial ":" |
| this.packageName = packageName |
| } |
| |
| /** |
| * Sets information available in the facet. |
| * |
| * Used when the Module exists. |
| */ |
| fun setFacet(facet: AndroidFacet) { |
| projectTemplateDataBuilder.setEssentials(facet.module.project) |
| |
| val target = AndroidPlatform.getInstance(facet.module)?.target |
| val moduleInfo = AndroidModuleInfo.getInstance(facet) |
| val targetSdkVersion = moduleInfo.targetSdkVersion |
| val buildSdkVersion = moduleInfo.buildSdkVersion ?: targetSdkVersion |
| val minSdkVersion = moduleInfo.minSdkVersion |
| |
| apis = ApiTemplateData( |
| buildApi = ApiVersion(buildSdkVersion.featureLevel, buildSdkVersion.apiString), |
| targetApi = ApiVersion(targetSdkVersion.featureLevel, targetSdkVersion.apiString), |
| minApi = ApiVersion(minSdkVersion.featureLevel, minSdkVersion.apiString), |
| // The highest supported/recommended appCompact version is P(28) |
| appCompatVersion = targetSdkVersion.featureLevel.coerceAtMost(P), |
| // Note: target is null for a non-preview release, see VersionItem.getAndroidTarget() |
| buildApiRevision = if (target?.version?.isPreview == true) target.revision else 0 |
| ) |
| |
| isLibrary = facet.configuration.isLibraryProject |
| |
| val appTheme = MergedManifestManager.getMergedManifestSupplier(facet.module).now |
| hasApplicationTheme = appTheme != null |
| |
| setApplicationTheme(facet) |
| |
| if (facet.configuration.projectType == PROJECT_TYPE_DYNAMIC_FEATURE) { |
| val baseFeature = DynamicAppUtils.getBaseFeature(facet.module) |
| ?: throw RuntimeException("Dynamic Feature Module '${facet.module.name}' has no Base Module") |
| |
| setBaseFeature(baseFeature) |
| } |
| } |
| |
| /** |
| * Adds information about base feature. |
| * |
| * Used only by dynamic modules. |
| */ |
| fun setBaseFeature(baseFeature: Module) { |
| val androidFacet = AndroidFacet.getInstance(baseFeature)!! |
| val mainSourceProvider = SourceProviderManager.getInstance(androidFacet).mainIdeaSourceProvider |
| val baseModuleResourceRootPath = mainSourceProvider.resDirectories.firstOrNull()?.path |
| ?: VfsUtilCore.urlToPath(mainSourceProvider.resDirectoryUrls.first()) |
| |
| this.baseFeature = BaseFeature( |
| GradleUtil.getGradlePath(baseFeature).orEmpty(), |
| AndroidRootUtil.findModuleRootFolderPath(baseFeature)!!, |
| File(baseModuleResourceRootPath) // Put the new resources in any of the available res directories |
| ) |
| } |
| |
| /** |
| * Same as [setFacet], but uses a [AndroidVersionsInfo.VersionItem]. |
| * This version is used when the Module is not created yet. |
| * |
| * @param buildVersion Build version information for the new Module being created. |
| * @param project Used to find the Gradle Dependencies versions. |
| */ |
| fun setBuildVersion(buildVersion: AndroidVersionsInfo.VersionItem, project: Project) { |
| projectTemplateDataBuilder.setBuildVersion(buildVersion, project) |
| themesData = ThemesData(appName = getAppNameForTheme(project.name)) // New modules always have a theme (unless its a library, but it will have no activity) |
| |
| apis = ApiTemplateData( |
| buildApi = ApiVersion(buildVersion.buildApiLevel, buildVersion.buildApiLevelStr), |
| targetApi = ApiVersion(buildVersion.targetApiLevel, buildVersion.targetApiLevelStr), |
| minApi = ApiVersion(buildVersion.minApiLevel, buildVersion.minApiLevelStr), |
| // The highest supported/recommended appCompact version is P(28) |
| appCompatVersion = buildVersion.buildApiLevel.coerceAtMost(P), |
| // Note: target is null for a non-preview release, see VersionItem.getAndroidTarget() |
| buildApiRevision = buildVersion.androidTarget?.revision ?: 0 |
| ) |
| } |
| |
| /** |
| * Adds information about application theme. |
| */ |
| private fun setApplicationTheme(facet: AndroidFacet) { |
| val module = facet.module |
| val projectFile = module.project.projectFile ?: return |
| val helper = ThemeHelper(module) |
| val themeName = helper.appThemeName ?: return |
| val configuration = ConfigurationManager.getOrCreateInstance(module).getConfiguration(projectFile) |
| |
| fun getDerivedTheme(themeName: String, derivedThemeName: String, useBaseThemeAsDerivedTheme: Boolean): ThemeData { |
| val fullThemeName = if (useBaseThemeAsDerivedTheme) themeName else "$themeName.$derivedThemeName" |
| val exists = ThemeHelper.themeExists(configuration, fullThemeName) |
| if (!exists && !helper.isLocalTheme(themeName)) { |
| return ThemeData(derivedThemeName, helper.isLocalTheme(derivedThemeName)) |
| } |
| |
| return ThemeData(fullThemeName, exists) |
| } |
| |
| val noActionBar = "NoActionBar" |
| val appBarOverlay = "AppBarOverlay" |
| val popupOverlay = "PopupOverlay" |
| ApplicationManager.getApplication().runReadAction { |
| val hasActionBar = ThemeHelper.hasActionBar(configuration, themeName) |
| themesData = ThemesData( |
| appName = getAppNameForTheme(module.project.name), |
| main = ThemeData(themeName, true), |
| noActionBar = getDerivedTheme(themeName, noActionBar, hasActionBar == false), |
| appBarOverlay = getDerivedTheme(themeName, appBarOverlay, false), |
| popupOverlay = getDerivedTheme(themeName, popupOverlay, false) |
| ) |
| } |
| } |
| |
| fun build() = ModuleTemplateData( |
| projectTemplateDataBuilder.build(), |
| srcDir!!, |
| resDir!!, |
| manifestDir!!, |
| testDir ?: srcDir!!.resolve(FD_TEST), |
| unitTestDir ?: srcDir!!.resolve(FD_UNIT_TEST), |
| aidlDir!!, |
| rootDir!!, |
| isNew, |
| hasApplicationTheme, |
| name!!, |
| isLibrary!!, |
| packageName!!, |
| formFactor!!, |
| themesData ?: ThemesData(appName = getAppNameForTheme(projectTemplateDataBuilder.applicationName!!)), |
| baseFeature, |
| apis!! |
| ) |
| } |
| |
| /** |
| * Computes a suitable build api string, e.g. for API level 18 the build API string is "18". |
| */ |
| fun AndroidVersion.toApiString(): String = |
| if (isPreview) AndroidTargetHash.getPlatformHashString(this) else apiString |
| |
| // Note: New projects are always created with androidx dependencies |
| fun Project?.hasAndroidxSupport(isNewProject: Boolean) = this == null || isNewProject || this.isAndroidx() |
| |
| fun getDummyModuleTemplateDataBuilder(project: Project): ModuleTemplateDataBuilder { |
| val projectStateBuilder = ProjectTemplateDataBuilder(true).apply { |
| androidXSupport = true |
| setProjectDefaults(project) |
| language = Language.Java |
| topOut = project.guessProjectDir()!!.toIoFile() |
| debugKeyStoreSha1 = KeystoreUtils.getSha1DebugKeystoreSilently(null) |
| applicationPackage = "" |
| overridePathCheck = false |
| } |
| |
| return ModuleTemplateDataBuilder(projectStateBuilder, true).apply { |
| name = "Fake module state" |
| packageName = "" |
| val paths = GradleAndroidModuleTemplate.createDefaultTemplateAt(project.basePath!!, name!!).paths |
| setModuleRoots(paths, projectTemplateDataBuilder.topOut!!.path, name!!, packageName!!) |
| isLibrary = false |
| formFactor = FormFactor.Mobile |
| themesData = ThemesData(appName = getAppNameForTheme(project.name)) |
| apis = ApiTemplateData( |
| buildApi = ApiVersion(HIGHEST_KNOWN_STABLE_API, HIGHEST_KNOWN_STABLE_API.toString()), |
| targetApi = ApiVersion(HIGHEST_KNOWN_STABLE_API, HIGHEST_KNOWN_STABLE_API.toString()), |
| minApi = ApiVersion(LOWEST_ACTIVE_API, LOWEST_ACTIVE_API.toString()), |
| // The highest supported/recommended appCompact version is P(28) |
| appCompatVersion = HIGHEST_KNOWN_STABLE_API.coerceAtMost(P), |
| buildApiRevision = null |
| ) |
| } |
| } |
| |