Java tutorial
/* * Copyright (C) 2016 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.lint; import com.android.ide.common.resources.ResourceUrl; import com.android.resources.ResourceType; import com.android.tools.idea.res.AppResourceRepository; import com.android.tools.idea.templates.TemplateUtils; import com.android.tools.lint.detector.api.ResourceEvaluator; import com.google.common.collect.Lists; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.impl.source.xml.XmlTagImpl; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlTagValue; import com.intellij.refactoring.psi.SearchUtils; import com.intellij.util.containers.SmartHashSet; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.inspections.lint.AndroidLintQuickFix; import org.jetbrains.android.inspections.lint.AndroidQuickfixContexts; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; import static com.android.SdkConstants.*; import static org.jetbrains.android.util.AndroidUtils.createChildDirectoryIfNotExist; /** * Quickfix for generating a backup descriptor. * <ul> * <li>Scan the project for all databases in use.</li> * <li>Scan the project for all sharedpreferences in use. </li> * <li>Generate a descriptor, exclude the databases and sharedpreferences.</li> * <li>Reformat and open the generated descriptor.</li> * </ul> */ class GenerateBackupDescriptorFix implements AndroidLintQuickFix { private static final String XML_CONTENT_START = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<full-backup-content>\n"; private static final String XML_CONTENT_END = "</full-backup-content>\n"; private final ResourceUrl myUrl; public GenerateBackupDescriptorFix(@NotNull ResourceUrl url) { myUrl = url; } @Override public void apply(@NotNull final PsiElement startElement, @NotNull PsiElement endElement, @NotNull AndroidQuickfixContexts.Context context) { final Project project = startElement.getProject(); final AndroidFacet facet = AndroidFacet.getInstance(startElement); if (facet == null) { return; } // Find all classes that extend the SQLiteOpenHelper GlobalSearchScope allScope = GlobalSearchScope.allScope(project); GlobalSearchScope useScope = GlobalSearchScope.projectScope(project); // All necessary PsiClassType's PsiClassType stringType = PsiType.getJavaLangString(PsiManager.getInstance(project), allScope); JavaPsiFacade javaPsiFacade = JavaPsiFacade.getInstance(project); PsiClass psiOpenHelperClass = javaPsiFacade.findClass("android.database.sqlite.SQLiteOpenHelper", allScope); assert psiOpenHelperClass != null; PsiClass psiContext = javaPsiFacade.findClass(CLASS_CONTEXT, allScope); assert psiContext != null; final Set<String> databaseNames = findDatabasesInProject(useScope, psiOpenHelperClass, stringType, javaPsiFacade); final Set<String> sharedPreferenceFiles = findSharedPrefsInProject(useScope, psiContext, facet, stringType, javaPsiFacade); WriteCommandAction.runWriteCommandAction(project, "Create Backup Descriptor", null, () -> { try { @SuppressWarnings("deprecation") VirtualFile primaryResourceDir = facet.getPrimaryResourceDir(); assert primaryResourceDir != null; VirtualFile xmlDir = createChildDirectoryIfNotExist(project, primaryResourceDir, FD_RES_XML); VirtualFile resFile = xmlDir.createChildData(project, myUrl.name + DOT_XML); VfsUtil.saveText(resFile, generateBackupDescriptorContents(databaseNames, sharedPreferenceFiles)); TemplateUtils.reformatAndRearrange(project, resFile); TemplateUtils.openEditor(project, resFile); TemplateUtils.selectEditor(project, resFile); } catch (IOException e) { String error = String.format("Failed to create file: %1$s", e.getMessage()); Messages.showErrorDialog(project, error, "Create Backup Resource"); } }); } @Override public boolean isApplicable(@NotNull PsiElement startElement, @NotNull PsiElement endElement, @NotNull AndroidQuickfixContexts.ContextType contextType) { AndroidFacet facet = AndroidFacet.getInstance(startElement); AppResourceRepository appResources = facet == null ? null : facet.getAppResources(true); return appResources == null || !appResources.getItemsOfType(ResourceType.XML).contains(myUrl.name); } @NotNull @Override public String getName() { return "Generate full-backup-content descriptor"; } private static Set<String> findSharedPrefsInProject(GlobalSearchScope useScope, PsiClass psiContext, @NotNull AndroidFacet facet, @NotNull final PsiClassType stringType, @NotNull JavaPsiFacade psiFacade) { final Set<String> prefFiles = new SmartHashSet<>(); // Note: To find the usages of a given method, we need to use the following: // 1. First find all the methods that override the given method. // 2. Search of usages of the given method and all the overriding methods. PsiMethod[] methods = psiContext.findMethodsByName("getSharedPreferences", true); List<PsiMethod> allMethods = new ArrayList<>(Arrays.asList(methods)); // Find all overriding methods of getSharedPreferences(..) for (PsiMethod method : methods) { allMethods.addAll(Lists.newArrayList(SearchUtils.findOverridingMethods(method))); } for (final PsiMethod method : allMethods) { Iterable<PsiReference> references = SearchUtils.findAllReferences(method, useScope); for (final PsiReference ref : references) { ref.getElement().getParent().accept(new JavaRecursiveElementWalkingVisitor() { @Override public void visitMethodCallExpression(PsiMethodCallExpression expression) { PsiMethod psiMethod = expression.resolveMethod(); String methodName = psiMethod == null ? null : psiMethod.getName(); // Processing an inner call to getString(R.string.$name$) // For example getSharedPreferences(getString(R.string.pref_name), mode) if (GET_STRING_METHOD.equals(methodName)) { PsiExpression[] expressions = expression.getArgumentList().getExpressions(); if (expressions.length == 1 && PsiType.INT.equals(expressions[0].getType())) { // Use a ResourceEvaluator to find the resource type/name. This has the // advantage that it can also resolve complex expressions used as the // getString argument. ResourceUrl resource = ResourceEvaluator.getResource( new LintIdeJavaParser.LintPsiJavaEvaluator(expression.getProject()), expressions[0]); if (resource == null || resource.framework || resource.type != ResourceType.STRING) { return; } List<PsiElement> resources = facet.getLocalResourceManager() .findResourcesByFieldName(ResourceType.STRING.getName(), resource.name); for (PsiElement resElement : resources) { if (resElement instanceof XmlAttributeValue) { // get the parent XmlTag and drill down to it's text. XmlTagValue value = ((XmlTagImpl) resElement.getParent().getParent()) .getValue(); prefFiles.add(value.getText()); break; } } } } else if (method.getName().equals(methodName)) { // Look for getSharedPreferences(String name, int mode) on the Context object PsiExpression[] expressions = expression.getArgumentList().getExpressions(); if (expressions.length == 2 && stringType.equals(expressions[0].getType())) { Object result = psiFacade.getConstantEvaluationHelper() .computeConstantExpression(expressions[0]); if (result != null) { prefFiles.add((String) result); } else { // let it run through in case it contains a call to getString super.visitMethodCallExpression(expression); } } } } }); } } return prefFiles; } @NotNull private static Set<String> findDatabasesInProject(GlobalSearchScope useScope, final PsiClass psiClass, final PsiClassType stringType, final JavaPsiFacade psiFacade) { PsiMethod[] constructors = psiClass.getConstructors(); final Set<String> databaseNames = new SmartHashSet<>(); for (final PsiMethod method : constructors) { Iterable<PsiReference> references = SearchUtils.findAllReferences(method, useScope); for (final PsiReference ref : references) { final PsiElement element = ref.getElement(); element.getParent().accept(new JavaRecursiveElementWalkingVisitor() { @Override public void visitMethodCallExpression(PsiMethodCallExpression expression) { PsiMethod method = expression.resolveMethod(); if (method != null && method.getContainingClass() != null && method.getContainingClass().isEquivalentTo(psiClass)) { PsiExpression[] expressions = expression.getArgumentList().getExpressions(); if (expressions.length > 2 && stringType.equals(expressions[1].getType())) { // 2nd parameter of one of the following constructors: // 1. SQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) // 2. SQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, // DatabaseErrorHandler errorHandler) PsiExpression expressionToEvaluate = expressions[1]; Object result = psiFacade.getConstantEvaluationHelper() .computeConstantExpression(expressionToEvaluate); if (result != null) { databaseNames.add((String) result); } } } super.visitMethodCallExpression(expression); } }); } } return databaseNames; } // TODO/consider: The error message from ManifestDetector.ALLOW_BACKUP already // contains an indication that the AndroidManifest.xml has a GCM receiver. // This could've been used to add a very specific comment in the descriptor // saying that the GCM regId should be excluded *but* relying on text of the message // seems a bit brittle (given that it can be localized). // Another way to address this would be to have a specific Lint Issue so that // this information is passed from the lint check to the IDE. // Yet another way would be to re-parse the AndroidManifest.xml to see if there is // a GCM receiver. (Since this is just for adding a comment, I've left that out) private static String generateBackupDescriptorContents(Set<String> databaseNames, Set<String> sharedPrefs) { StringBuilder sb = new StringBuilder(); sb.append(XML_CONTENT_START); if (!databaseNames.isEmpty() || !sharedPrefs.isEmpty()) { sb.append( "<!-- TODO Remove the following \"exclude\" elements to make them a part of the auto backup -->\n"); } // Databases for (String name : databaseNames) { sb.append(String.format("<exclude domain=\"database\" path=\"%1$s\"/>\n", name)); } // shared preferences if (!sharedPrefs.isEmpty()) { sb.append("<!-- Exclude the shared preferences file that contains the GCM registrationId -->\n"); } else { sb.append("<!-- Exclude specific shared preferences that contain GCM registration Id -->\n"); } for (String name : sharedPrefs) { sb.append(String.format("<exclude domain=\"sharedpref\" path=\"%1$s.xml\" />\n", name)); } sb.append(XML_CONTENT_END); return sb.toString(); } }