summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYi Zhang <yizhan@google.com>2015-10-13 14:23:24 +0800
committerYi Zhang <yizhan@google.com>2015-10-27 14:43:50 +0800
commit634e7e4b4946919c549783e6646b422890ec0dd7 (patch)
tree8f4ad730d8ff19e8f3a15e13a8eb930c894df3fc
parent88bcdbe3f21beea2e90b6dc19a3d220e38e7bd25 (diff)
downloadappindexing-634e7e4b4946919c549783e6646b422890ec0dd7.tar.gz
Add App Indexing API creation tool - a generator and an intention
action. Change-Id: Id50bc3c95cae012b2a0b0e5936f2767689a0c80f
-rw-r--r--google-appindexing.iml3
-rw-r--r--resources/intentionDescriptions/InsertApiCodeIntentionAction/after.gradle.template3
-rw-r--r--resources/intentionDescriptions/InsertApiCodeIntentionAction/after.java.template66
-rw-r--r--resources/intentionDescriptions/InsertApiCodeIntentionAction/after.xml.template20
-rw-r--r--resources/intentionDescriptions/InsertApiCodeIntentionAction/before.gradle.template2
-rw-r--r--resources/intentionDescriptions/InsertApiCodeIntentionAction/before.java.template6
-rw-r--r--resources/intentionDescriptions/InsertApiCodeIntentionAction/before.xml.template15
-rw-r--r--resources/intentionDescriptions/InsertApiCodeIntentionAction/description.html22
-rw-r--r--src/META-INF/plugin.xml8
-rw-r--r--src/com/google/appindexing/actions/GenerateApiCodeAction.java39
-rw-r--r--src/com/google/appindexing/actions/GenerateApiCodeHandler.java40
-rw-r--r--src/com/google/appindexing/actions/InsertApiCodeIntentionAction.java60
-rw-r--r--src/com/google/appindexing/api/ApiCreator.java1070
-rw-r--r--src/com/google/appindexing/api/StatementFilter.java97
-rw-r--r--src/com/google/appindexing/util/DeepLinkUtils.java134
-rw-r--r--src/com/google/appindexing/util/ManifestUtils.java92
16 files changed, 1677 insertions, 0 deletions
diff --git a/google-appindexing.iml b/google-appindexing.iml
index b67bd1a..6b396a9 100644
--- a/google-appindexing.iml
+++ b/google-appindexing.iml
@@ -37,5 +37,8 @@
<SOURCES />
</library>
</orderEntry>
+ <orderEntry type="module" module-name="java-analysis-impl" />
+ <orderEntry type="module" module-name="openapi" />
+ <orderEntry type="module" module-name="java-impl" />
</component>
</module> \ No newline at end of file
diff --git a/resources/intentionDescriptions/InsertApiCodeIntentionAction/after.gradle.template b/resources/intentionDescriptions/InsertApiCodeIntentionAction/after.gradle.template
new file mode 100644
index 0000000..6b142ef
--- /dev/null
+++ b/resources/intentionDescriptions/InsertApiCodeIntentionAction/after.gradle.template
@@ -0,0 +1,3 @@
+dependencies {
+ compile 'com.google.android.gms:play-services-appindexing:8.1.0'
+}
diff --git a/resources/intentionDescriptions/InsertApiCodeIntentionAction/after.java.template b/resources/intentionDescriptions/InsertApiCodeIntentionAction/after.java.template
new file mode 100644
index 0000000..38dbb0a
--- /dev/null
+++ b/resources/intentionDescriptions/InsertApiCodeIntentionAction/after.java.template
@@ -0,0 +1,66 @@
+package com.example.appindexing;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+
+import com.google.android.gms.appindexing.Action;
+import com.google.android.gms.appindexing.AppIndex;
+import com.google.android.gms.common.api.GoogleApiClient;
+
+public class MainActivity extends Activity {
+ /**
+ * ATTENTION: This was auto-generated to implement the App Indexing API.
+ * See https://g.co/AppIndexing/AndroidStudio for more information.
+ */
+ private GoogleApiClient client;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // ATTENTION: This was auto-generated to implement the App Indexing API.
+ // See https://g.co/AppIndexing/AndroidStudio for more information.
+ client = new GoogleApiClient.Builder(this).addApi(AppIndex.API).build();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ // ATTENTION: This was auto-generated to implement the App Indexing API.
+ // See https://g.co/AppIndexing/AndroidStudio for more information.
+ client.connect();
+ Action viewAction = Action.newAction(
+ Action.TYPE_VIEW, // TODO: choose an action type.
+ "Main Page", // TODO: Define a title for the content shown.
+ // TODO: If you have web page content that matches this app activity's content,
+ // make sure this auto-generated web page URL is correct.
+ // Otherwise, set the URL to null.
+ Uri.parse("http://www.example.com/main"),
+ // TODO: Make sure this auto-generated app deep link URI is correct.
+ Uri.parse("android-app://com.example.appindexing/http/www.example.com/main")
+ );
+ AppIndex.AppIndexApi.start(client, viewAction);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ // ATTENTION: This was auto-generated to implement the App Indexing API.
+ // See https://g.co/AppIndexing/AndroidStudio for more information.
+ Action viewAction = Action.newAction(
+ Action.TYPE_VIEW, // TODO: choose an action type.
+ "Main Page", // TODO: Define a title for the content shown.
+ // TODO: If you have web page content that matches this app activity's content,
+ // make sure this auto-generated web page URL is correct.
+ // Otherwise, set the URL to null.
+ Uri.parse("http://www.example.com/main"),
+ // TODO: Make sure this auto-generated app deep link URI is correct.
+ Uri.parse("android-app://com.example.appindexing/http/www.example.com/main")
+ );
+ AppIndex.AppIndexApi.end(client, viewAction);
+ client.disconnect();
+ }
+}
diff --git a/resources/intentionDescriptions/InsertApiCodeIntentionAction/after.xml.template b/resources/intentionDescriptions/InsertApiCodeIntentionAction/after.xml.template
new file mode 100644
index 0000000..755a526
--- /dev/null
+++ b/resources/intentionDescriptions/InsertApiCodeIntentionAction/after.xml.template
@@ -0,0 +1,20 @@
+<manifest>
+ <application>
+ <activity android:name=".MainActivity" >
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data
+ android:host="www.example.com"
+ android:pathPrefix="/main"
+ android:scheme="http" />
+ </intent-filter>
+ </activity>
+ <!-- ATTENTION: This was auto-generated to add Google Play services to your project for
+ App Indexing. See https://g.co/AppIndexing/AndroidStudio for more information. -->
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version" />
+ </application>
+</manifest>
diff --git a/resources/intentionDescriptions/InsertApiCodeIntentionAction/before.gradle.template b/resources/intentionDescriptions/InsertApiCodeIntentionAction/before.gradle.template
new file mode 100644
index 0000000..7d82dc7
--- /dev/null
+++ b/resources/intentionDescriptions/InsertApiCodeIntentionAction/before.gradle.template
@@ -0,0 +1,2 @@
+dependencies {
+}
diff --git a/resources/intentionDescriptions/InsertApiCodeIntentionAction/before.java.template b/resources/intentionDescriptions/InsertApiCodeIntentionAction/before.java.template
new file mode 100644
index 0000000..ddc4ac7
--- /dev/null
+++ b/resources/intentionDescriptions/InsertApiCodeIntentionAction/before.java.template
@@ -0,0 +1,6 @@
+package com.example.appindexing;
+
+import android.app.Activity;
+
+public class MainActivity extends Activity {
+}
diff --git a/resources/intentionDescriptions/InsertApiCodeIntentionAction/before.xml.template b/resources/intentionDescriptions/InsertApiCodeIntentionAction/before.xml.template
new file mode 100644
index 0000000..16cb528
--- /dev/null
+++ b/resources/intentionDescriptions/InsertApiCodeIntentionAction/before.xml.template
@@ -0,0 +1,15 @@
+<manifest>
+ <application>
+ <activity android:name=".MainActivity" >
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data
+ android:host="www.example.com"
+ android:pathPrefix="/main"
+ android:scheme="http" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/resources/intentionDescriptions/InsertApiCodeIntentionAction/description.html b/resources/intentionDescriptions/InsertApiCodeIntentionAction/description.html
new file mode 100644
index 0000000..b6fa9f2
--- /dev/null
+++ b/resources/intentionDescriptions/InsertApiCodeIntentionAction/description.html
@@ -0,0 +1,22 @@
+<!--
+ ~ 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.
+ -->
+<html>
+<body>
+This intention creates App Indexing API code for publishing deep links.
+<br />
+Please reference g.co/AppIndexing.
+</body>
+</html>
diff --git a/src/META-INF/plugin.xml b/src/META-INF/plugin.xml
index 4e3c378..c193878 100644
--- a/src/META-INF/plugin.xml
+++ b/src/META-INF/plugin.xml
@@ -15,6 +15,10 @@
<depends>com.google.gct.login</depends>
<extensions defaultExtensionNs="com.intellij">
+ <intentionAction>
+ <className>com.google.appindexing.actions.InsertApiCodeIntentionAction</className>
+ <category>Android</category>
+ </intentionAction>
</extensions>
<application-components>
@@ -26,6 +30,10 @@
</project-components>
<actions>
+ <action id="AppIndexingCodeGeneratorAction" class="com.google.appindexing.actions.GenerateApiCodeAction"
+ text="App Indexing API Code" description="Generate App Indexing API Code to Annotate this Activity for App Indexing">
+ <add-to-group group-id="GenerateGroup" anchor="last"/>
+ </action>
</actions>
</idea-plugin>
diff --git a/src/com/google/appindexing/actions/GenerateApiCodeAction.java b/src/com/google/appindexing/actions/GenerateApiCodeAction.java
new file mode 100644
index 0000000..381f6c1
--- /dev/null
+++ b/src/com/google/appindexing/actions/GenerateApiCodeAction.java
@@ -0,0 +1,39 @@
+/*
+ * 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.actions;
+
+import com.google.appindexing.api.ApiCreator;
+
+import com.intellij.codeInsight.generation.actions.BaseGenerateAction;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.*;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Generates App Indexing API code.
+ */
+public class GenerateApiCodeAction extends BaseGenerateAction {
+
+ GenerateApiCodeAction() {
+ super(new GenerateApiCodeHandler());
+ }
+
+ @Override
+ protected boolean isValidForFile(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
+ return super.isValidForFile(project, editor, file) && ApiCreator.eligibleForInsertingAppIndexingApiCode(editor, file);
+ }
+}
diff --git a/src/com/google/appindexing/actions/GenerateApiCodeHandler.java b/src/com/google/appindexing/actions/GenerateApiCodeHandler.java
new file mode 100644
index 0000000..c697292
--- /dev/null
+++ b/src/com/google/appindexing/actions/GenerateApiCodeHandler.java
@@ -0,0 +1,40 @@
+/*
+ * 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.actions;
+
+import com.android.tools.idea.stats.UsageTracker;
+import com.google.appindexing.api.ApiCreator;
+import com.intellij.codeInsight.CodeInsightActionHandler;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+
+public class GenerateApiCodeHandler implements CodeInsightActionHandler {
+
+ @Override
+ public void invoke(@NotNull final Project project, @NotNull Editor editor, @NotNull PsiFile file) {
+ UsageTracker.getInstance()
+ .trackEvent(UsageTracker.CATEGORY_APP_INDEXING, UsageTracker.ACTION_APP_INDEXING_API_CODE_CREATED, null, null);
+ ApiCreator creator = new ApiCreator(project, editor, file);
+ creator.insertAppIndexingApiCodeForActivity();
+ }
+
+ @Override
+ public boolean startInWriteAction() {
+ return true;
+ }
+}
diff --git a/src/com/google/appindexing/actions/InsertApiCodeIntentionAction.java b/src/com/google/appindexing/actions/InsertApiCodeIntentionAction.java
new file mode 100644
index 0000000..43ec7e1
--- /dev/null
+++ b/src/com/google/appindexing/actions/InsertApiCodeIntentionAction.java
@@ -0,0 +1,60 @@
+/*
+ * 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.actions;
+
+import com.android.tools.idea.stats.UsageTracker;
+import com.google.appindexing.api.ApiCreator;
+
+import com.intellij.codeInsight.intention.AbstractIntentionAction;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiFile;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An intention action to insert App Indexing API code.
+ */
+public class InsertApiCodeIntentionAction extends AbstractIntentionAction {
+
+ @Override
+ public String getText() {
+ return "Insert App Indexing API Code";
+ }
+
+ @Override
+ public boolean startInWriteAction() {
+ return true;
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
+ if (editor == null || file == null) {
+ return false;
+ }
+ return ApiCreator.eligibleForInsertingAppIndexingApiCode(editor, file);
+ }
+
+ @Override
+ public void invoke(@NotNull final Project project, Editor editor, PsiFile file) {
+ if (editor != null && file != null) {
+ UsageTracker.getInstance()
+ .trackEvent(UsageTracker.CATEGORY_APP_INDEXING, UsageTracker.ACTION_APP_INDEXING_API_CODE_CREATED, null, null);
+ ApiCreator creator = new ApiCreator(project, editor, file);
+ creator.insertAppIndexingApiCodeForActivity();
+ }
+ }
+}
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);
+ }
+ }
+}
diff --git a/src/com/google/appindexing/api/StatementFilter.java b/src/com/google/appindexing/api/StatementFilter.java
new file mode 100644
index 0000000..b809414
--- /dev/null
+++ b/src/com/google/appindexing/api/StatementFilter.java
@@ -0,0 +1,97 @@
+/*
+ * 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.google.common.base.CharMatcher;
+import com.google.common.collect.Lists;
+
+import com.intellij.psi.JavaRecursiveElementVisitor;
+import com.intellij.psi.PsiCodeBlock;
+import com.intellij.psi.PsiDeclarationStatement;
+import com.intellij.psi.PsiExpressionStatement;
+import com.intellij.psi.PsiStatement;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * A filter for searching a part of code and getting statements which contains the filter string.
+ * <p/>
+ * Example:
+ * Given filter string "start", StatementFilter returns
+ * "AppIndex.AppIndexApi.start(mClient, viewAction);" for the code below.
+ * int a = 0;
+ * if (a == 0) {
+ * AppIndex.AppIndexApi.start(mClient, viewAction);
+ * }
+ */
+public final class StatementFilter extends JavaRecursiveElementVisitor {
+
+ private String myFilterString;
+
+ private List<PsiStatement> myStatements = Lists.newArrayList();
+
+ public StatementFilter(@NotNull String filterString) {
+ myFilterString = CharMatcher.WHITESPACE.removeFrom(filterString);
+ }
+
+ @NotNull
+ public List<PsiStatement> getStatements() {
+ return myStatements;
+ }
+
+ @Override
+ public void visitExpressionStatement(PsiExpressionStatement statement) {
+ if (CharMatcher.WHITESPACE.removeFrom(statement.getText()).contains(myFilterString)) {
+ myStatements.add(statement);
+ }
+ }
+
+ @Override
+ public void visitDeclarationStatement(PsiDeclarationStatement statement) {
+ if (CharMatcher.WHITESPACE.removeFrom(statement.getText()).contains(myFilterString)) {
+ myStatements.add(statement);
+ }
+ }
+
+ /**
+ * Filters statements in a code block with the filter string.
+ *
+ * @param codeBlock The code block to process.
+ * @return Statements that contain the string and pass the filter.
+ */
+ @NotNull
+ public static List<PsiStatement> filterCodeBlock(@NotNull String filterString, @NotNull PsiCodeBlock codeBlock) {
+ StatementFilter filter = new StatementFilter(filterString);
+ codeBlock.accept(filter);
+ return filter.getStatements();
+ }
+
+ /**
+ * Filters statements in the list.
+ *
+ * @param statements The list of statements.
+ * @return Statements that contain the string and pass the filter.
+ */
+ @NotNull
+ public static List<PsiStatement> filterStatements(@NotNull String filterString, @NotNull List<PsiStatement> statements) {
+ StatementFilter filter = new StatementFilter(filterString);
+ for (PsiStatement statement : statements) {
+ statement.accept(filter);
+ }
+ return filter.getStatements();
+ }
+}
diff --git a/src/com/google/appindexing/util/DeepLinkUtils.java b/src/com/google/appindexing/util/DeepLinkUtils.java
new file mode 100644
index 0000000..d2c8a7c
--- /dev/null
+++ b/src/com/google/appindexing/util/DeepLinkUtils.java
@@ -0,0 +1,134 @@
+/*
+ * 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.util;
+
+import com.android.SdkConstants;
+import com.google.common.collect.Lists;
+import com.intellij.psi.xml.XmlTag;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Util functions for searching deep links.
+ * <p/>
+ * For more information, see
+ * http://developer.android.com/training/app-indexing/deep-linking.html
+ */
+public final class DeepLinkUtils {
+ private static final String TAG_ACTION = "action";
+ private static final String TAG_CATEGORY = "category";
+
+ private static final String ACTION_VIEW = "android.intent.action.VIEW";
+ private static final String CATEGORY_DEFAULT = "android.intent.category.DEFAULT";
+ private static final String CATEGORY_BROWSABLE = "android.intent.category.BROWSABLE";
+
+ /**
+ * Returns all matching deep links from a root xml tag.
+ *
+ * @param root The root xml tag, usually is the root tag of AndroidManifest.xml
+ * @return All matching deep links
+ */
+ @NotNull
+ public static List<String> getAllDeepLinks(@NotNull XmlTag root) {
+ List<XmlTag> intentFilters = ManifestUtils.searchXmlTagsByName(root, SdkConstants.TAG_INTENT_FILTER);
+ List<String> deepLinks = Lists.newArrayList();
+ for (XmlTag intentFilter : intentFilters) {
+ String deepLink = getDeepLinkFromIntentFilter(intentFilter);
+ if (deepLink != null) {
+ deepLinks.add(deepLink);
+ }
+ }
+ return deepLinks;
+ }
+
+ /**
+ * Returns the deep link within an intent filter xml tag.
+ */
+ @Nullable
+ private static String getDeepLinkFromIntentFilter(@NotNull XmlTag intentFilter) {
+ // Check action.
+ List<XmlTag> actions = ManifestUtils.searchXmlTagsByName(intentFilter, TAG_ACTION);
+ boolean hasActionView = false;
+ for (XmlTag action : actions) {
+ String name = action.getAttributeValue(SdkConstants.ATTR_NAME, SdkConstants.NS_RESOURCES);
+ if (name != null && name.equals(ACTION_VIEW)) {
+ hasActionView = true;
+ break;
+ }
+ }
+ if (!hasActionView) {
+ return null;
+ }
+
+ // Check category
+ List<XmlTag> categories = ManifestUtils.searchXmlTagsByName(intentFilter, TAG_CATEGORY);
+ boolean hasDefaultCategory = false;
+ boolean hasBrowsableCategory = false;
+ for (XmlTag category : categories) {
+ String name = category.getAttributeValue(SdkConstants.ATTR_NAME, SdkConstants.NS_RESOURCES);
+ if (name != null && name.equals(CATEGORY_DEFAULT)) {
+ hasDefaultCategory = true;
+ }
+ else if (name != null && name.equals(CATEGORY_BROWSABLE)) {
+ hasBrowsableCategory = true;
+ }
+ }
+ if (!hasDefaultCategory || !hasBrowsableCategory) {
+ return null;
+ }
+
+ // Parse deep link
+ List<XmlTag> datas = ManifestUtils.searchXmlTagsByName(intentFilter, SdkConstants.TAG_DATA);
+ String scheme = null, host = null, pathPrefix = null, path = null;
+ for (XmlTag data : datas) {
+ if (scheme == null) {
+ scheme = data.getAttributeValue(SdkConstants.ATTR_SCHEME, SdkConstants.NS_RESOURCES);
+ }
+ if (host == null) {
+ host = data.getAttributeValue(SdkConstants.ATTR_HOST, SdkConstants.NS_RESOURCES);
+ }
+ if (pathPrefix == null) {
+ pathPrefix = data.getAttributeValue(SdkConstants.ATTR_PATH_PREFIX, SdkConstants.NS_RESOURCES);
+ }
+ if (path == null) {
+ path = data.getAttributeValue(SdkConstants.ATTR_PATH, SdkConstants.NS_RESOURCES);
+ }
+ }
+
+ // Build a deep link, which should look something like
+ // "http://", "http://host", "http://host/path-prefix" or "http://host/path.html".
+ // Incomplete deep links are OK since we only want to generate what we can,
+ // and AppIndexingApiDetector will issue errors for users to fix bad deep links.
+ if (scheme != null) {
+ StringBuilder buf = new StringBuilder(scheme);
+ buf.append("://");
+ if (host != null) {
+ buf.append(host);
+ if (path != null && path.startsWith("/")) {
+ buf.append(path);
+ }
+ else if (pathPrefix != null && pathPrefix.startsWith("/")) {
+ buf.append(pathPrefix);
+ }
+ }
+ return buf.toString();
+ }
+ return null;
+ }
+}
+
diff --git a/src/com/google/appindexing/util/ManifestUtils.java b/src/com/google/appindexing/util/ManifestUtils.java
new file mode 100644
index 0000000..a373df7
--- /dev/null
+++ b/src/com/google/appindexing/util/ManifestUtils.java
@@ -0,0 +1,92 @@
+/*
+ * 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.util;
+
+import com.google.common.collect.Lists;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.XmlRecursiveElementVisitor;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.psi.xml.XmlFile;
+
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Util functions for getting AndroidManifest.xml file or tags within the file.
+ */
+public final class ManifestUtils {
+ /**
+ * Returns the file of AndroidManifest.xml.
+ */
+ @Nullable
+ public static VirtualFile getAndroidManiFest(@NotNull Module module) {
+ AndroidFacet facet = AndroidFacet.getInstance(module);
+ if (facet != null) {
+ File file = facet.getMainSourceProvider().getManifestFile();
+ if (file != null) {
+ return LocalFileSystem.getInstance().findFileByIoFile(file);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the file of AndroidManifest.xml with type of XmlFile.
+ */
+ @Nullable
+ public static XmlFile getAndroidManifestPsi(@NotNull Module module) {
+ VirtualFile manifest = getAndroidManiFest(module);
+ if (manifest != null) {
+ PsiFile psiFile = PsiManager.getInstance(module.getProject()).findFile(manifest);
+ if (psiFile instanceof XmlFile) {
+ return (XmlFile)psiFile;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Searches xml tags with a specific tag name within a root tag.
+ *
+ * @param root The root tag to search in.
+ * @param tagName The tag name.
+ * @return All the xml tags with the name.
+ */
+ @NotNull
+ public static List<XmlTag> searchXmlTagsByName(@NotNull XmlTag root, @NotNull final String tagName) {
+ final List<XmlTag> tags = Lists.newArrayList();
+ root.accept(new XmlRecursiveElementVisitor() {
+ @Override
+ public void visitXmlTag(XmlTag tag) {
+ super.visitXmlTag(tag);
+ if (tag.getName().equalsIgnoreCase(tagName)) {
+ tags.add(tag);
+ }
+ }
+ });
+ return tags;
+ }
+}
+