blob: 0de4b5b8b2e1fabfa706ceb9ebc72af1d918df3f [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.instantapp;
import com.google.common.annotations.VisibleForTesting;
import com.android.ide.common.rendering.api.ResourceNamespace;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.ResourceValueImpl;
import com.android.ide.common.resources.ResourceResolver;
import com.android.resources.ResourceType;
import com.android.tools.idea.configurations.ConfigurationManager;
import com.android.tools.idea.model.MergedManifestManager;
import com.android.tools.lint.checks.AndroidPatternMatcher;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.TreeMultimap;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.tools.lint.checks.AndroidPatternMatcher.*;
import static com.android.xml.AndroidManifest.NODE_DATA;
import static com.android.xml.AndroidManifest.NODE_INTENT;
import static com.intellij.openapi.util.text.StringUtil.isEmpty;
import static com.intellij.openapi.util.text.StringUtil.isNotEmpty;
public final class InstantAppUrlFinder {
@NotNull private final Collection<Element> myActivities;
@NotNull private final AttributesResolver myResolver;
public InstantAppUrlFinder(@NotNull Module module) {
this(new AttributesResolver(module), MergedManifestManager.getSnapshot(module).getActivities());
}
@VisibleForTesting
InstantAppUrlFinder(@NotNull AttributesResolver resolver, @NotNull Collection<Element> activities) {
myResolver = resolver;
myActivities = activities;
}
/**
* Returns all instant app URLs found in this manifest
*/
@NotNull
public ImmutableCollection<String> getAllUrls() {
TreeMultimap<Integer, String> allUrls = TreeMultimap.create();
for (Element activity : myActivities) {
Node node = activity.getFirstChild();
while (node != null) {
InstantAppIntentFilterWrapper wrapper = new InstantAppIntentFilterWrapper(myResolver, node);
UrlData urlData = wrapper.getUrlData();
if (urlData.isValid()) {
allUrls.put(wrapper.getOrder(), urlData.getUrl());
}
node = node.getNextSibling();
}
}
return ImmutableList.copyOf(allUrls.values());
}
/**
* Gets the default URL for this manifest. Returns an empty string if no URLs are found.
*
* @return
*/
@NotNull
public String getDefaultUrl() {
ImmutableCollection<String> urls = getAllUrls();
if (!urls.isEmpty()) {
return urls.iterator().next();
}
return "";
}
public boolean matchesUrl(@NotNull String url) {
for (Element activity : myActivities) {
Node node = activity.getFirstChild();
while (node != null) {
InstantAppIntentFilterWrapper wrapper = new InstantAppIntentFilterWrapper(myResolver, node);
if (wrapper.matchesUrl(url)) {
return true;
}
node = node.getNextSibling();
}
}
return false;
}
@VisibleForTesting
static class InstantAppIntentFilterWrapper {
@NotNull AttributesResolver myResolver;
@Nullable/*No valid element*/ private final Element myElement;
private final int myOrder;
@VisibleForTesting
InstantAppIntentFilterWrapper(@NotNull AttributesResolver resolver, @NotNull Node node) {
Element element;
int order;
try {
element = getElement(node);
order = getOrder(element);
}
catch (IllegalArgumentException unused) {
element = null;
order = -1;
}
myElement = element;
myOrder = order;
myResolver = resolver;
}
@NotNull
@VisibleForTesting
static Element getElement(@NotNull Node node) {
if (node.getNodeType() == Node.ELEMENT_NODE && NODE_INTENT.equals(node.getNodeName())) {
return (Element)node;
}
else {
throw new IllegalArgumentException();
}
}
@VisibleForTesting
static int getOrder(@NotNull Element element) {
String orderValue = element.getAttributeNS(ANDROID_URI, "order");
if (isNotEmpty(orderValue)) {
try {
return Integer.parseUnsignedInt(orderValue);
}
catch (NumberFormatException unused) {
throw new IllegalArgumentException();
}
}
return 0;
}
@NotNull
UrlData getUrlData() {
UrlData urlData = new UrlData(myResolver);
if (myElement != null) {
Node node = myElement.getFirstChild();
while (node != null) {
urlData.addFromNode(node);
node = node.getNextSibling();
}
}
return urlData;
}
int getOrder() {
return myOrder;
}
boolean matchesUrl(@NotNull String url) {
return getUrlData().matchesUrl(url);
}
}
@VisibleForTesting
static class UrlData {
@NotNull private AttributesResolver myResolver;
@NotNull private final Collection<String> mySchemes = new HashSet<>();
@NotNull private final Collection<String> myHosts = new HashSet<>();
@NotNull private final Collection<String> myPaths = new HashSet<>();
@NotNull private final Collection<String> myPathPrefixes = new HashSet<>();
@NotNull private final Collection<String> myPathPatterns = new HashSet<>();
// Documentation here: https://developer.android.com/guide/topics/manifest/data-element.html
// port and mimeType should be ignored.
@VisibleForTesting
UrlData(@NotNull AttributesResolver resolver) {
myResolver = resolver;
}
@VisibleForTesting
// Test only
void addFromStrings(@NotNull String scheme, @NotNull String host, @NotNull String path, @NotNull String pathPrefix, @NotNull String pathPattern) {
addTo(mySchemes, scheme);
addTo(myHosts, host);
addTo(myPaths, path);
addTo(myPathPrefixes, pathPrefix);
addTo(myPathPatterns, pathPattern);
}
@VisibleForTesting
void addFromNode(@NotNull Node node) {
if (node.getNodeType() == Node.ELEMENT_NODE && NODE_DATA.equals(node.getNodeName())) {
Element element = (Element)node;
addTo(mySchemes, myResolver.resolveResource("scheme", element.getAttributeNS(ANDROID_URI, "scheme")));
addTo(myHosts, myResolver.resolveResource("host", element.getAttributeNS(ANDROID_URI, "host")));
addTo(myPaths, myResolver.resolveResource("path", element.getAttributeNS(ANDROID_URI, "path")));
addTo(myPathPrefixes, myResolver.resolveResource("pathPrefix", element.getAttributeNS(ANDROID_URI, "pathPrefix")));
addTo(myPathPatterns, myResolver.resolveResource("pathPattern", element.getAttributeNS(ANDROID_URI, "pathPattern")));
}
}
private static void addTo(@NotNull Collection<String> collection, @Nullable String string) {
if (isNotEmpty(string)) {
collection.add(string);
}
}
@NotNull
@VisibleForTesting
static String convertPatternToExample(@NotNull String pattern) {
return pattern.replace(".*", "example");
}
@VisibleForTesting
boolean isValid() {
return !mySchemes.isEmpty() && !myHosts.isEmpty() && getEffectivePath().startsWith("/");
}
@NotNull
private String getEffectivePath() {
String path = myPaths.isEmpty() ? "" : myPaths.iterator().next();
if (isEmpty(path)) {
path = myPathPrefixes.isEmpty() ? "" : myPathPrefixes.iterator().next() + "/.*";
}
if (isEmpty(path)) {
path = myPathPatterns.isEmpty() ? "" : myPathPatterns.iterator().next();
}
return isNotEmpty(path) ? path : "/";
}
@NotNull
@VisibleForTesting
String getUrl() {
if (!isValid()) {
return "";
}
String scheme = mySchemes.iterator().next();
String host = myHosts.iterator().next();
return String.format("%s://%s%s", scheme, host, convertPatternToExample(getEffectivePath()));
}
@VisibleForTesting
boolean matchesUrl(@NotNull String url) {
if (!isValid()) {
return false;
}
boolean schemeMatched = false;
for (String scheme : mySchemes) {
if (url.startsWith(scheme + "://")) {
url = url.replaceFirst(scheme + "://", "");
schemeMatched = true;
break;
}
}
if (!schemeMatched) {
return false;
}
boolean hostMatched = false;
for (String host : myHosts) {
if (url.startsWith(host)) {
url = url.replaceFirst(host, "");
hostMatched = true;
break;
}
}
if (!hostMatched) {
return false;
}
for (String path : myPaths) {
if (isNotEmpty(path) && new AndroidPatternMatcher(path, PATTERN_LITERAL).match(url)) {
return true;
}
}
for (String pathPrefix : myPathPrefixes) {
if (isNotEmpty(pathPrefix) && new AndroidPatternMatcher(pathPrefix, PATTERN_PREFIX).match(url)) {
return true;
}
}
for (String pathPattern : myPathPatterns) {
if (isNotEmpty(pathPattern) && new AndroidPatternMatcher(pathPattern, PATTERN_SIMPLE_GLOB).match(url)) {
return true;
}
}
if (!myPaths.isEmpty() || !myPathPrefixes.isEmpty() || !myPathPatterns.isEmpty()) {
return false;
}
return url.isEmpty() || url.compareTo("/") == 0;
}
}
@VisibleForTesting
static class AttributesResolver {
@Nullable private final ResourceResolver myResourceResolver;
private AttributesResolver(@NotNull Module module) {
List<VirtualFile> manifestFiles = MergedManifestManager.getSnapshot(module).getManifestFiles();
myResourceResolver = manifestFiles == null || manifestFiles.isEmpty()
? null
: ConfigurationManager.getOrCreateInstance(module).getConfiguration(manifestFiles.get(0)).getResourceResolver();
}
@Nullable
String resolveResource(@NotNull String name, @Nullable String value) {
if (myResourceResolver == null) {
return value;
}
ResourceValue resolvedResource = myResourceResolver.resolveResValue(
new ResourceValueImpl(ResourceNamespace.RES_AUTO, ResourceType.STRING, name, value));
return resolvedResource == null ? value : resolvedResource.getValue();
}
}
}