blob: 4e89ef41486ec2a2ee09843c03ee2ffbea938c69 [file] [log] [blame]
/*
* Copyright (C) 2017 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.manifmerger
import com.android.SdkConstants
import com.android.SdkConstants.ANDROID_URI
import com.android.SdkConstants.ATTR_AUTO_VERIFY
import com.android.SdkConstants.ATTR_HOST
import com.android.SdkConstants.ATTR_NAME
import com.android.SdkConstants.ATTR_PATH
import com.android.SdkConstants.ATTR_PATH_PATTERN
import com.android.SdkConstants.ATTR_PATH_PREFIX
import com.android.SdkConstants.ATTR_PORT
import com.android.SdkConstants.ATTR_SCHEME
import com.android.SdkConstants.TAG_ACTION
import com.android.SdkConstants.TAG_CATEGORY
import com.android.SdkConstants.TAG_DATA
import com.google.common.annotations.VisibleForTesting
import com.android.ide.common.blame.SourceFilePosition
import com.android.utils.XmlUtils
import com.google.common.collect.ImmutableList
import org.w3c.dom.Element
import org.w3c.dom.NamedNodeMap
/**
* Singleton with [expandNavGraphs] method, which returns an XmlDocument with the <nav-graph>
* elements converted into the corresponding <intent-filter> elements
*/
@VisibleForTesting
object NavGraphExpander {
/**
* Return an [XmlDocument] with the <nav-graph> elements converted into the corresponding
* <intent-filter> elements
*
* @param xmlDocument the input XmlDocument whose <nav-graph> elements will be converted
* into <intent-filter> elements in the output XmlDocument.
* This method assumes that this XmlDocument matches the underlying
* XmlDocument.xml; e.g., if <nav-graph> elements have been added
* to the DOM document since this XmlDocument was created, the new
* <nav-graph> elements will not be converted.
* @param loadedNavigationMap the map of navigationId Strings to NavigationXmlDocuments,
* which determine the set of DeepLinks used to generate the
* <intent-filter> element for each <nav-graph> element
* @param mergingReportBuilder the MergingReport.Builder used for the entire merge
* @return a new XmlDocument similar to the input XmlDocument, but with any <nav-graph>
* elements replaced with corresponding <intent-filter> elements
*/
fun expandNavGraphs(
xmlDocument: XmlDocument,
loadedNavigationMap: Map<String, NavigationXmlDocument>,
mergingReportBuilder: MergingReport.Builder): XmlDocument {
expandNavGraphs(xmlDocument.rootNode, loadedNavigationMap, mergingReportBuilder)
return xmlDocument.reparse()
}
/**
* Helper method for [expandNavGraphs]. This method checks if the given xmlElement is
* an <activity> element with <nav-graph> children, and if so, it calls the [expandNavGraph]
* helper method to generate the <intent-filter> elements, and then it finally
* removes the original <nav-graph> elements.
*/
private fun expandNavGraphs(
xmlElement: XmlElement,
loadedNavigationMap: Map<String, NavigationXmlDocument>,
mergingReportBuilder: MergingReport.Builder) {
for (childElement in xmlElement.mergeableElements) {
expandNavGraphs(childElement, loadedNavigationMap, mergingReportBuilder)
}
if (xmlElement.xml.tagName != SdkConstants.TAG_ACTIVITY) {
return
}
val navGraphs = xmlElement.getAllNodesByType(ManifestModel.NodeTypes.NAV_GRAPH)
if (navGraphs.isEmpty()) {
return
}
// expand each navGraph
for (navGraph in navGraphs) {
val namedNodeMap: NamedNodeMap? = navGraph.xml.attributes
val graphValue: String? =
namedNodeMap
?.getNamedItemNS(ANDROID_URI, SdkConstants.ATTR_VALUE)
?.nodeValue
val navigationXmlId =
if (graphValue?.startsWith(SdkConstants.NAVIGATION_PREFIX) == true)
graphValue.substring(SdkConstants.NAVIGATION_PREFIX.length)
else null
if (navigationXmlId == null) {
val nsUriPrefix =
XmlUtils.lookupNamespacePrefix(navGraph.xml, ANDROID_URI, false)
val graphName = nsUriPrefix + XmlUtils.NS_SEPARATOR + SdkConstants.ATTR_VALUE
mergingReportBuilder.addMessage(
SourceFilePosition(xmlElement.document.sourceFile, xmlElement.position),
MergingReport.Record.Severity.ERROR,
"Missing or malformed attribute in <nav-graph> element. " +
"Android manifest <nav-graph> element must contain a $graphName " +
"attribute with a value beginning " +
"with \"${SdkConstants.NAVIGATION_PREFIX}\".")
return
}
expandNavGraph(xmlElement, navigationXmlId, loadedNavigationMap, mergingReportBuilder)
}
// then remove each navGraph
for (navGraph in navGraphs) {
xmlElement.xml.removeChild(navGraph.xml)
mergingReportBuilder.actionRecorder.recordNodeAction(
navGraph, Actions.ActionType.CONVERTED)
}
}
/**
* Helper method for [expandNavGraphs]. This method calls [findDeepLinks] to get all the
* [DeepLink]s associated with a given navigationId, then generates the corresponding
* <intent-filter> elements.
*/
private fun expandNavGraph(
xmlElement: XmlElement,
navigationXmlId: String,
loadedNavigationMap: Map<String, NavigationXmlDocument>,
mergingReportBuilder: MergingReport.Builder) {
val deepLinks = try {
findDeepLinks(navigationXmlId, loadedNavigationMap)
} catch (e: NavGraphException) {
mergingReportBuilder.addMessage(
SourceFilePosition(xmlElement.document.sourceFile, xmlElement.position),
MergingReport.Record.Severity.ERROR,
e.message ?: "Error finding deep links.")
return
}
val actionRecorder = mergingReportBuilder.actionRecorder
val deepLinkGroups = deepLinks.groupBy { getDeepLinkUriBody(it, false) }
for (deepLinkGroup in deepLinkGroups.values) {
val deepLink = deepLinkGroup.first()
// first create <intent-filter> element
val intentFilterElement =
addChildElement(SdkConstants.TAG_INTENT_FILTER, xmlElement.xml)
if (deepLink.isAutoVerify) {
addAttribute(ANDROID_URI, ATTR_AUTO_VERIFY, "true", intentFilterElement)
}
// then add children elements to <intent-filter> element
val childElementDataList: MutableList<ChildElementData> =
mutableListOf(
ChildElementData(
TAG_ACTION, ATTR_NAME, "android.intent.action.VIEW"),
ChildElementData(
TAG_CATEGORY, ATTR_NAME, "android.intent.category.DEFAULT"),
ChildElementData(
TAG_CATEGORY, ATTR_NAME, "android.intent.category.BROWSABLE"))
for (scheme in deepLinkGroup.flatMap { it.schemes }.toSet()) {
childElementDataList.add(ChildElementData(TAG_DATA, ATTR_SCHEME, scheme))
}
if (deepLink.host != null) {
childElementDataList.add(ChildElementData(TAG_DATA, ATTR_HOST, deepLink.host))
}
if (deepLink.port != -1) {
childElementDataList.add(
ChildElementData(TAG_DATA, ATTR_PORT, deepLink.port.toString()))
}
val path = deepLink.path
when {
path.substringBefore(".*").length == path.length - 2 ->
childElementDataList.add(
ChildElementData(TAG_DATA, ATTR_PATH_PREFIX, path.substringBefore(".*")))
path.contains(".*") ->
childElementDataList.add(
ChildElementData(TAG_DATA, ATTR_PATH_PATTERN, path))
else -> childElementDataList.add(ChildElementData(TAG_DATA, ATTR_PATH, path))
}
childElementDataList.forEach {
addChildElementWithSingleAttribute(
it.tagName, intentFilterElement, ANDROID_URI, it.attrName, it.attrValue)
}
// finally record all added elements and attributes
val intentFilterXmlElement = XmlElement(intentFilterElement, xmlElement.document)
for (dl in deepLinkGroup) {
recordXmlElementAddition(
intentFilterXmlElement, dl.sourceFilePosition, actionRecorder)
}
}
}
/**
* Find [DeepLink]s from referenced [NavigationXmlDocument]s and return a List of them.
*
* If duplicate [DeepLink]s are found, throws a [NavGraphException]
*/
@VisibleForTesting
@Throws(NavGraphException::class)
fun findDeepLinks(
navigationXmlId: String,
loadedNavigationMap: Map<String, NavigationXmlDocument>): List<DeepLink> {
val deepLinkList: MutableList<DeepLink> = mutableListOf()
findDeepLinks(navigationXmlId, loadedNavigationMap, deepLinkList, mutableMapOf(), hashSetOf())
return ImmutableList.copyOf(deepLinkList)
}
/**
* Find [DeepLink]s from referenced [NavigationXmlDocument]s and add them
* to the deepLinkList.
*
* If duplicate [DeepLink]s are found, throws a [NavGraphException]
*/
@Throws(NavGraphException::class)
private fun findDeepLinks(
navigationXmlId: String,
loadedNavigationMap: Map<String, NavigationXmlDocument>,
deepLinkList: MutableList<DeepLink>,
deepLinkUriMap: MutableMap<String, DeepLink>,
visitedNavigationFiles: MutableSet<String>) {
// This method tracks visitedNavigationFiles to avoid an infinite loop caused by a
// circular reference among the navigation files.
if (!visitedNavigationFiles.add(navigationXmlId)) {
throw NavGraphException(
"Illegal circular reference among navigation files when traversing navigation" +
" file references starting with navigationXmlId: $navigationXmlId")
}
val navigationXmlDocument = loadedNavigationMap[navigationXmlId]
navigationXmlDocument ?: throw NavGraphException(
"Referenced navigation file with navigationXmlId = $navigationXmlId not found")
for (deepLink in navigationXmlDocument.deepLinks) {
for (deepLinkUri in getDeepLinkUris(deepLink)) {
if (deepLinkUriMap.containsKey(deepLinkUri)) {
throw NavGraphException(
"Multiple destinations found with a deep link to $deepLinkUri")
}
deepLinkUriMap[deepLinkUri] = deepLink
}
deepLinkList.add(deepLink)
}
for (otherNavigationXmlId in navigationXmlDocument.navigationXmlIds) {
findDeepLinks(
otherNavigationXmlId,
loadedNavigationMap,
deepLinkList,
deepLinkUriMap,
visitedNavigationFiles)
}
}
private fun getDeepLinkUriBody(deepLink: DeepLink, includeQuery: Boolean): String {
val hostString = if (deepLink.host == null) "//" else "//" + deepLink.host
val portString = if (deepLink.port == -1) "" else ":" + deepLink.port
val queryString = if (deepLink.query == null || !includeQuery) "" else "?${deepLink.query}"
return hostString + portString + deepLink.path + queryString
}
/**
* Returns the list of possible URIs from a [DeepLink]
*
* Not guaranteed to return valid URI's (e.g., if a host is not specified, but
* a port is), but that's okay because we're only using the returned URI's to check
* for duplicate [DeepLink]s
*/
private fun getDeepLinkUris(deepLink: DeepLink): List<String> {
val builder: ImmutableList.Builder<String> = ImmutableList.builder()
val body = getDeepLinkUriBody(deepLink, true)
for (scheme in deepLink.schemes) {
builder.add("$scheme:$body")
}
return builder.build()
}
/** Add a new Element with a single specified Attribute to parentElement */
private fun addChildElementWithSingleAttribute(
childTagName: String,
parentElement: Element,
nsUri: String,
attrName: String,
attrValue: String) {
val childElement = addChildElement(childTagName, parentElement)
addAttribute(nsUri, attrName, attrValue, childElement)
}
/** Add a new Element with no Attributes to parentElement */
private fun addChildElement(childTagName: String, parentElement: Element): Element {
val document = parentElement.ownerDocument
val childElement = document.createElement(childTagName)
return parentElement.appendChild(childElement) as? Element ?:
throw RuntimeException(
"Unable to add $childTagName element to ${parentElement.tagName} element.")
}
/** Add a new Attribute to the element */
private fun addAttribute(nsUri: String, attrName: String, attrValue: String, element: Element) {
val prefix = XmlUtils.lookupNamespacePrefix(element, nsUri, true)
element.setAttributeNS(nsUri, prefix + XmlUtils.NS_SEPARATOR + attrName, attrValue)
}
/** Record addition of xmlElement and all of its descendants (attributes and elements) */
private fun recordXmlElementAddition(
xmlElement: XmlElement,
sourceFilePosition: SourceFilePosition,
actionRecorder: ActionRecorder) {
val nodeRecord =
Actions.NodeRecord(
Actions.ActionType.ADDED,
sourceFilePosition,
xmlElement.id,
null,
xmlElement.operationType)
actionRecorder.recordNodeAction(xmlElement, nodeRecord)
for (xmlAttribute in xmlElement.attributes) {
recordXmlAttributeAddition(xmlAttribute, sourceFilePosition, actionRecorder)
}
for (childXmlElement in xmlElement.mergeableElements) {
recordXmlElementAddition(childXmlElement, sourceFilePosition, actionRecorder)
}
}
/** Record addition of xmlAttribute */
private fun recordXmlAttributeAddition(
xmlAttribute: XmlAttribute,
sourceFilePosition: SourceFilePosition,
actionRecorder: ActionRecorder) {
val attributeRecord =
Actions.AttributeRecord(
Actions.ActionType.ADDED, sourceFilePosition, xmlAttribute.id, null, null)
actionRecorder.recordAttributeAction(xmlAttribute, attributeRecord)
}
/** class to hold data for child elements of added <intent-filter> element. */
private data class ChildElementData(
val tagName: String, val attrName: String, val attrValue: String)
/** An exception during the evaluation of an Android Manifest <nav-graph> element. */
class NavGraphException(s: String) : RuntimeException(s)
}