summaryrefslogtreecommitdiff
path: root/src/com/google/appindexing/api/ApiCreator.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/google/appindexing/api/ApiCreator.java')
-rw-r--r--src/com/google/appindexing/api/ApiCreator.java1070
1 files changed, 1070 insertions, 0 deletions
diff --git a/src/com/google/appindexing/api/ApiCreator.java b/src/com/google/appindexing/api/ApiCreator.java
new file mode 100644
index 0000000..ead753d
--- /dev/null
+++ b/src/com/google/appindexing/api/ApiCreator.java
@@ -0,0 +1,1070 @@
+/*
+ * Copyright (C) 2015 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.google.appindexing.api;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.builder.model.AndroidArtifact;
+import com.android.builder.model.AndroidLibrary;
+import com.android.builder.model.MavenCoordinates;
+import com.android.ide.common.repository.GradleCoordinate;
+import com.android.tools.idea.gradle.AndroidGradleModel;
+import com.android.tools.idea.gradle.dsl.dependencies.Dependencies;
+import com.android.tools.idea.gradle.dsl.dependencies.ExternalDependencySpec;
+import com.android.tools.idea.gradle.dsl.dependencies.external.ExternalDependency;
+import com.android.tools.idea.gradle.dsl.model.GradleBuildModel;
+import com.google.appindexing.util.DeepLinkUtils;
+import com.google.appindexing.util.ManifestUtils;
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import com.android.SdkConstants;
+import com.android.tools.idea.gradle.project.GradleProjectImporter;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.intellij.ide.highlighter.JavaFileType;
+import com.intellij.lang.java.JavaLanguage;
+import com.intellij.lang.xml.XMLLanguage;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtilCore;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.*;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.codeStyle.JavaCodeStyleManager;
+import com.intellij.psi.codeStyle.VariableKind;
+import com.intellij.psi.util.InheritanceUtil;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.xml.XmlComment;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.psi.xml.XmlTag;
+
+import com.siyeh.ig.psiutils.ImportUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.android.tools.idea.gradle.dsl.dependencies.CommonConfigurationNames.COMPILE;
+
+/**
+ * A class for detecting the situation where developers need App Indexing API code and inserting
+ * the API code in their projects.
+ */
+public final class ApiCreator {
+ static private final String COMPILE_GMS_GROUP = "com.google.android.gms";
+ static private final String COMPILE_APPINDEXING = "play-services-appindexing";
+ static private final String MINIMUM_VERSION = "8.1.0";
+
+ static private final String TAG_METADATA = "meta-data";
+ static private final String ATTR_NAME_METADATA_GMS = "com.google.android.gms.version";
+ static private final String ATTR_VALUE_METADATA_GMS = "@integer/google_play_services_version";
+
+ static private final String CLASS_ACTION = "Action";
+ static private final String CLASS_APP_INDEX = "AppIndex";
+ static private final String CLASS_GOOGLE_API_CLIENT = "GoogleApiClient";
+ static private final String CLASS_ACTION_FULL = "com.google.android.gms.appindexing.Action";
+ static private final String CLASS_APP_INDEX_FULL = "com.google.android.gms.appindexing.AppIndex";
+ static private final String CLASS_GOOGLE_API_CLIENT_FULL = "com.google.android.gms.common.api.GoogleApiClient";
+
+ static private final List<String> SIGNATURE_ON_CREATE = Lists.newArrayList("android.os.Bundle");
+ static private final List<String> SIGNATURE_ON_START = Lists.newArrayList();
+ static private final List<String> SIGNATURE_ON_STOP = Lists.newArrayList();
+ static private final String ON_CREATE_FORMAT = "@Override\n" +
+ "protected void onCreate(android.os.Bundle savedInstanceState) {\n" +
+ " super.onCreate(savedInstanceState);\n" +
+ " \n" +
+ " // ATTENTION: This was auto-generated to implement the App Indexing API.\n" +
+ " // See https://g.co/AppIndexing/AndroidStudio for more information.\n" +
+ " %1$s = new %2$s.Builder(this).addApi(%3$s.API).build();\n" +
+ "}";
+ static private final String ON_START_FORMAT = "@Override\n" +
+ "public void onStart(){\n" +
+ " super.onStart();\n" +
+ " \n" +
+ " // ATTENTION: This was auto-generated to implement the App Indexing API.\n" +
+ " // See https://g.co/AppIndexing/AndroidStudio for more information.\n" +
+ " %1$s.connect();\n" +
+ " %3$s\n" +
+ " %4$s.AppIndexApi.start(%1$s, %2$s);\n" +
+ "}";
+ static private final String ON_STOP_FORMAT = "@Override\n" +
+ "public void onStop() {\n" +
+ " super.onStop();\n" +
+ " \n" +
+ " // ATTENTION: This was auto-generated to implement the App Indexing API.\n" +
+ " // See https://g.co/AppIndexing/AndroidStudio for more information.\n" +
+ " %3$s\n" +
+ " %4$s.AppIndexApi.end(%1$s, %2$s);\n" +
+ " %1$s.disconnect();\n" +
+ "}";
+
+ static private final String ACTION_FORMAT = "%6$s %1$s = %6$s.newAction(\n" +
+ " %6$s.TYPE_VIEW, // TODO: choose an action type.\n" +
+ " \"%2$s Page\", // TODO: Define a title for the content shown.\n" +
+ " // TODO: If you have web page content that matches this app activity's content,\n" +
+ " // make sure this auto-generated web page URL is correct.\n" +
+ " // Otherwise, set the URL to null.\n" +
+ " android.net.Uri.parse(\"http://%4$s\"), \n" +
+ " // TODO: Make sure this auto-generated app deep link URI is correct.\n" +
+ " android.net.Uri.parse(\"android-app://%3$s/%5$s/%4$s\")\n" +
+ ");";
+ static private final String CLIENT_FORMAT = "%1$s = new %2$s.Builder(this).addApi(%3$s.API).build();";
+ static private final String BUILDER_APPINDEXAPI = ".addApi(%s.API)";
+ static private final String APP_INDEXING_START = "%3$s.AppIndexApi.start(%1$s,%2$s);";
+ static private final String APP_INDEXING_END = "%3$s.AppIndexApi.end(%1$s,%2$s);";
+ static private final String APP_INDEXING_START_HALF = "AppIndexApi.start(%1$s,";
+ static private final String APP_INDEXING_END_HALF = "AppIndexApi.end(%1$s,";
+ static private final String APP_INDEXING_VIEW = "AppIndexApi.view(%1$s,";
+ static private final String APP_INDEXING_VIEWEND = "AppIndexApi.viewEnd(%1$s,";
+
+ static private final List<String> COMMENT_BUILDER_APPINDEXAPI = Lists
+ .newArrayList("// ATTENTION: This \"addApi(AppIndex.API)\"was auto-generated to implement the App Indexing API.",
+ "// See https://g.co/AppIndexing/AndroidStudio for more information.");
+ static private final List<String> COMMENT_IN_JAVA = Lists
+ .newArrayList("// ATTENTION: This was auto-generated to implement the App Indexing API.",
+ "// See https://g.co/AppIndexing/AndroidStudio for more information.");
+ static private final String COMMENT_FOR_FIELD = "/**\n" +
+ " * ATTENTION: This %1$swas auto-generated to implement the App Indexing API.\n" +
+ " * See https://g.co/AppIndexing/AndroidStudio for more information.\n" +
+ " */";
+ static private final String COMMENT_IN_MANIFEST =
+ "<!-- ATTENTION: This was auto-generated to add Google Play services to your project for\n" +
+ " App Indexing. See https://g.co/AppIndexing/AndroidStudio for more information. -->";
+
+ private Project myProject = null;
+ private Module myModule = null;
+ private PsiFile myFile = null;
+
+ private PsiClass myActivity = null;
+
+ private PsiElementFactory myFactory = null;
+ private CodeStyleManager myCodeStyleManager = null;
+
+ private PsiCodeBlock myOnCreate = null;
+ private PsiCodeBlock myOnStart = null;
+ private PsiCodeBlock myOnStop = null;
+
+ /* The App Indexing API statements in onStart / onStop method */
+ private List<PsiStatement> myStartStatements = Lists.newArrayList();
+ private List<PsiStatement> myEndStatements = Lists.newArrayList();
+ private List<PsiStatement> myViewStatements = Lists.newArrayList();
+ private List<PsiStatement> myViewEndStatements = Lists.newArrayList();
+
+ private String myHighestGmsLibVersion = null;
+ private String myAppIndexingLibVersion = null;
+
+ private Map<String, String> myImportClasses = Maps.newHashMap();
+
+ public ApiCreator(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
+ myProject = project;
+ myFile = file;
+ myModule = ModuleUtilCore.findModuleForFile(myFile.getVirtualFile(), myProject);
+
+ if (myFile instanceof PsiJavaFile) {
+ PsiElement element = myFile.findElementAt(editor.getCaretModel().getOffset());
+ if (element != null) {
+ myActivity = getSurroundingInheritingClass(element, SdkConstants.CLASS_ACTIVITY);
+ if (myActivity != null) {
+ myOnCreate = getMethodBodyByName("onCreate", SIGNATURE_ON_CREATE, myActivity);
+ myOnStart = getMethodBodyByName("onStart", SIGNATURE_ON_START, myActivity);
+ myOnStop = getMethodBodyByName("onStop", SIGNATURE_ON_STOP, myActivity);
+ if (myOnStart != null) {
+ myStartStatements = StatementFilter.filterCodeBlock("AppIndex.AppIndexApi.start", myOnStart);
+ myViewStatements = StatementFilter.filterCodeBlock("AppIndex.AppIndexApi.view", myOnStart);
+ }
+ if (myOnStop != null) {
+ myEndStatements = StatementFilter.filterCodeBlock("AppIndex.AppIndexApi.end", myOnStop);
+ myViewEndStatements = StatementFilter.filterCodeBlock("AppIndex.AppIndexApi.viewEnd", myOnStop);
+ }
+ }
+ }
+ }
+ myFactory = (PsiElementFactory)JVMElementFactories.getFactory(JavaLanguage.INSTANCE, myProject);
+ myCodeStyleManager = CodeStyleManager.getInstance(myProject);
+ myImportClasses.put(CLASS_ACTION, CLASS_ACTION_FULL);
+ myImportClasses.put(CLASS_APP_INDEX, CLASS_APP_INDEX_FULL);
+ myImportClasses.put(CLASS_GOOGLE_API_CLIENT, CLASS_GOOGLE_API_CLIENT_FULL);
+ }
+
+ /**
+ * If the current caret's position is eligible for creating App Indexing API code,
+ * i.e. the caret is inside an 'Activity' class
+ * and the class does not call app indexing api.
+ *
+ * @return true if the current caret's position needs app indexing generating intention.
+ */
+ public static boolean eligibleForInsertingAppIndexingApiCode(@NotNull Editor editor, @NotNull PsiFile file) {
+ if (!(file instanceof PsiJavaFile)) return false;
+ PsiElement element = file.findElementAt(editor.getCaretModel().getOffset());
+ if (element == null) return false;
+ PsiClass activity = getSurroundingInheritingClass(element, SdkConstants.CLASS_ACTIVITY);
+ if (activity == null) return false;
+ PsiCodeBlock onStart = getMethodBodyByName("onStart", SIGNATURE_ON_START, activity);
+ if (onStart == null) return true;
+ PsiCodeBlock onStop = getMethodBodyByName("onStop", SIGNATURE_ON_STOP, activity);
+ if (onStop == null) return true;
+ List<PsiStatement> startStatements = StatementFilter.filterCodeBlock("AppIndexApi.start", onStart);
+ List<PsiStatement> viewStatements = StatementFilter.filterCodeBlock("AppIndexApi.view", onStart);
+ if (startStatements.isEmpty() && viewStatements.isEmpty()) return true;
+ List<PsiStatement> endStatements = StatementFilter.filterCodeBlock("AppIndexApi.end", onStop);
+ List<PsiStatement> viewEndStatements = StatementFilter.filterCodeBlock("AppIndexApi.viewEnd", onStop);
+ if (endStatements.isEmpty() && viewEndStatements.isEmpty()) return true;
+ return false;
+ }
+
+ /**
+ * Inserts the AppIndexing API code for the activity where the caret is in.
+ * Steps:
+ * 1. Generates Google Play Service Support in build.gradle and AndroidManifest.xml of current
+ * module.
+ * 2. Generates App Indexing API code in Java source file.
+ * 3. Sync the project if build.gradle of module is changed.
+ */
+ public void insertAppIndexingApiCodeForActivity() {
+ boolean needGradleSync = false;
+ try {
+ if (myModule == null || myActivity == null || myFactory == null || myCodeStyleManager == null) {
+ Logger.getInstance(ApiCreator.class).info("Unable to generate App Indexing API code.");
+ return;
+ }
+
+ getGmsDependencyVersion();
+ needGradleSync = insertGmsCompileDependencyInGradleIfNeeded();
+
+ XmlFile manifestPsiFile = ManifestUtils.getAndroidManifestPsi(myModule);
+ if (manifestPsiFile == null) {
+ Logger.getInstance(ApiCreator.class).info("AndroidManifest.xml not found.");
+ return;
+ }
+ insertGmsVersionTagInManifestIfNeeded(manifestPsiFile);
+
+ insertAppIndexingApiCodeInJavaFile(getDeepLinkOfActivity(), !needGradleSync);
+ }
+ catch (ApiCreatorException e) {
+ e.printStackTrace();
+ }
+ finally {
+ if (needGradleSync) {
+ GradleProjectImporter.getInstance().requestProjectSync(myProject, null);
+ }
+ }
+ }
+
+ private void getGmsDependencyVersion() throws ApiCreatorException {
+ AndroidGradleModel model = AndroidGradleModel.get(myModule);
+ if (model == null) {
+ throw new ApiCreatorException("AndroidGradleModel not found.");
+ }
+ AndroidArtifact artifact = model.getMainArtifact();
+ Collection<AndroidLibrary> libraries = artifact.getDependencies().getLibraries();
+ for (AndroidLibrary library : libraries) {
+ getDependencyVersionFromAndroidLibrary(library);
+ }
+ }
+
+ private void getDependencyVersionFromAndroidLibrary(AndroidLibrary library) {
+ MavenCoordinates coordinates = library.getResolvedCoordinates();
+ if (coordinates != null && coordinates.getGroupId().equals(COMPILE_GMS_GROUP)) {
+ String version = coordinates.getVersion();
+ if (coordinates.getArtifactId().equals(COMPILE_APPINDEXING)) {
+ myAppIndexingLibVersion = version;
+ }
+ if (myHighestGmsLibVersion == null || compareVersion(version, myHighestGmsLibVersion) > 0) {
+ myHighestGmsLibVersion = version;
+ }
+ }
+
+ for (AndroidLibrary dependency : library.getLibraryDependencies()) {
+ getDependencyVersionFromAndroidLibrary(dependency);
+ }
+ }
+
+ /**
+ * Generates App Indexing Support in build.gradle of app module, if no such support exists.
+ *
+ * @return true if gradle myFile is changed and needs sync.
+ */
+ @VisibleForTesting
+ boolean insertGmsCompileDependencyInGradleIfNeeded() throws ApiCreatorException {
+ GradleBuildModel buildModel = GradleBuildModel.get(myModule);
+ if (buildModel == null) {
+ throw new ApiCreatorException("Build model not found.");
+ }
+ Dependencies dependencies = buildModel.dependencies();
+ String versionToUse = MINIMUM_VERSION;
+ if (myHighestGmsLibVersion != null && compareVersion(myHighestGmsLibVersion, MINIMUM_VERSION) > 0) {
+ versionToUse = myHighestGmsLibVersion;
+ }
+
+ boolean gradleChange = false;
+ if (myAppIndexingLibVersion == null) {
+ ExternalDependencySpec newDependency = new ExternalDependencySpec(COMPILE_APPINDEXING, COMPILE_GMS_GROUP, versionToUse);
+ dependencies = buildModel.dependencies();
+ dependencies.add(COMPILE, newDependency);
+ dependencies.applyChanges();
+ gradleChange = true;
+ }
+
+ if (myHighestGmsLibVersion != null && compareVersion(myHighestGmsLibVersion, versionToUse) < 0) {
+ ImmutableList<ExternalDependency> dependencyList = dependencies.external();
+ for (ExternalDependency dependency : dependencyList) {
+ String group = dependency.group();
+ if (group != null && group.equals(COMPILE_GMS_GROUP)) {
+ dependency.version(versionToUse);
+ dependency.applyChanges();
+ gradleChange = true;
+ }
+ }
+ }
+ return gradleChange;
+ }
+
+ /**
+ * Generates App Indexing Support in AndroidManifest.xml.
+ *
+ * @param manifestPsiFile The psi file of AndroidManifest.xml.
+ */
+ @VisibleForTesting
+ void insertGmsVersionTagInManifestIfNeeded(@NotNull XmlFile manifestPsiFile) {
+ XmlTag root = manifestPsiFile.getRootTag();
+ if (root != null) {
+ List<XmlTag> applications = ManifestUtils.searchXmlTagsByName(root, SdkConstants.TAG_APPLICATION);
+ for (XmlTag application : applications) {
+ if (getGmsTag(application) == null) {
+ XmlTag gms = application.createChildTag(TAG_METADATA, null, null, false);
+ gms = application.addSubTag(gms, false);
+ gms.setAttribute(SdkConstants.ATTR_NAME, SdkConstants.ANDROID_URI, ATTR_NAME_METADATA_GMS);
+ gms.setAttribute(SdkConstants.ATTR_VALUE, SdkConstants.ANDROID_URI, ATTR_VALUE_METADATA_GMS);
+
+ XmlTag GmsTag = getGmsTag(application);
+ XmlComment comment = createXmlComment(COMMENT_IN_MANIFEST);
+ if (comment != null && GmsTag != null) {
+ application.addBefore(comment, GmsTag);
+ }
+ }
+ }
+ unlockFromPsiOperation(manifestPsiFile);
+ }
+ }
+
+ /**
+ * Generates App Indexing API code in Java source file.
+ * Steps:
+ * 1. Generates import statements in activity class.
+ * 2. Generates GoogleApiClient variable.
+ * The method will firstly scan the code to find existing App Indexing API call to get
+ * GoogleApiClient candidate. Then verify if the candidate could be reused in the generated
+ * code - it's a class member and the initialization can be adjusted to add App Indexing API.
+ * If not, a new GoogleApiClient class member will be created.
+ * 3. Generates App Indexing API call in activity.
+ *
+ * @param deepLink The deep link for the activity. It may be nullable, because
+ * we should deal with situation where no deep link is specified
+ * in AndroidManifest.xml.
+ * @param withAppIndexingDependency If gradle will be sync later.
+ */
+ @VisibleForTesting
+ void insertAppIndexingApiCodeInJavaFile(@Nullable String deepLink, boolean withAppIndexingDependency) {
+ insertImportStatements(withAppIndexingDependency);
+
+ String clientName = null, clientNameCandidate = null;
+ List<PsiStatement> statementsAppIndexApi = getAppIndexApiStatement();
+ for (PsiStatement statementAppIndexApi : statementsAppIndexApi) {
+ clientNameCandidate = getClientInAppIndexingApi(statementAppIndexApi);
+ if (clientNameCandidate != null) {
+ break;
+ }
+ }
+ if (clientNameCandidate == null) {
+ List<String> clientNames = getFieldNameByType(CLASS_GOOGLE_API_CLIENT);
+ if (!clientNames.isEmpty()) {
+ clientNameCandidate = clientNames.get(0);
+ }
+ }
+ if (clientNameCandidate != null) {
+ PsiField clientField = getFieldByName(clientNameCandidate);
+ PsiStatement clientInitStatement = null;
+ if (clientField != null && !clientField.hasInitializer()) {
+ clientInitStatement = getClientInitStatements(clientNameCandidate);
+ }
+ // Adjusts client's initialization to add AppIndexing API.
+ if (clientField != null) {
+ boolean adjustmentSucceed = false;
+ if (clientField.hasInitializer()) {
+ adjustmentSucceed = adjustClientFieldInitializerIfNeeded(clientField);
+ }
+ else if (clientInitStatement != null) {
+ adjustmentSucceed = adjustClientInitStatementIfNeeded(clientInitStatement);
+ }
+ if (adjustmentSucceed) {
+ clientName = clientNameCandidate;
+ }
+ }
+ }
+ if (clientName == null) {
+ // Creates a class member with GoogleApiClient type, and initializes it at onCreate method.
+ clientName = createGoogleApiClientField();
+ }
+
+ String actionName = getUnusedName("viewAction", VariableKind.LOCAL_VARIABLE);
+ String actionStatement = getActionStatement(deepLink, actionName);
+ addOrMergeOnStart(clientName, actionName, actionStatement);
+ addOrMergeOnStop(clientName, actionName, actionStatement);
+
+ JavaCodeStyleManager.getInstance(myProject).shortenClassReferences(myActivity);
+
+ unlockFromPsiOperation(myFile);
+ }
+
+ /**
+ * Generates import statements of app indexing Java Code.
+ *
+ * @param withAppIndexingDependency If gradle will be sync later.
+ */
+ private void insertImportStatements(boolean withAppIndexingDependency) {
+ if (!withAppIndexingDependency) {
+ // Without App Indexing dependency, App-Indexing-related classes cannot be found,
+ // so "shortenClassReferences" cannot be used.
+ for (Map.Entry<String, String> className : myImportClasses.entrySet()) {
+ if (!ImportUtils.hasOnDemandImportConflict(className.getValue(), myActivity) &&
+ !ImportUtils.hasExactImportConflict(className.getValue(), (PsiJavaFile)myFile)) {
+ insertSingleImportIfNeeded(className.getValue());
+ className.setValue(className.getKey());
+ }
+ }
+ }
+ }
+
+ /**
+ * Searches App Indexing API call in onStart & onStop.
+ *
+ * @return All the App Indexing API call found.
+ */
+ @NotNull
+ private List<PsiStatement> getAppIndexApiStatement() {
+ List<PsiStatement> result = Lists.newArrayList(myStartStatements);
+ result.addAll(myViewStatements);
+ if (!result.isEmpty()) {
+ return result;
+ }
+ result.addAll(myEndStatements);
+ result.addAll(myViewEndStatements);
+ return result;
+ }
+
+ /**
+ * Gets all assignment statements of the client in onCreate.
+ *
+ * @param clientName The GoogleApiClient name.
+ * @return All the init statements.
+ */
+ @Nullable
+ private PsiStatement getClientInitStatements(@NotNull String clientName) {
+ if (myOnCreate != null) {
+ List<PsiStatement> statements = StatementFilter.filterCodeBlock(".build()", myOnCreate);
+ for (PsiStatement statement : statements) {
+ if (statement instanceof PsiExpressionStatement) {
+ PsiType statementType = ((PsiExpressionStatement)statement).getExpression().getType();
+ if (statementType != null &&
+ statementType.getPresentableText().equals(CLASS_GOOGLE_API_CLIENT) &&
+ CharMatcher.WHITESPACE.removeFrom(statement.getText()).startsWith(clientName + "=")) {
+ return statement;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adjusts the initializer of the client field to add App Indexing API,
+ * if there is the initializer ends with ".build()" and have not added App Indexing API.
+ *
+ * @param clientField The client field.
+ * @return true if the adjustment succeeds, i.e. the initializer has added App Indexing API.
+ */
+ private boolean adjustClientFieldInitializerIfNeeded(@NotNull PsiField clientField) {
+ PsiExpression clientInitializer = clientField.getInitializer();
+ if (clientInitializer != null) {
+ String oldInitializerText = clientInitializer.getText();
+ String newInitializerText = generateApiClientInitializeString(oldInitializerText);
+ if (newInitializerText != null) {
+ if (!newInitializerText.equals(oldInitializerText)) {
+ clientField.setInitializer(myFactory.createExpressionFromText(newInitializerText, null));
+ String commentText = String.format(COMMENT_FOR_FIELD, "\"addApi(AppIndex.API)\" ");
+ myActivity.addBefore(myFactory.createCommentFromText(commentText, null), clientField);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Adjusts the the client initializing statement to add AppIndexing API,
+ * if there is the initializer ends with ".build()" and have not added App Indexing API.
+ *
+ * @param statement The init statement of client.
+ * @return true if the adjustment succeeds, i.e. the statement has added App Indexing API.
+ */
+ private boolean adjustClientInitStatementIfNeeded(@NotNull PsiStatement statement) {
+ String oldText = statement.getText();
+ String newText = generateApiClientInitializeString(oldText);
+ if (newText != null) {
+ PsiElement newStatement = statement.replace(myFactory.createStatementFromText(newText, null));
+ if (!newText.equals(oldText)) {
+ addCommentsBefore(COMMENT_BUILDER_APPINDEXAPI, myOnCreate, newStatement);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Creates a class member with GoogleApiClient type, and initializes it at onCreate method.
+ *
+ * @return The GoogleApiClient name.
+ */
+ @NotNull
+ private String createGoogleApiClientField() {
+ String clientName = getUnusedName("client", VariableKind.FIELD);
+ PsiClassType type = myFactory.createTypeByFQClassName(myImportClasses.get(CLASS_GOOGLE_API_CLIENT));
+ PsiField clientField = (PsiField)myActivity.add(myFactory.createField(clientName, type));
+ myCodeStyleManager.reformat(myActivity);
+ String comment = String.format(COMMENT_FOR_FIELD, "");
+ myActivity.addBefore(myFactory.createCommentFromText(comment, null), clientField);
+ generateGoogleApiClientInitializationInOnCreate(clientName);
+ return clientName;
+ }
+
+ /**
+ * Generates GoogleApiClient initialization in onCreate method.
+ * Used when there is no initializing statement before.
+ *
+ * @param clientName The GoogleApiClient name.
+ */
+ private void generateGoogleApiClientInitializationInOnCreate(@NotNull String clientName) {
+ if (myOnCreate == null) {
+ String onCreateMethod =
+ String.format(ON_CREATE_FORMAT, clientName, myImportClasses.get(CLASS_GOOGLE_API_CLIENT), myImportClasses.get(CLASS_APP_INDEX));
+ PsiMethod newOnCreate = myFactory.createMethodFromText(onCreateMethod, null);
+ myActivity.add(newOnCreate);
+ }
+ else {
+ String initText =
+ String.format(CLIENT_FORMAT, clientName, myImportClasses.get(CLASS_GOOGLE_API_CLIENT), myImportClasses.get(CLASS_APP_INDEX));
+ PsiElement initStatement = myOnCreate.add(myFactory.createStatementFromText(initText, null));
+ addCommentsBefore(COMMENT_IN_JAVA, myOnCreate, initStatement);
+ }
+ }
+
+ /**
+ * Generates app indexing api calls (GoogleApiClient's connect & start) in onStart method.
+ *
+ * @param clientName The GoogleApiClient name.
+ * @param actionName The Action name.
+ * @param actionInitText The text of initializing statement for Action local variable.
+ */
+ private void addOrMergeOnStart(@NotNull String clientName, @NotNull String actionName, @NotNull String actionInitText) {
+ if (myOnStart == null) {
+ String onStartMethod = String.format(ON_START_FORMAT, clientName, actionName, actionInitText, myImportClasses.get(CLASS_APP_INDEX));
+ myActivity.add(myFactory.createMethodFromText(onStartMethod, null));
+ }
+ else {
+ String connectCall = clientName + ".connect();";
+ List<PsiStatement> connectStatements = StatementFilter.filterCodeBlock(connectCall, myOnStart);
+ // Creates connect() if needed. It should be on the top.
+ if (connectStatements.isEmpty()) {
+ PsiStatement connectStatement = myFactory.createStatementFromText(connectCall, null);
+ List<PsiStatement> superOnStartStatements = StatementFilter.filterCodeBlock("super.onStart();", myOnStart);
+ if (!superOnStartStatements.isEmpty()) {
+ // Adds connect() statement after "super.onStart();".
+ connectStatement = (PsiStatement)myOnStart.addAfter(connectStatement, superOnStartStatements.get(0));
+ addCommentsBefore(COMMENT_IN_JAVA, myOnStart, connectStatement);
+ }
+ else {
+ // Adds connect() statement after "{" in the code block.
+ connectStatement = (PsiStatement)myOnStart.addAfter(connectStatement, myOnStart.getFirstBodyElement());
+ addCommentsBefore(COMMENT_IN_JAVA, myOnStart, connectStatement);
+ }
+ }
+ String startTextHalf = String.format(APP_INDEXING_START_HALF, clientName);
+ myStartStatements = StatementFilter.filterStatements(startTextHalf, myStartStatements);
+ String viewText = String.format(APP_INDEXING_VIEW, clientName);
+ myViewStatements = StatementFilter.filterStatements(viewText, myViewStatements);
+ // Creates Action variable and AppIndex.AppIndexApi.start() at the bottom.
+ if (myStartStatements.isEmpty() && myViewStatements.isEmpty()) {
+ PsiElement actionStatement = myOnStart.add(myFactory.createStatementFromText(actionInitText, null));
+ addCommentsBefore(COMMENT_IN_JAVA, myOnStart, actionStatement);
+ String startText = String.format(APP_INDEXING_START, clientName, actionName, myImportClasses.get(CLASS_APP_INDEX));
+ myOnStart.add(myFactory.createStatementFromText(startText, null));
+ }
+ }
+ }
+
+ /**
+ * Generates app indexing api calls (GoogleApiClient's disconnect & end) in onStop method.
+ *
+ * @param clientName The GoogleApiClient name.
+ * @param actionName The Action name.
+ * @param actionInitText The initializing statement for Action local variable.
+ */
+ private void addOrMergeOnStop(@NotNull String clientName, @NotNull String actionName, @NotNull String actionInitText) {
+ if (myOnStop == null) {
+ String onStopMethod = String.format(ON_STOP_FORMAT, clientName, actionName, actionInitText, myImportClasses.get(CLASS_APP_INDEX));
+ myActivity.add(myFactory.createMethodFromText(onStopMethod, null));
+ }
+ else {
+ String disconnectCall = clientName + ".disconnect();";
+ List<PsiStatement> disconnectStatements = StatementFilter.filterCodeBlock(disconnectCall, myOnStop);
+ // Creates disconnect() if needed. It should be at the bottom.
+ if (disconnectStatements.isEmpty()) {
+ PsiElement disconnectStatement = myOnStop.add(myFactory.createStatementFromText(disconnectCall, null));
+ addCommentsBefore(COMMENT_IN_JAVA, myOnStop, disconnectStatement);
+ }
+ String endTextHalf = String.format(APP_INDEXING_END_HALF, clientName);
+ myEndStatements = StatementFilter.filterStatements(endTextHalf, myEndStatements);
+ String viewEndText = String.format(APP_INDEXING_VIEWEND, clientName);
+ myViewEndStatements = StatementFilter.filterStatements(viewEndText, myViewEndStatements);
+ // Creates Action variable and AppIndex.AppIndexApi.end() on the top.
+ if (myEndStatements.isEmpty() && myViewEndStatements.isEmpty()) {
+ String endText = String.format(APP_INDEXING_END, clientName, actionName, myImportClasses.get(CLASS_APP_INDEX));
+ PsiStatement endStatement = myFactory.createStatementFromText(endText, null);
+ List<PsiStatement> superOnStopStatements = StatementFilter.filterCodeBlock("super.onStop();", myOnStop);
+ if (!superOnStopStatements.isEmpty()) {
+ // after "super.onStop();"
+ myOnStop.addAfter(endStatement, superOnStopStatements.get(0));
+ PsiStatement actionStatement = myFactory.createStatementFromText(actionInitText, null);
+ actionStatement = (PsiStatement)myOnStop.addAfter(actionStatement, superOnStopStatements.get(0));
+ addCommentsBefore(COMMENT_IN_JAVA, myOnStop, actionStatement);
+ }
+ else {
+ // after "{" in the code block.
+ endStatement = (PsiStatement)myOnStop.addAfter(endStatement, myOnStop.getFirstBodyElement());
+ PsiStatement actionStatement = myFactory.createStatementFromText(actionInitText, null);
+ actionStatement = (PsiStatement)myOnStop.addBefore(actionStatement, endStatement);
+ addCommentsBefore(COMMENT_IN_JAVA, myOnStop, actionStatement);
+ }
+ }
+ }
+ }
+
+ /**
+ * Compares two versions in the form of "12.3.456" etc.
+ *
+ * @return < 0 if version1 < version2; = 0 if version1 = version2; > 0 if version1 > version2
+ */
+ private static int compareVersion(@NotNull String version1, @NotNull String version2) {
+ GradleCoordinate coordinate1 = GradleCoordinate.parseVersionOnly(version1);
+ GradleCoordinate coordinate2 = GradleCoordinate.parseVersionOnly(version2);
+ return GradleCoordinate.COMPARE_PLUS_HIGHER.compare(coordinate1, coordinate2);
+ }
+
+ /**
+ * Gets the GMS meta-data tag in the application tag.
+ */
+ @Nullable
+ private static XmlTag getGmsTag(@NotNull XmlTag application) {
+ XmlTag[] children = application.getSubTags();
+ for (XmlTag child : children) {
+ if (child.getName().equalsIgnoreCase(TAG_METADATA)) {
+ String tagName = child.getAttributeValue(SdkConstants.ATTR_NAME, SdkConstants.ANDROID_URI);
+ if (tagName != null && tagName.equals(ATTR_NAME_METADATA_GMS)) {
+ return child;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the client name in an app indexing API call statement.
+ */
+ @Nullable
+ private static String getClientInAppIndexingApi(@NotNull PsiStatement statement) {
+ if (statement instanceof PsiExpressionStatement) {
+ // In the form of "AppIndex.AppIndexApi.start(mClient, viewAction);".
+ PsiElement[] children = statement.getChildren();
+ for (PsiElement child : children) {
+ if (child instanceof PsiMethodCallExpression) {
+ String callExpression = ((PsiMethodCallExpression)child).getMethodExpression().getText();
+ callExpression = CharMatcher.WHITESPACE.removeFrom(callExpression);
+ PsiType[] argsType = ((PsiMethodCallExpression)child).getArgumentList().getExpressionTypes();
+ // API start / end has 2 arguments, viewEnd has 3 arguments, and view has 2.
+ // The first arg is GoogleApiClient. If not declared before used, it has a null type.
+ if ((callExpression.startsWith("AppIndex.AppIndexApi.") ||
+ callExpression.startsWith("AppIndexApi") ||
+ callExpression.startsWith(CLASS_APP_INDEX_FULL)) &&
+ (argsType.length == 2 || argsType.length == 3 || argsType.length == 6) &&
+ argsType[0] != null) {
+ PsiExpression[] args = ((PsiMethodCallExpression)child).getArgumentList().getExpressions();
+ String clientName = args[0].getText();
+ if (clientName != null) {
+ return clientName;
+ }
+ }
+ }
+ }
+ }
+ else if (statement instanceof PsiDeclarationStatement) {
+ // In form of "PendingResult<Status> a = AppIndex.AppIndexApi.start(mClient, viewAction);".
+ PsiElement[] children = ((PsiDeclarationStatement)statement).getDeclaredElements();
+ for (PsiElement child : children) {
+ if (child instanceof PsiVariable) {
+ PsiExpression initializer = ((PsiVariable)child).getInitializer();
+ if (initializer != null) {
+ // Initializer is in the form of "AppIndex.AppIndexApi.start(mClient, viewAction)".
+ PsiElement[] initChildren = initializer.getChildren();
+ if (initChildren.length == 2 &&
+ initChildren[0] instanceof PsiReferenceExpression &&
+ initChildren[1] instanceof PsiExpressionList) {
+ // initChildren[0] should be in the form of "AppIndex.AppIndexApi.start".
+ // initChildren[1] should be in the form of "(mClient, ...)".
+ // args should be in the form of {"mClient", ...}. Check its length and type.
+ String referenceExpression = CharMatcher.WHITESPACE.removeFrom(initChildren[0].getText());
+ PsiExpression[] args = ((PsiExpressionList)initChildren[1]).getExpressions();
+ // API start / end has 2 arguments, viewEnd has 3 arguments, and view has 2.
+ // The first arg is GoogleApiClient. If not declared before used, it has a null type.
+ if ((referenceExpression.startsWith("AppIndex.AppIndexApi.") ||
+ referenceExpression.startsWith("AppIndexApi.") ||
+ referenceExpression.startsWith(CLASS_APP_INDEX_FULL)) &&
+ (args.length == 2 || args.length == 3 || args.length == 6) && args[0].getType() != null) {
+ String clientName = args[0].getText();
+ if (clientName != null) {
+ return clientName;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Generates a name which has not been used.
+ * Example: if "name" is used, "name2" will be returned;
+ * if "name2" is also used, "name3" will be returned; ...
+ *
+ * @param name The initial default name.
+ * @return A name based on the initial name which has not been used.
+ */
+ @NotNull
+ private String getUnusedName(String name, VariableKind kind) {
+ JavaCodeStyleManager javaCodeStyleManager = JavaCodeStyleManager.getInstance(myProject);
+ name = javaCodeStyleManager.suggestVariableName(kind, name, null, null).names[0];
+
+ Set<String> usedName = getUsedVariableName();
+ String unusedName = name;
+ int suffix = 2;
+ while (usedName.contains(unusedName)) {
+ unusedName = name + suffix;
+ suffix++;
+ }
+ return unusedName;
+ }
+
+ /**
+ * Generates GoogleApiClient initializing string.
+ *
+ * @param statementText The original initialize string.
+ * @return The new string.
+ */
+ @Nullable
+ private String generateApiClientInitializeString(@NotNull String statementText) {
+ int splitPoint = statementText.lastIndexOf(".build()");
+ if (!statementText.contains(String.format(BUILDER_APPINDEXAPI, CLASS_APP_INDEX_FULL)) &&
+ !statementText.contains(String.format(BUILDER_APPINDEXAPI, CLASS_APP_INDEX)) && splitPoint != -1) {
+ return statementText.substring(0, splitPoint) +
+ String.format(BUILDER_APPINDEXAPI, myImportClasses.get(CLASS_APP_INDEX)) +
+ statementText.substring(splitPoint);
+ }
+ else if (splitPoint != -1) {
+ return statementText;
+ }
+ return null;
+ }
+
+ /**
+ * Generates Action initializing statement.
+ *
+ * @param deepLink The deep link for the activity. It may be nullable because, we should deal
+ * with situation where no deep link is specified in AndroidManifest.xml.
+ * @param actionName The name of the Action local variable.
+ * @return The initializing statement.
+ */
+ @NotNull
+ @VisibleForTesting
+ String getActionStatement(@Nullable String deepLink, @NotNull String actionName) {
+ String pageName = myActivity.getName();
+ if (pageName.endsWith("Activity")) {
+ pageName = pageName.substring(0, pageName.length() - 8);
+ }
+ String packageName = ((PsiJavaFile)myFile).getPackageName();
+ String scheme = (deepLink == null) ? "http" : deepLink.substring(0, deepLink.indexOf("://"));
+ String hostAndPath = getHostAndPathOfDeepLink(deepLink);
+ return String.format(ACTION_FORMAT, actionName, pageName, packageName, hostAndPath, scheme, myImportClasses.get(CLASS_ACTION));
+ }
+
+ /**
+ * Gets the non-scheme part of a deep link, including the host and the path / path prefix;
+ *
+ * @param deepLink The deep link string.
+ * @return The host and path of a deep link.
+ */
+ @NotNull
+ private static String getHostAndPathOfDeepLink(@Nullable String deepLink) {
+ String hostAndPath = "host/path";
+ if (deepLink != null) {
+ if (!deepLink.substring(deepLink.indexOf("://") + 3).isEmpty()) {
+ hostAndPath = deepLink.substring(deepLink.indexOf("://") + 3);
+ }
+ if (!hostAndPath.contains("/")) {
+ hostAndPath += "/path";
+ }
+ }
+ return hostAndPath;
+ }
+
+ /**
+ * Returns deep links of current activity in AndroidManifest.xml.
+ */
+ @Nullable
+ @VisibleForTesting
+ String getDeepLinkOfActivity() {
+ XmlFile manifest = ManifestUtils.getAndroidManifestPsi(myModule);
+ if (manifest != null && manifest.getRootTag() != null) {
+ List<XmlTag> activityTags = ManifestUtils.searchXmlTagsByName(manifest.getRootTag(), SdkConstants.TAG_ACTIVITY);
+ for (XmlTag activityTag : activityTags) {
+ String activityName = activityTag.getAttributeValue(SdkConstants.ATTR_NAME, SdkConstants.ANDROID_URI);
+ if (activityName != null && activityName.equals("." + myActivity.getName())) {
+ List<String> deepLinks = DeepLinkUtils.getAllDeepLinks(activityTag);
+ if (!deepLinks.isEmpty()) {
+ return deepLinks.get(0);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Creates xml comment element.
+ *
+ * @param text The text form of comment.
+ * @return The created comment.
+ */
+ @Nullable
+ private XmlComment createXmlComment(@NotNull String text) {
+ // XmlElementFactory does not provide API for creating comment.
+ // So we create a tag wrapping the comment, and extract comment from the created tag.
+ XmlElementFactory xmlElementFactory = XmlElementFactory.getInstance(myProject);
+ XmlTag commentElement = xmlElementFactory.createTagFromText("<foo>" + text + "</foo>", XMLLanguage.INSTANCE);
+ return PsiTreeUtil.getChildOfType(commentElement, XmlComment.class);
+ }
+
+ /**
+ * Adds comments before a specific psi element.
+ *
+ * @param texts The text of the comments.
+ * "// Comment1.
+ * // Comment2." are 2 comments.
+ * Factory cannot create 2 comments together, so we split them into list of comment texts.
+ * @param element The psi element for adding comment in.
+ * @param anchor The psi element the added comment is anchored before.
+ */
+ private void addCommentsBefore(@NotNull List<String> texts, @NotNull PsiElement element, @NotNull PsiElement anchor) {
+ myCodeStyleManager.reformat(element);
+ for (String text : texts) {
+ element.addBefore(myFactory.createCommentFromText(text, null), anchor);
+ myCodeStyleManager.reformat(element);
+ }
+ }
+
+ /**
+ * Unlocks the file locked by PSI operation, so that the project can sync.
+ *
+ * @param file The file locked by PSI operation.
+ */
+ private void unlockFromPsiOperation(@NotNull PsiFile file) {
+ Document doc = PsiDocumentManager.getInstance(myProject).getDocument(file);
+ if (doc != null) {
+ PsiDocumentManager.getInstance(myProject).doPostponedOperationsAndUnblockDocument(doc);
+ }
+ }
+
+ /**
+ * Gets the surrounding class, if it inherits from a particular super class.
+ *
+ * @param element The PsiElement for query.
+ * @param inheritedClass the name of the class to inherit from
+ * @return The surrounding class that meet the inheriting requirement.
+ */
+ @Nullable
+ private static PsiClass getSurroundingInheritingClass(@NotNull PsiElement element, @NotNull String inheritedClass) {
+ while (element != null) {
+ if (element instanceof PsiClass) {
+ PsiClass psiClass = (PsiClass)element;
+ if (InheritanceUtil.isInheritor(psiClass, inheritedClass)) {
+ return psiClass;
+ }
+ }
+ if (element instanceof PsiFile) {
+ return null;
+ }
+ element = element.getParent();
+ }
+ return null;
+ }
+
+ /**
+ * Gets the body of the method in a specific class, given the method name.
+ *
+ * @param name The method name.
+ * @param parametersTypeName The type name of the parameters in the specific method.
+ * @param psiClass The class to find method in.
+ * @return The code block body of the method.
+ */
+ @Nullable
+ private static PsiCodeBlock getMethodBodyByName(@NotNull String name,
+ @NotNull List<String> parametersTypeName,
+ @NotNull PsiClass psiClass) {
+ PsiMethod[] psiMethods = psiClass.findMethodsByName(name, false);
+ for (PsiMethod psiMethod : psiMethods) {
+ PsiType[] types = psiMethod.getSignature(PsiSubstitutor.EMPTY).getParameterTypes();
+ if (types.length != parametersTypeName.size()) {
+ continue;
+ }
+ boolean correctSignature = true;
+ for (int i = 0; i < types.length; i++) {
+ if (!types[i].getCanonicalText().equals(parametersTypeName.get(i))) {
+ correctSignature = false;
+ break;
+ }
+ }
+ if (correctSignature) {
+ return psiMethod.getBody();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets names of class members, given the type name.
+ *
+ * @param typeName The type presentable name.
+ * @return The members' names
+ */
+ @NotNull
+ private List<String> getFieldNameByType(@NotNull String typeName) {
+ List<String> result = Lists.newArrayList();
+ PsiField[] psiFields = myActivity.getFields();
+ for (PsiField psiField : psiFields) {
+ if (psiField.getType().getPresentableText().equals(typeName)) {
+ result.add(psiField.getName());
+ }
+ }
+ return result;
+ }
+
+ @Nullable
+ private PsiField getFieldByName(@NotNull String name) {
+ PsiField[] psiFields = myActivity.getFields();
+ for (PsiField psiField : psiFields) {
+ if (psiField.getName().equals(name)) {
+ return psiField;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Inserts a single import statement.
+ *
+ * @param className The name of the import class.
+ */
+ private void insertSingleImportIfNeeded(@NotNull String className) {
+ PsiImportList importList = ((PsiJavaFile)myFile).getImportList();
+ if (importList != null && !hasImportStatement(importList, className)) {
+ String dummyFileName = "_Dummy_" + className + "_." + JavaFileType.INSTANCE.getDefaultExtension();
+ PsiJavaFile aFile = (PsiJavaFile)PsiFileFactory.getInstance(myProject)
+ .createFileFromText(dummyFileName, JavaFileType.INSTANCE, "import " + className + ";");
+ PsiImportList dummyImportList = aFile.getImportList();
+ if (dummyImportList != null) {
+ PsiImportStatement[] statements = dummyImportList.getImportStatements();
+ PsiImportStatement statement = (PsiImportStatement)myCodeStyleManager.reformat(statements[0]);
+ importList.add(statement);
+ }
+ }
+ }
+
+ /**
+ * If an class has been imported in the importList.
+ *
+ * @param importList The import list.
+ * @param className The name of the class to be imported.
+ * @return True if an class has been imported.
+ */
+ private static boolean hasImportStatement(@NotNull PsiImportList importList, @NotNull String className) {
+ String packageName = className.substring(0, className.lastIndexOf('.'));
+ PsiImportStatement singleImport = importList.findSingleClassImportStatement(className);
+ PsiImportStatement onDemandImport = importList.findOnDemandImportStatement(packageName);
+ return singleImport != null || onDemandImport != null;
+ }
+
+ /**
+ * Returns all the fields' and local variables' names which have already been used.
+ */
+ @NotNull
+ private Set<String> getUsedVariableName() {
+ Set<String> usedNames = Sets.newHashSet();
+ // name of PsiFields in PsiClass
+ PsiField[] psiFields = myActivity.getFields();
+ for (PsiField psiField : psiFields) {
+ usedNames.add(psiField.getName());
+ }
+ // name of PsiLocalVariables in PsiMethods in PsiClass
+ PsiMethod[] psiMethods = myActivity.getMethods();
+ for (PsiMethod psiMethod : psiMethods) {
+ PsiCodeBlock methodBody = psiMethod.getBody();
+ if (methodBody != null) {
+ List<PsiStatement> psiStatements = StatementFilter.filterCodeBlock("", methodBody);
+ for (PsiStatement psiStatement : psiStatements) {
+ if (psiStatement instanceof PsiDeclarationStatement) {
+ PsiElement[] declaredElements = ((PsiDeclarationStatement)psiStatement).getDeclaredElements();
+ for (PsiElement declaredElement : declaredElements) {
+ usedNames.add(((PsiLocalVariable)declaredElement).getName());
+ }
+ }
+ }
+ }
+ }
+ return usedNames;
+ }
+
+ static class ApiCreatorException extends Exception {
+ public ApiCreatorException(String message) {
+ super(message);
+ }
+ }
+}