| /* |
| * 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.templates; |
| |
| import com.android.sdklib.repository.FullRevision; |
| import com.android.tools.idea.actions.NewAndroidComponentAction; |
| import com.android.utils.XmlUtils; |
| import com.google.common.base.Charsets; |
| import com.google.common.collect.*; |
| import com.google.common.io.Files; |
| import com.intellij.ide.actions.NonEmptyActionGroup; |
| import com.intellij.ide.IdeView; |
| import com.intellij.openapi.actionSystem.*; |
| import com.intellij.openapi.application.PathManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.vfs.LocalFileSystem; |
| import com.intellij.openapi.vfs.VfsUtilCore; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.platform.templates.github.ZipUtil; |
| import icons.AndroidIcons; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.android.sdk.AndroidSdkData; |
| import org.jetbrains.android.sdk.AndroidSdkUtils; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.w3c.dom.Document; |
| |
| import java.io.File; |
| import java.io.FileFilter; |
| import java.io.IOException; |
| import java.util.*; |
| |
| import static com.android.SdkConstants.*; |
| import static com.android.tools.idea.templates.Template.TEMPLATE_XML_NAME; |
| import static com.android.tools.idea.templates.TemplateUtils.listFiles; |
| |
| /** |
| * Handles locating templates and providing template metadata |
| */ |
| public class TemplateManager { |
| private static final Logger LOG = Logger.getInstance("#" + TemplateManager.class.getName()); |
| |
| /** |
| * A directory relative to application home folder where we can find an extra template folder. This lets us ship more up-to-date |
| * templates with the application instead of waiting for SDK updates. |
| */ |
| private static final String BUNDLED_TEMPLATE_PATH = "/plugins/android/lib/templates"; |
| private static final String[] DEVELOPMENT_TEMPLATE_PATHS = {"/../../tools/base/templates", "/android/tools-base/templates", "/community/android/tools-base/templates"}; |
| private static final String EXPLODED_AAR_PATH = "build/intermediates/exploded-aar"; |
| |
| public static final String CATEGORY_OTHER = "Other"; |
| private static final String ACTION_ID_PREFIX = "template.create."; |
| private static final boolean USE_SDK_TEMPLATES = false; |
| private static final Set<String> EXCLUDED_CATEGORIES = ImmutableSet.of("Application", "Applications"); |
| public static final Set<String> EXCLUDED_TEMPLATES = ImmutableSet.of("Empty Activity"); |
| private static final String TEMPLATE_ZIP_NAME = "templates.zip"; |
| |
| /** |
| * Cache for {@link #getTemplate(File)} |
| */ |
| private Map<File, TemplateMetadata> myTemplateMap; |
| |
| /** Table mapping (Category, Template Name) -> Template File */ |
| private Table<String, String, File> myCategoryTable; |
| |
| /** |
| * Cache location for templates pulled from exploded-aars |
| */ |
| private File myAarCache; |
| |
| private static TemplateManager ourInstance = new TemplateManager(); |
| private DefaultActionGroup myTopGroup; |
| |
| private TemplateManager() { |
| } |
| |
| public static TemplateManager getInstance() { |
| return ourInstance; |
| } |
| |
| /** |
| * @return the root folder containing templates |
| */ |
| @Nullable |
| public static File getTemplateRootFolder() { |
| String homePath = FileUtil.toSystemIndependentName(PathManager.getHomePath()); |
| // Release build? |
| VirtualFile root = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(homePath + BUNDLED_TEMPLATE_PATH)); |
| if (root == null) { |
| // Development build? |
| for (String path : DEVELOPMENT_TEMPLATE_PATHS) { |
| root = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(homePath + path)); |
| |
| if (root != null) { |
| break; |
| } |
| } |
| } |
| if (root != null) { |
| File rootFile = VfsUtilCore.virtualToIoFile(root); |
| if (templateRootIsValid(rootFile)) { |
| return rootFile; |
| } |
| } |
| |
| // Fall back to SDK template root |
| AndroidSdkData sdkData = AndroidSdkUtils.tryToChooseAndroidSdk(); |
| if (sdkData != null) { |
| File location = sdkData.getLocation(); |
| File folder = new File(location, FD_TOOLS + File.separator + FD_TEMPLATES); |
| if (folder.isDirectory()) { |
| return folder; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * @return A list of root folders containing extra templates |
| */ |
| @NotNull |
| public static List<File> getExtraTemplateRootFolders() { |
| List<File> folders = new ArrayList<File>(); |
| |
| // Check in various locations in the SDK |
| AndroidSdkData sdkData = AndroidSdkUtils.tryToChooseAndroidSdk(); |
| if (sdkData != null) { |
| File location = sdkData.getLocation(); |
| |
| if (USE_SDK_TEMPLATES) { |
| // Look in SDK/tools/templates |
| File toolsTemplatesFolder = new File(location, FileUtil.join(FD_TOOLS, FD_TEMPLATES)); |
| if (toolsTemplatesFolder.isDirectory()) { |
| File[] templateRoots = toolsTemplatesFolder.listFiles(new FileFilter() { |
| @Override |
| public boolean accept(File pathname) { |
| return pathname.isDirectory(); |
| } |
| }); |
| if (templateRoots != null) { |
| Collections.addAll(folders, templateRoots); |
| } |
| } |
| } |
| |
| // Look in SDK/extras/* |
| File extras = new File(location, FD_EXTRAS); |
| if (extras.isDirectory()) { |
| for (File vendor : listFiles(extras)) { |
| if (!vendor.isDirectory()) { |
| continue; |
| } |
| for (File pkg : listFiles(vendor)) { |
| if (pkg.isDirectory()) { |
| File folder = new File(pkg, FD_TEMPLATES); |
| if (folder.isDirectory()) { |
| folders.add(folder); |
| } |
| } |
| } |
| } |
| |
| // Legacy |
| File folder = new File(extras, FD_TEMPLATES); |
| if (folder.isDirectory()) { |
| folders.add(folder); |
| } |
| } |
| |
| // Look in SDK/add-ons |
| File addOns = new File(location, FD_ADDONS); |
| if (addOns.isDirectory()) { |
| for (File addOn : listFiles(addOns)) { |
| if (!addOn.isDirectory()) { |
| continue; |
| } |
| File folder = new File(addOn, FD_TEMPLATES); |
| if (folder.isDirectory()) { |
| folders.add(folder); |
| } |
| } |
| } |
| } |
| |
| // Look for source tree files |
| String homePath = FileUtil.toSystemIndependentName(PathManager.getHomePath()); |
| // Release build? |
| VirtualFile root = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(homePath + BUNDLED_TEMPLATE_PATH)); |
| if (root == null) { |
| // Development build? |
| for (String path : DEVELOPMENT_TEMPLATE_PATHS) { |
| root = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(homePath + path)); |
| |
| if (root != null) { |
| break; |
| } |
| } |
| } |
| |
| if (root == null) { |
| // error message tailored for release build file layout |
| LOG.error("Templates not found in: " + homePath + BUNDLED_TEMPLATE_PATH + |
| " or " + homePath + Arrays.toString(DEVELOPMENT_TEMPLATE_PATHS)); |
| } else { |
| File templateDir = new File(root.getCanonicalPath()).getAbsoluteFile(); |
| if (templateDir.isDirectory()) { |
| folders.add(templateDir); |
| } |
| } |
| return folders; |
| } |
| |
| /** |
| * Returns all the templates with the given prefix |
| * |
| * @param folder the folder prefix |
| * @return the available templates |
| */ |
| @NotNull |
| public List<File> getTemplates(@NotNull String folder) { |
| List<File> templates = new ArrayList<File>(); |
| Map<String, File> templateNames = Maps.newHashMap(); |
| File root = getTemplateRootFolder(); |
| if (root != null) { |
| File[] files = new File(root, folder).listFiles(); |
| if (files != null) { |
| for (File file : files) { |
| if (file.isDirectory() && (new File(file, TEMPLATE_XML_NAME)).exists()) { // Avoid .DS_Store etc, & non Freemarker templates |
| templates.add(file); |
| templateNames.put(file.getName(), file); |
| } |
| } |
| } |
| } |
| |
| // Add in templates from extras/ as well. |
| for (File extra : getExtraTemplateRootFolders()) { |
| for (File file : listFiles(new File(extra, folder))) { |
| if (file.isDirectory() && (new File(file, TEMPLATE_XML_NAME)).exists()) { |
| File replaces = templateNames.get(file.getName()); |
| if (replaces != null) { |
| int compare = compareTemplates(replaces, file); |
| if (compare > 0) { |
| int index = templates.indexOf(replaces); |
| if (index != -1) { |
| templates.set(index, file); |
| } |
| else { |
| templates.add(file); |
| } |
| } |
| } |
| else { |
| templates.add(file); |
| } |
| } |
| } |
| } |
| |
| // Sort by file name (not path as is File's default) |
| if (templates.size() > 1) { |
| Collections.sort(templates, new Comparator<File>() { |
| @Override |
| public int compare(File file1, File file2) { |
| return file1.getName().compareTo(file2.getName()); |
| } |
| }); |
| } |
| |
| return templates; |
| } |
| |
| @NotNull |
| public static List<File> getTemplatesFromDirectory(@NotNull File externalDirectory, boolean recursive) { |
| List<File> templates = Lists.newArrayList(); |
| if (new File(externalDirectory, TEMPLATE_XML_NAME).exists()) { |
| templates.add(externalDirectory); |
| } |
| if (recursive) { |
| File[] files = externalDirectory.listFiles(); |
| if (files != null) { |
| for (File file : files) { |
| if (file.isDirectory()) { |
| templates.addAll(getTemplatesFromDirectory(file, true)); |
| } |
| } |
| } |
| } |
| return templates; |
| } |
| |
| @NotNull |
| public List<File> getTemplateDirectoriesFromAars(@Nullable Project project) { |
| List<File> templateDirectories = Lists.newArrayList(); |
| if (project != null && project.getBaseDir() != null) { |
| if (myAarCache == null) { |
| try { |
| myAarCache = FileUtil.createTempDirectory(project.getName(), "aar_cache"); |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| return templateDirectories; |
| } |
| } |
| File aarRoot = new File(project.getBaseDir().getPath(), FileUtil.toSystemDependentName(EXPLODED_AAR_PATH)); |
| if (aarRoot.isDirectory()) { |
| for (File artifactPackage : listFiles(aarRoot)) { |
| if (artifactPackage.isDirectory() && !artifactPackage.isHidden()) { |
| for (File artifactName : listFiles(artifactPackage)) { |
| if (artifactName.isDirectory() && !artifactName.isHidden()) { |
| templateDirectories.addAll(getHighestVersionedTemplateRoot(artifactName)); |
| } |
| } |
| } |
| } |
| } |
| } |
| return templateDirectories; |
| } |
| |
| @NotNull |
| private List<File> getHighestVersionedTemplateRoot(@NotNull File artifactNameRoot) { |
| List<File> templateDirectories = Lists.newArrayList(); |
| File highestVersionDir = null; |
| FullRevision highestVersionNumber = null; |
| for (File versionDir : listFiles(artifactNameRoot)) { |
| if (!versionDir.isDirectory() || versionDir.isHidden()) { |
| continue; |
| } |
| // Find the highest version of this AAR |
| FullRevision revision; |
| try { |
| revision = FullRevision.parseRevision(versionDir.getName()); |
| } catch (NumberFormatException e) { |
| // Revision was not parse-able, consider it to be the lowest version revision |
| revision = FullRevision.NOT_SPECIFIED; |
| } |
| if (highestVersionNumber == null || revision.compareTo(highestVersionNumber) > 0) { |
| highestVersionNumber = revision; |
| highestVersionDir = versionDir; |
| } |
| } |
| if (highestVersionDir != null) { |
| String name = artifactNameRoot.getName() + "-" + highestVersionNumber.toString(); |
| File inflated = new File(myAarCache, name); |
| if (!inflated.isDirectory()) { |
| // Only unzip once |
| File zipFile = new File(highestVersionDir, TEMPLATE_ZIP_NAME); |
| if (zipFile.isFile()) { |
| try { |
| ZipUtil.unzip(null, inflated, zipFile, null, null, true); |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| } |
| } |
| if (inflated.isDirectory()) { |
| templateDirectories.add(inflated); |
| } |
| } |
| return templateDirectories; |
| } |
| |
| /** |
| * @return a list of template files that declare the given category. |
| */ |
| @NotNull |
| public List<File> getTemplatesInCategory(@NotNull String category) { |
| if (getCategoryTable().containsRow(category)) { |
| return Lists.newArrayList(getCategoryTable().row(category).values()); |
| } else { |
| return Lists.newArrayList(); |
| } |
| } |
| |
| @Nullable |
| public ActionGroup getTemplateCreationMenu(@Nullable Project project) { |
| refreshDynamicTemplateMenu(project); |
| return myTopGroup; |
| } |
| |
| public void refreshDynamicTemplateMenu(@Nullable Project project) { |
| if (myTopGroup == null) { |
| myTopGroup = new DefaultActionGroup("AndroidTemplateGroup", false); |
| } else { |
| myTopGroup.removeAll(); |
| } |
| myTopGroup.addSeparator(); |
| ActionManager am = ActionManager.getInstance(); |
| for (final String category : getCategoryTable(true, project).rowKeySet()) { |
| if (EXCLUDED_CATEGORIES.contains(category)) { |
| continue; |
| } |
| // Create the menu group item |
| NonEmptyActionGroup categoryGroup = new NonEmptyActionGroup() { |
| @Override |
| public void update(AnActionEvent e) { |
| IdeView view = LangDataKeys.IDE_VIEW.getData(e.getDataContext()); |
| final Module module = LangDataKeys.MODULE.getData(e.getDataContext()); |
| final AndroidFacet facet = module != null ? AndroidFacet.getInstance(module) : null; |
| Presentation presentation = e.getPresentation(); |
| boolean isProjectReady = facet != null && facet.getAndroidModel() != null; |
| presentation.setText(category + (isProjectReady ? "" : " (Project not ready)")); |
| presentation.setVisible(getChildrenCount() > 0 && view != null && facet != null && facet.requiresAndroidModel()); |
| } |
| }; |
| categoryGroup.setPopup(true); |
| Presentation presentation = categoryGroup.getTemplatePresentation(); |
| presentation.setIcon(AndroidIcons.Android); |
| presentation.setText(category); |
| |
| Map<String, File> categoryRow = myCategoryTable.row(category); |
| for (String templateName : categoryRow.keySet()) { |
| if (EXCLUDED_TEMPLATES.contains(templateName)) { |
| continue; |
| } |
| TemplateMetadata metadata = getTemplate(myCategoryTable.get(category, templateName)); |
| NewAndroidComponentAction templateAction = new NewAndroidComponentAction(category, templateName, metadata); |
| String actionId = ACTION_ID_PREFIX + category + templateName; |
| am.unregisterAction(actionId); |
| am.registerAction(actionId, templateAction); |
| categoryGroup.add(templateAction); |
| } |
| myTopGroup.add(categoryGroup); |
| } |
| } |
| |
| private Table<String, String, File> getCategoryTable() { |
| return getCategoryTable(false, null); |
| } |
| |
| private Table<String, String, File> getCategoryTable(boolean forceReload, @Nullable Project project) { |
| if (myCategoryTable== null || forceReload) { |
| if (myTemplateMap != null) { |
| myTemplateMap.clear(); |
| } |
| myCategoryTable = TreeBasedTable.create(); |
| for (File categoryDirectory : listFiles(getTemplateRootFolder())) { |
| for (File newTemplate : listFiles(categoryDirectory)) { |
| addTemplateToTable(newTemplate); |
| } |
| } |
| |
| for (File rootDirectory : getExtraTemplateRootFolders()) { |
| for (File categoryDirectory : listFiles(rootDirectory)) { |
| for (File newTemplate : listFiles(categoryDirectory)) { |
| addTemplateToTable(newTemplate); |
| } |
| } |
| } |
| |
| for (File aarDirectory : getTemplateDirectoriesFromAars(project)) { |
| for (File newTemplate : listFiles(aarDirectory)) { |
| addTemplateToTable(newTemplate); |
| } |
| } |
| } |
| |
| return myCategoryTable; |
| } |
| |
| private void addTemplateToTable(@NotNull File newTemplate) { |
| TemplateMetadata newMetadata = getTemplate(newTemplate); |
| if (newMetadata != null) { |
| String title = newMetadata.getTitle(); |
| if (title == null || (newMetadata.getCategory() == null && |
| myCategoryTable.columnKeySet().contains(title) && |
| myCategoryTable.get(CATEGORY_OTHER, title) == null)) { |
| // If this template is uncategorized, and we already have a template of this name that has a category, |
| // that is NOT "Other," then ignore this new template since it's undoubtedly older. |
| return; |
| } |
| String category = newMetadata.getCategory() != null ? newMetadata.getCategory() : CATEGORY_OTHER; |
| File existingTemplate = myCategoryTable.get(category, title); |
| if (existingTemplate == null || compareTemplates(existingTemplate, newTemplate) > 0) { |
| myCategoryTable.put(category, title, newTemplate); |
| } |
| } |
| } |
| |
| /** |
| * Compare two files, and return the one with the HIGHEST revision, and if |
| * the same, most recently modified |
| */ |
| private int compareTemplates(@NotNull File file1, @NotNull File file2) { |
| TemplateMetadata template1 = getTemplate(file1); |
| TemplateMetadata template2 = getTemplate(file2); |
| |
| if (template1 == null) { |
| return 1; |
| } |
| else if (template2 == null) { |
| return -1; |
| } |
| else { |
| int delta = template2.getRevision() - template1.getRevision(); |
| if (delta == 0) { |
| delta = (int)(file2.lastModified() - file1.lastModified()); |
| } |
| return delta; |
| } |
| } |
| |
| @Nullable |
| public File getTemplateFile(@Nullable String category, @Nullable String templateName) { |
| return getCategoryTable().get(category, templateName); |
| } |
| |
| @Nullable |
| public TemplateMetadata getTemplate(@Nullable String category, @Nullable String templateName) { |
| File templateDir = getTemplateFile(category, templateName); |
| return templateDir != null ? getTemplate(templateDir) : null; |
| } |
| |
| @Nullable |
| public TemplateMetadata getTemplate(@NotNull File templateDir) { |
| if (myTemplateMap != null) { |
| TemplateMetadata metadata = myTemplateMap.get(templateDir); |
| if (metadata != null) { |
| return metadata; |
| } |
| } |
| else { |
| myTemplateMap = Maps.newHashMap(); |
| } |
| |
| try { |
| File templateFile = new File(templateDir, TEMPLATE_XML_NAME); |
| if (templateFile.isFile()) { |
| String xml = Files.toString(templateFile, Charsets.UTF_8); |
| Document doc = XmlUtils.parseDocumentSilently(xml, true); |
| if (doc != null && doc.getDocumentElement() != null) { |
| TemplateMetadata metadata = new TemplateMetadata(doc); |
| myTemplateMap.put(templateDir, metadata); |
| return metadata; |
| } |
| } |
| } |
| catch (IOException e) { |
| LOG.warn(e); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Do a sanity check to see if we have templates that look compatible, otherwise we get really strange problems. The existence |
| * of a gradle wrapper in the templates directory is a good sign. |
| * @return whether the templates pass the check or not |
| */ |
| public static boolean templatesAreValid() { |
| try { |
| File templateRootFolder = getTemplateRootFolder(); |
| if (templateRootFolder == null) { |
| return false; |
| } |
| return templateRootIsValid(templateRootFolder); |
| } |
| catch (Exception e) { |
| return false; |
| } |
| } |
| |
| public static File getWrapperLocation(@NotNull File templateRootFolder) { |
| return new File(templateRootFolder, FD_GRADLE_WRAPPER); |
| |
| } |
| |
| public static boolean templateRootIsValid(@NotNull File templateRootFolder) { |
| return new File(getWrapperLocation(templateRootFolder), FN_GRADLE_WRAPPER_UNIX).exists(); |
| } |
| } |