/* * Copyright (C) 2012 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.motorola.studio.android.model.manifest; import java.io.IOException; import java.io.StringWriter; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.xml.serialize.OutputFormat; import org.apache.xml.serialize.XMLSerializer; import org.eclipse.core.runtime.IStatus; import org.eclipse.jface.text.IDocument; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import com.motorola.studio.android.common.exception.AndroidException; import com.motorola.studio.android.common.log.StudioLogger; import com.motorola.studio.android.common.utilities.AndroidStatus; import com.motorola.studio.android.common.utilities.i18n.UtilitiesNLS; import com.motorola.studio.android.model.manifest.dom.AbstractBuildingBlockNode; import com.motorola.studio.android.model.manifest.dom.ActionNode; import com.motorola.studio.android.model.manifest.dom.ActivityNode; import com.motorola.studio.android.model.manifest.dom.AndroidManifestNode; import com.motorola.studio.android.model.manifest.dom.AndroidManifestNode.NodeType; import com.motorola.studio.android.model.manifest.dom.ApplicationNode; import com.motorola.studio.android.model.manifest.dom.CommentNode; import com.motorola.studio.android.model.manifest.dom.IntentFilterNode; import com.motorola.studio.android.model.manifest.dom.ManifestNode; import com.motorola.studio.android.model.manifest.parser.AndroidManifestParser; /** * Class that represents an AndroidManifest.xml file */ @SuppressWarnings("deprecation") public class AndroidManifestFile extends AndroidManifestParser { /** * Adds an AndroidManifestNode to the file * * @param node The node to be added */ public void addNode(AndroidManifestNode node) { if ((node != null) && !rootNodes.contains(node)) { rootNodes.add(node); } } /** * Removes an AndroidManifestNode from the file * * @param node The node to be removed */ public void removeNode(AndroidManifestNode node) { if ((node != null) && !rootNodes.contains(node)) { rootNodes.add(node); } } /** * Retrieves an array containing all nodes present on xml root. * If the file is valid, there will be only one node, the <manifest> node * * @return an array containing all nodes present on xml root. */ public AndroidManifestNode[] getNodes() { AndroidManifestNode[] nodes = new AndroidManifestNode[rootNodes.size()]; nodes = rootNodes.toArray(nodes); return nodes; } /** * Retrieves the <manifest> node * * @return the <manifest> node */ public ManifestNode getManifestNode() { ManifestNode manifestNode = null; for (AndroidManifestNode node : rootNodes) { if (node.getNodeType() == NodeType.Manifest) { manifestNode = (ManifestNode) node; break; } } return manifestNode; } /** * Retrieves the <application> node from the manifest file * * @return The <application> node of the manifest file. */ public ApplicationNode getApplicationNode() { // Retrieve <manifest> node and return application node return getManifestNode().getApplicationNode(); } /** * Retrieves a building block node, which can be of the following types: * NodeType.Activity * NodeType.Provider * NodeType.Receiver * NodeType.Service * * @param type The NodeType. * @param androidName The android:name property value. Should be the fully qualified name of the building block class. * For example, for the Activity class "Test" located in the package "com.motorola", the androidName parameter should be "com.motorola.Teste" * * @return A AbstractBuildingBlockNode that represents the building block. If no matching node is found or * the type passed is invalid, null is returned. */ public AbstractBuildingBlockNode getBuildingBlockNode(NodeType type, String androidName) { // Result AbstractBuildingBlockNode resultNode = null; // Candidate list of nodes to iterate through List<AbstractBuildingBlockNode> candidateList = new LinkedList<AbstractBuildingBlockNode>(); // Retrieve the Manifest node to check the default package ManifestNode manifestNode = getManifestNode(); String manifestPackage = manifestNode.getNodeProperties().get(PROP_PACKAGE); // Compare the qualified name from the parameter with the manifest package. If equal, we can use the class name for comparison purposes. String androidNamePackage = androidName.substring(0, androidName.lastIndexOf('.')); String shortAndroidName = new String(); if (manifestPackage.equals(androidNamePackage)) { shortAndroidName = androidName.substring(androidName.lastIndexOf('.')); } // Retrieve the application node ApplicationNode applicationNode = getApplicationNode(); // Check the building block type switch (type) { case Activity: candidateList.addAll(applicationNode.getActivityNodes()); break; case Provider: candidateList.addAll(applicationNode.getProviderNodes()); break; case Receiver: candidateList.addAll(applicationNode.getReceiverNodes()); break; case Service: candidateList.addAll(applicationNode.getServiceNodes()); break; default: break; } // Search the candidate list for the target node for (AbstractBuildingBlockNode node : candidateList) { // In the case that shortAndroidName is not null or empty, we check if it's like that in the manifest first if ((shortAndroidName != null) && (shortAndroidName.length() > 0)) { if (node.getNodeProperties().get(PROP_NAME).equals(shortAndroidName)) { resultNode = node; break; } } if (node.getNodeProperties().get(PROP_NAME).equals(androidName)) { // We found the node! resultNode = node; break; } } return resultNode; } /** * Retrieves an IDocument object containing the xml content for the file * * @return an IDocument object containing the xml content for the file */ public IDocument getContent() throws AndroidException { IDocument document = null; DocumentBuilder documentBuilder = null; try { documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); } catch (ParserConfigurationException e) { StudioLogger.error(AndroidManifestFile.class, UtilitiesNLS.EXC_AndroidManifestFile_ErrorCreatingTheDocumentBuilder, e); throw new AndroidException( UtilitiesNLS.EXC_AndroidManifestFile_ErrorCreatingTheDocumentBuilder); } Document xmlDocument = documentBuilder.newDocument(); for (AndroidManifestNode node : rootNodes) { addNode(xmlDocument, null, node); } document = new org.eclipse.jface.text.Document(getXmlContent(xmlDocument)); return document; } /** * Recursive function to build a XML file from AndroidManifestNode objects * * @param xmlDocument The XML Document * @param xmlParentNode The XML parent node * @param nodeToAdd The AndroidManifestNode to be added */ private void addNode(Document xmlDocument, Node xmlParentNode, AndroidManifestNode nodeToAdd) { Node xmlNode; if (nodeToAdd instanceof CommentNode) { CommentNode commentNode = (CommentNode) nodeToAdd; xmlNode = xmlDocument.createComment(commentNode.getComment()); } else { xmlNode = xmlDocument.createElement(nodeToAdd.getNodeName()); Map<String, String> attributes = nodeToAdd.getNodeProperties(); Map<String, String> unknownAttributes = nodeToAdd.getNodeUnknownProperties(); AndroidManifestNode[] children = nodeToAdd.getChildren(); AndroidManifestNode[] unknown = nodeToAdd.getUnkownChildren(); // Adds valid attributes if ((attributes != null) && (attributes.size() > 0)) { NamedNodeMap xmlAttributes = xmlNode.getAttributes(); for (String attrName : attributes.keySet()) { Attr attr = xmlDocument.createAttribute(attrName); attr.setValue(attributes.get(attrName)); xmlAttributes.setNamedItem(attr); } } // Adds invalid attributes if ((unknownAttributes != null) && (unknownAttributes.size() > 0)) { NamedNodeMap xmlAttributes = xmlNode.getAttributes(); for (String attrName : unknownAttributes.keySet()) { Attr attr = xmlDocument.createAttribute(attrName); attr.setNodeValue(unknownAttributes.get(attrName)); xmlAttributes.setNamedItem(attr); } } // Adds known child nodes for (AndroidManifestNode child : children) { addNode(xmlDocument, xmlNode, child); } // Adds unknown child nodes for (AndroidManifestNode child : unknown) { addNode(xmlDocument, xmlNode, child); } } if (xmlParentNode == null) { xmlDocument.appendChild(xmlNode); } else { xmlParentNode.appendChild(xmlNode); } } /** * Creates the XML content from a XML Document * * @param xmlDocument The XML Document * @return a String object containing the XML content */ private String getXmlContent(Document xmlDocument) throws AndroidException { // Despite Xerces is deprecated, its formatted xml source output works // better than W3C xml output classes OutputFormat outputFormat = new OutputFormat(); XMLSerializer xmlSerializer = new XMLSerializer(); StringWriter writer = new StringWriter(); String content = null; outputFormat.setEncoding("UTF-8"); outputFormat.setLineSeparator(System.getProperty("line.separator")); outputFormat.setIndenting(true); xmlSerializer.setOutputCharStream(writer); xmlSerializer.setOutputFormat(outputFormat); try { xmlSerializer.serialize(xmlDocument); content = writer.toString(); } catch (IOException e) { StudioLogger.error(AndroidManifestFile.class, UtilitiesNLS.EXC_AndroidManifestFile_ErrorFormattingTheXMLOutput, e); throw new AndroidException( UtilitiesNLS.EXC_AndroidManifestFile_ErrorFormattingTheXMLOutput); } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { //Do nothing. } } } return content; } /** * Gets all file problems: Errors and Warnings * * @return all file problems */ public IStatus[] getProblems() { ManifestNode manifestNode = getManifestNode(); IStatus[] errors; if (manifestNode == null) { errors = new IStatus[] { new AndroidStatus( IStatus.ERROR, UtilitiesNLS.ERR_AndroidManifestFile_TheFileAndroidManifestXmlIsMalFormed) }; } else { errors = getManifestNode().getRecursiveNodeErrors(); } return errors; } /** * Gets all file errors * * @return all file errors */ public IStatus[] getErrors() { List<IStatus> errors = new LinkedList<IStatus>(); for (IStatus status : getProblems()) { if (status.getSeverity() == IStatus.ERROR) { errors.add(status); } } return errors.toArray(new IStatus[0]); } /** * Checks if the file has errors in the model * * @return true if the file has errors in the model and false otherwise */ public boolean hasErrors() { boolean hasErrors = false; for (IStatus status : getProblems()) { if (status.getSeverity() == IStatus.ERROR) { hasErrors = true; break; } } return hasErrors; } public AndroidManifestNode getNode(NodeType nodeType) { AndroidManifestNode requiredNode = null; AndroidManifestNode[] manifestChildren = null; for (AndroidManifestNode node : rootNodes) { if (node instanceof ManifestNode) { manifestChildren = ((ManifestNode) node).getChildren(); break; } } if ((manifestChildren != null) && (manifestChildren.length > 0)) { for (AndroidManifestNode manifestChild : manifestChildren) { if (manifestChild.getNodeType().equals(nodeType)) { requiredNode = manifestChild; break; } } } return requiredNode; } /** * This method sets the main activity of and android project be the class identified by {@code className}. * * @param className The name of the class to be set as the main activity. * @param isMainActivity If true, the activity will be set as main activity. If false, the activity will no longer be a main activity. * @return True if the activity exist, is declared on the manifest and was successfully set as the main activity. False otherwise. * */ public boolean setAsMainActivity(String className, boolean isMainActivity) { boolean result = false; List<ActivityNode> activityNodes = getApplicationNode().getActivityNodes(); for (ActivityNode activityNode : activityNodes) { if (activityNode.getName().equals(className)) { result = activityNode.setAsMainActivity(isMainActivity); break; } } return result; } /** * Convenience method that returns the main activity of the application. * The main activity is the one declared with the intent filter * <action android:name="android.intent.action.MAIN"/> * If more than one main activity is declared, then the first one declared is returned. * This behavior follows the android behavior to choose the main activity. * */ public ActivityNode getMainActivity() { ActivityNode mainActivity = null; ApplicationNode appNode = getApplicationNode(); List<ActivityNode> activities = appNode.getActivityNodes(); for (ActivityNode activity : activities) { for (IntentFilterNode intent : activity.getIntentFilterNodes()) { for (ActionNode actionNode : intent.getActionNodes()) { if (actionNode.getNodeProperties().get("android:name") .equals("android.intent.action.MAIN")) { mainActivity = activity; break; } } if (mainActivity != null) { break; } } if (mainActivity != null) { break; } } return mainActivity; } }