/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php * * 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.ide.eclipse.adt.internal.editors.manifest; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_NAME; import static com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors.USES_PERMISSION; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; import com.android.ide.eclipse.adt.internal.editors.manifest.pages.ApplicationPage; import com.android.ide.eclipse.adt.internal.editors.manifest.pages.InstrumentationPage; import com.android.ide.eclipse.adt.internal.editors.manifest.pages.OverviewPage; import com.android.ide.eclipse.adt.internal.editors.manifest.pages.PermissionPage; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient; import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IMarkerDelta; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.PartInitException; import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import java.util.Collection; import java.util.List; /** * Multi-page form editor for AndroidManifest.xml. */ @SuppressWarnings("restriction") public final class ManifestEditor extends AndroidXmlEditor { public static final String ID = AdtConstants.EDITORS_NAMESPACE + ".manifest.ManifestEditor"; //$NON-NLS-1$ private final static String EMPTY = ""; //$NON-NLS-1$ /** Root node of the UI element hierarchy */ private UiElementNode mUiManifestNode; /** The Application Page tab */ private ApplicationPage mAppPage; /** The Overview Manifest Page tab */ private OverviewPage mOverviewPage; /** The Permission Page tab */ private PermissionPage mPermissionPage; /** The Instrumentation Page tab */ private InstrumentationPage mInstrumentationPage; private IFileListener mMarkerMonitor; /** * Creates the form editor for AndroidManifest.xml. */ public ManifestEditor() { super(); addDefaultTargetListener(); } @Override public void dispose() { super.dispose(); GlobalProjectMonitor.getMonitor().removeFileListener(mMarkerMonitor); } @Override public void activated() { super.activated(); clearActionBindings(false); } @Override public void deactivated() { super.deactivated(); updateActionBindings(); } @Override protected void pageChange(int newPageIndex) { super.pageChange(newPageIndex); if (newPageIndex == mTextPageIndex) { updateActionBindings(); } else { clearActionBindings(false); } } @Override protected int getPersistenceCategory() { return CATEGORY_MANIFEST; } /** * Return the root node of the UI element hierarchy, which here * is the "manifest" node. */ @Override public UiElementNode getUiRootNode() { return mUiManifestNode; } /** * Returns the Manifest descriptors for the file being edited. */ public AndroidManifestDescriptors getManifestDescriptors() { AndroidTargetData data = getTargetData(); if (data != null) { return data.getManifestDescriptors(); } return null; } // ---- Base Class Overrides ---- /** * Returns whether the "save as" operation is supported by this editor. * <p/> * Save-As is a valid operation for the ManifestEditor since it acts on a * single source file. * * @see IEditorPart */ @Override public boolean isSaveAsAllowed() { return true; } @Override public void doSave(IProgressMonitor monitor) { // Look up the current (pre-save) values of minSdkVersion and targetSdkVersion int prevMinSdkVersion = -1; int prevTargetSdkVersion = -1; IProject project = null; ManifestInfo info = null; try { project = getProject(); if (project != null) { info = ManifestInfo.get(project); prevMinSdkVersion = info.getMinSdkVersion(); prevTargetSdkVersion = info.getTargetSdkVersion(); info.clear(); } } catch (Throwable t) { // We don't expect exceptions from the above calls, but we *really* // need to make sure that nothing can prevent the save function from // getting called! AdtPlugin.log(t, null); } // Actually save super.doSave(monitor); // If the target/minSdkVersion has changed, clear all lint warnings (since many // of them are tied to the min/target sdk levels), in order to avoid showing stale // results try { if (info != null) { int newMinSdkVersion = info.getMinSdkVersion(); int newTargetSdkVersion = info.getTargetSdkVersion(); if (newMinSdkVersion != prevMinSdkVersion || newTargetSdkVersion != prevTargetSdkVersion) { assert project != null; EclipseLintClient.clearMarkers(project); } } } catch (Throwable t) { AdtPlugin.log(t, null); } } /** * Creates the various form pages. */ @Override protected void createFormPages() { try { addPage(mOverviewPage = new OverviewPage(this)); addPage(mAppPage = new ApplicationPage(this)); addPage(mPermissionPage = new PermissionPage(this)); addPage(mInstrumentationPage = new InstrumentationPage(this)); } catch (PartInitException e) { AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$ } } /* (non-java doc) * Change the tab/title name to include the project name. */ @Override protected void setInput(IEditorInput input) { super.setInput(input); IFile inputFile = getInputFile(); if (inputFile != null) { startMonitoringMarkers(); setPartName(String.format("%1$s Manifest", inputFile.getProject().getName())); } } /** * Processes the new XML Model, which XML root node is given. * * @param xml_doc The XML document, if available, or null if none exists. */ @Override protected void xmlModelChanged(Document xml_doc) { // create the ui root node on demand. initUiRootNode(false /*force*/); loadFromXml(xml_doc); } private void loadFromXml(Document xmlDoc) { mUiManifestNode.setXmlDocument(xmlDoc); Node node = getManifestXmlNode(xmlDoc); if (node != null) { // Refresh the manifest UI node and all its descendants mUiManifestNode.loadFromXmlNode(node); } } private Node getManifestXmlNode(Document xmlDoc) { if (xmlDoc != null) { ElementDescriptor manifestDesc = mUiManifestNode.getDescriptor(); String manifestXmlName = manifestDesc == null ? null : manifestDesc.getXmlName(); assert manifestXmlName != null; if (manifestXmlName != null) { Node node = xmlDoc.getDocumentElement(); if (node != null && manifestXmlName.equals(node.getNodeName())) { return node; } for (node = xmlDoc.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE && manifestXmlName.equals(node.getNodeName())) { return node; } } } } return null; } private void onDescriptorsChanged() { IStructuredModel model = getModelForRead(); if (model != null) { try { Node node = getManifestXmlNode(getXmlDocument(model)); mUiManifestNode.reloadFromXmlNode(node); } finally { model.releaseFromRead(); } } if (mOverviewPage != null) { mOverviewPage.refreshUiApplicationNode(); } if (mAppPage != null) { mAppPage.refreshUiApplicationNode(); } if (mPermissionPage != null) { mPermissionPage.refreshUiNode(); } if (mInstrumentationPage != null) { mInstrumentationPage.refreshUiNode(); } } /** * Reads and processes the current markers and adds a listener for marker changes. */ private void startMonitoringMarkers() { final IFile inputFile = getInputFile(); if (inputFile != null) { updateFromExistingMarkers(inputFile); mMarkerMonitor = new IFileListener() { @Override public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, int kind, @Nullable String extension, int flags, boolean isAndroidProject) { if (isAndroidProject && file.equals(inputFile)) { processMarkerChanges(markerDeltas); } } }; GlobalProjectMonitor.getMonitor().addFileListener( mMarkerMonitor, IResourceDelta.CHANGED); } } /** * Processes the markers of the specified {@link IFile} and updates the error status of * {@link UiElementNode}s and {@link UiAttributeNode}s. * @param inputFile the file being edited. */ private void updateFromExistingMarkers(IFile inputFile) { try { // get the markers for the file IMarker[] markers = inputFile.findMarkers( AdtConstants.MARKER_ANDROID, true, IResource.DEPTH_ZERO); AndroidManifestDescriptors desc = getManifestDescriptors(); if (desc != null) { ElementDescriptor appElement = desc.getApplicationElement(); if (appElement != null && mUiManifestNode != null) { UiElementNode appUiNode = mUiManifestNode.findUiChildNode( appElement.getXmlName()); List<UiElementNode> children = appUiNode.getUiChildren(); for (IMarker marker : markers) { processMarker(marker, children, IResourceDelta.ADDED); } } } } catch (CoreException e) { // findMarkers can throw an exception, in which case, we'll do nothing. } } /** * Processes a {@link IMarker} change. * @param markerDeltas the list of {@link IMarkerDelta} */ private void processMarkerChanges(IMarkerDelta[] markerDeltas) { AndroidManifestDescriptors descriptors = getManifestDescriptors(); if (descriptors != null && descriptors.getApplicationElement() != null) { UiElementNode app_ui_node = mUiManifestNode.findUiChildNode( descriptors.getApplicationElement().getXmlName()); List<UiElementNode> children = app_ui_node.getUiChildren(); for (IMarkerDelta markerDelta : markerDeltas) { processMarker(markerDelta.getMarker(), children, markerDelta.getKind()); } } } /** * Processes a new/old/updated marker. * @param marker The marker being added/removed/changed * @param nodeList the list of activity/service/provider/receiver nodes. * @param kind the change kind. Can be {@link IResourceDelta#ADDED}, * {@link IResourceDelta#REMOVED}, or {@link IResourceDelta#CHANGED} */ private void processMarker(IMarker marker, List<UiElementNode> nodeList, int kind) { // get the data from the marker String nodeType = marker.getAttribute(AdtConstants.MARKER_ATTR_TYPE, EMPTY); if (nodeType == EMPTY) { return; } String className = marker.getAttribute(AdtConstants.MARKER_ATTR_CLASS, EMPTY); if (className == EMPTY) { return; } for (UiElementNode ui_node : nodeList) { if (ui_node.getDescriptor().getXmlName().equals(nodeType)) { for (UiAttributeNode attr : ui_node.getAllUiAttributes()) { if (attr.getDescriptor().getXmlLocalName().equals( AndroidManifestDescriptors.ANDROID_NAME_ATTR)) { if (attr.getCurrentValue().equals(className)) { if (kind == IResourceDelta.REMOVED) { attr.setHasError(false); } else { attr.setHasError(true); } return; } } } } } } /** * Creates the initial UI Root Node, including the known mandatory elements. * @param force if true, a new UiManifestNode is recreated even if it already exists. */ @Override protected void initUiRootNode(boolean force) { // The manifest UI node is always created, even if there's no corresponding XML node. if (mUiManifestNode != null && force == false) { return; } AndroidManifestDescriptors manifestDescriptor = getManifestDescriptors(); if (manifestDescriptor != null) { ElementDescriptor manifestElement = manifestDescriptor.getManifestElement(); mUiManifestNode = manifestElement.createUiNode(); mUiManifestNode.setEditor(this); // Similarly, always create the /manifest/uses-sdk followed by /manifest/application // (order of the elements now matters) ElementDescriptor element = manifestDescriptor.getUsesSdkElement(); boolean present = false; for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) { if (ui_node.getDescriptor() == element) { present = true; break; } } if (!present) { mUiManifestNode.appendNewUiChild(element); } element = manifestDescriptor.getApplicationElement(); present = false; for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) { if (ui_node.getDescriptor() == element) { present = true; break; } } if (!present) { mUiManifestNode.appendNewUiChild(element); } onDescriptorsChanged(); } else { // create a dummy descriptor/uinode until we have real descriptors ElementDescriptor desc = new ElementDescriptor("manifest", //$NON-NLS-1$ "temporary descriptors due to missing decriptors", //$NON-NLS-1$ null /*tooltip*/, null /*sdk_url*/, null /*attributes*/, null /*children*/, false /*mandatory*/); mUiManifestNode = desc.createUiNode(); mUiManifestNode.setEditor(this); } } /** * Adds the given set of permissions into the manifest file in the suitable * location * * @param permissions permission fqcn's to be added * @param show if true, show one or more of the newly added permissions */ public void addPermissions(@NonNull final List<String> permissions, final boolean show) { wrapUndoEditXmlModel("Add permissions", new Runnable() { @Override public void run() { // Ensure that the model is current: initUiRootNode(true /*force*/); UiElementNode root = getUiRootNode(); ElementDescriptor descriptor = getManifestDescriptors().getUsesPermissionElement(); boolean shown = false; for (String permission : permissions) { // Find the first permission which sorts alphabetically laster than // this permission (or the last permission, if none are after in the alphabet) // and insert it there int lastPermissionIndex = -1; int nextPermissionIndex = -1; int index = 0; for (UiElementNode sibling : root.getUiChildren()) { Node node = sibling.getXmlNode(); if (node.getNodeName().equals(USES_PERMISSION)) { lastPermissionIndex = index; String name = ((Element) node).getAttributeNS(ANDROID_URI, ATTR_NAME); if (permission.compareTo(name) < 0) { nextPermissionIndex = index; break; } } else if (node.getNodeName().equals("application")) { //$NON-NLS-1$ // permissions should come before the application element nextPermissionIndex = index; break; } index++; } if (nextPermissionIndex != -1) { index = nextPermissionIndex; } else if (lastPermissionIndex != -1) { index = lastPermissionIndex + 1; } else { index = root.getUiChildren().size(); } UiElementNode usesPermission = root.insertNewUiChild(index, descriptor); usesPermission.setAttributeValue(ATTR_NAME, ANDROID_URI, permission, true /*override*/); Node node = usesPermission.createXmlNode(); if (show && !shown) { shown = true; if (node instanceof IndexedRegion && getInputFile() != null) { IndexedRegion indexedRegion = (IndexedRegion) node; IRegion region = new Region(indexedRegion.getStartOffset(), indexedRegion.getEndOffset() - indexedRegion.getStartOffset()); try { AdtPlugin.openFile(getInputFile(), region, true /*show*/); } catch (PartInitException e) { AdtPlugin.log(e, null); } } else { show(node); } } } } }); } /** * Removes the permissions from the manifest editor * * @param permissions the permission fqcn's to be removed */ public void removePermissions(@NonNull final Collection<String> permissions) { wrapUndoEditXmlModel("Remove permissions", new Runnable() { @Override public void run() { // Ensure that the model is current: initUiRootNode(true /*force*/); UiElementNode root = getUiRootNode(); for (String permission : permissions) { for (UiElementNode sibling : root.getUiChildren()) { Node node = sibling.getXmlNode(); if (node.getNodeName().equals(USES_PERMISSION)) { String name = ((Element) node).getAttributeNS(ANDROID_URI, ATTR_NAME); if (name.equals(permission)) { sibling.deleteXmlNode(); break; } } } } } }); } }