Home | History | Annotate | Download | only in manifest
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.editors.manifest;
     18 
     19 import static com.android.SdkConstants.ANDROID_URI;
     20 import static com.android.SdkConstants.ATTR_NAME;
     21 import static com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors.USES_PERMISSION;
     22 
     23 import com.android.annotations.NonNull;
     24 import com.android.annotations.Nullable;
     25 import com.android.ide.eclipse.adt.AdtConstants;
     26 import com.android.ide.eclipse.adt.AdtPlugin;
     27 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     28 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     29 import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
     30 import com.android.ide.eclipse.adt.internal.editors.manifest.pages.ApplicationPage;
     31 import com.android.ide.eclipse.adt.internal.editors.manifest.pages.InstrumentationPage;
     32 import com.android.ide.eclipse.adt.internal.editors.manifest.pages.OverviewPage;
     33 import com.android.ide.eclipse.adt.internal.editors.manifest.pages.PermissionPage;
     34 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
     35 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     36 import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient;
     37 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
     38 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
     39 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     40 
     41 import org.eclipse.core.resources.IFile;
     42 import org.eclipse.core.resources.IMarker;
     43 import org.eclipse.core.resources.IMarkerDelta;
     44 import org.eclipse.core.resources.IProject;
     45 import org.eclipse.core.resources.IResource;
     46 import org.eclipse.core.resources.IResourceDelta;
     47 import org.eclipse.core.runtime.CoreException;
     48 import org.eclipse.core.runtime.IProgressMonitor;
     49 import org.eclipse.jface.text.IRegion;
     50 import org.eclipse.jface.text.Region;
     51 import org.eclipse.ui.IEditorInput;
     52 import org.eclipse.ui.IEditorPart;
     53 import org.eclipse.ui.PartInitException;
     54 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
     55 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
     56 import org.w3c.dom.Document;
     57 import org.w3c.dom.Element;
     58 import org.w3c.dom.Node;
     59 
     60 import java.util.Collection;
     61 import java.util.List;
     62 
     63 /**
     64  * Multi-page form editor for AndroidManifest.xml.
     65  */
     66 @SuppressWarnings("restriction")
     67 public final class ManifestEditor extends AndroidXmlEditor {
     68 
     69     public static final String ID = AdtConstants.EDITORS_NAMESPACE + ".manifest.ManifestEditor"; //$NON-NLS-1$
     70 
     71     private final static String EMPTY = ""; //$NON-NLS-1$
     72 
     73     /** Root node of the UI element hierarchy */
     74     private UiElementNode mUiManifestNode;
     75     /** The Application Page tab */
     76     private ApplicationPage mAppPage;
     77     /** The Overview Manifest Page tab */
     78     private OverviewPage mOverviewPage;
     79     /** The Permission Page tab */
     80     private PermissionPage mPermissionPage;
     81     /** The Instrumentation Page tab */
     82     private InstrumentationPage mInstrumentationPage;
     83 
     84     private IFileListener mMarkerMonitor;
     85 
     86 
     87     /**
     88      * Creates the form editor for AndroidManifest.xml.
     89      */
     90     public ManifestEditor() {
     91         super();
     92         addDefaultTargetListener();
     93     }
     94 
     95     @Override
     96     public void dispose() {
     97         super.dispose();
     98 
     99         GlobalProjectMonitor.getMonitor().removeFileListener(mMarkerMonitor);
    100     }
    101 
    102     @Override
    103     public void activated() {
    104         super.activated();
    105         clearActionBindings(false);
    106     }
    107 
    108     @Override
    109     public void deactivated() {
    110         super.deactivated();
    111         updateActionBindings();
    112     }
    113 
    114     @Override
    115     protected void pageChange(int newPageIndex) {
    116         super.pageChange(newPageIndex);
    117         if (newPageIndex == mTextPageIndex) {
    118             updateActionBindings();
    119         } else {
    120             clearActionBindings(false);
    121         }
    122     }
    123 
    124     @Override
    125     protected int getPersistenceCategory() {
    126         return CATEGORY_MANIFEST;
    127     }
    128 
    129     /**
    130      * Return the root node of the UI element hierarchy, which here
    131      * is the "manifest" node.
    132      */
    133     @Override
    134     public UiElementNode getUiRootNode() {
    135         return mUiManifestNode;
    136     }
    137 
    138     /**
    139      * Returns the Manifest descriptors for the file being edited.
    140      */
    141     public AndroidManifestDescriptors getManifestDescriptors() {
    142         AndroidTargetData data = getTargetData();
    143         if (data != null) {
    144             return data.getManifestDescriptors();
    145         }
    146 
    147         return null;
    148     }
    149 
    150     // ---- Base Class Overrides ----
    151 
    152     /**
    153      * Returns whether the "save as" operation is supported by this editor.
    154      * <p/>
    155      * Save-As is a valid operation for the ManifestEditor since it acts on a
    156      * single source file.
    157      *
    158      * @see IEditorPart
    159      */
    160     @Override
    161     public boolean isSaveAsAllowed() {
    162         return true;
    163     }
    164 
    165     @Override
    166     public void doSave(IProgressMonitor monitor) {
    167         // Look up the current (pre-save) values of minSdkVersion and targetSdkVersion
    168         int prevMinSdkVersion = -1;
    169         int prevTargetSdkVersion = -1;
    170         IProject project = null;
    171         ManifestInfo info = null;
    172         try {
    173             project = getProject();
    174             if (project != null) {
    175                 info = ManifestInfo.get(project);
    176                 prevMinSdkVersion = info.getMinSdkVersion();
    177                 prevTargetSdkVersion = info.getTargetSdkVersion();
    178                 info.clear();
    179             }
    180         } catch (Throwable t) {
    181             // We don't expect exceptions from the above calls, but we *really*
    182             // need to make sure that nothing can prevent the save function from
    183             // getting called!
    184             AdtPlugin.log(t, null);
    185         }
    186 
    187         // Actually save
    188         super.doSave(monitor);
    189 
    190         // If the target/minSdkVersion has changed, clear all lint warnings (since many
    191         // of them are tied to the min/target sdk levels), in order to avoid showing stale
    192         // results
    193         try {
    194             if (info != null) {
    195                 int newMinSdkVersion = info.getMinSdkVersion();
    196                 int newTargetSdkVersion = info.getTargetSdkVersion();
    197                 if (newMinSdkVersion != prevMinSdkVersion
    198                         || newTargetSdkVersion != prevTargetSdkVersion) {
    199                     assert project != null;
    200                     EclipseLintClient.clearMarkers(project);
    201                 }
    202             }
    203         } catch (Throwable t) {
    204             AdtPlugin.log(t, null);
    205         }
    206     }
    207 
    208     /**
    209      * Creates the various form pages.
    210      */
    211     @Override
    212     protected void createFormPages() {
    213         try {
    214             addPage(mOverviewPage = new OverviewPage(this));
    215             addPage(mAppPage = new ApplicationPage(this));
    216             addPage(mPermissionPage = new PermissionPage(this));
    217             addPage(mInstrumentationPage = new InstrumentationPage(this));
    218         } catch (PartInitException e) {
    219             AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
    220         }
    221     }
    222 
    223     /* (non-java doc)
    224      * Change the tab/title name to include the project name.
    225      */
    226     @Override
    227     protected void setInput(IEditorInput input) {
    228         super.setInput(input);
    229         IFile inputFile = getInputFile();
    230         if (inputFile != null) {
    231             startMonitoringMarkers();
    232             setPartName(String.format("%1$s Manifest", inputFile.getProject().getName()));
    233         }
    234     }
    235 
    236     /**
    237      * Processes the new XML Model, which XML root node is given.
    238      *
    239      * @param xml_doc The XML document, if available, or null if none exists.
    240      */
    241     @Override
    242     protected void xmlModelChanged(Document xml_doc) {
    243         // create the ui root node on demand.
    244         initUiRootNode(false /*force*/);
    245 
    246         loadFromXml(xml_doc);
    247     }
    248 
    249     private void loadFromXml(Document xmlDoc) {
    250         mUiManifestNode.setXmlDocument(xmlDoc);
    251         Node node = getManifestXmlNode(xmlDoc);
    252 
    253         if (node != null) {
    254             // Refresh the manifest UI node and all its descendants
    255             mUiManifestNode.loadFromXmlNode(node);
    256         }
    257     }
    258 
    259     private Node getManifestXmlNode(Document xmlDoc) {
    260         if (xmlDoc != null) {
    261             ElementDescriptor manifestDesc = mUiManifestNode.getDescriptor();
    262             String manifestXmlName = manifestDesc == null ? null : manifestDesc.getXmlName();
    263             assert manifestXmlName != null;
    264 
    265             if (manifestXmlName != null) {
    266                 Node node = xmlDoc.getDocumentElement();
    267                 if (node != null && manifestXmlName.equals(node.getNodeName())) {
    268                     return node;
    269                 }
    270 
    271                 for (node = xmlDoc.getFirstChild();
    272                      node != null;
    273                      node = node.getNextSibling()) {
    274                     if (node.getNodeType() == Node.ELEMENT_NODE &&
    275                             manifestXmlName.equals(node.getNodeName())) {
    276                         return node;
    277                     }
    278                 }
    279             }
    280         }
    281 
    282         return null;
    283     }
    284 
    285     private void onDescriptorsChanged() {
    286         IStructuredModel model = getModelForRead();
    287         if (model != null) {
    288             try {
    289                 Node node = getManifestXmlNode(getXmlDocument(model));
    290                 mUiManifestNode.reloadFromXmlNode(node);
    291             } finally {
    292                 model.releaseFromRead();
    293             }
    294         }
    295 
    296         if (mOverviewPage != null) {
    297             mOverviewPage.refreshUiApplicationNode();
    298         }
    299 
    300         if (mAppPage != null) {
    301             mAppPage.refreshUiApplicationNode();
    302         }
    303 
    304         if (mPermissionPage != null) {
    305             mPermissionPage.refreshUiNode();
    306         }
    307 
    308         if (mInstrumentationPage != null) {
    309             mInstrumentationPage.refreshUiNode();
    310         }
    311     }
    312 
    313     /**
    314      * Reads and processes the current markers and adds a listener for marker changes.
    315      */
    316     private void startMonitoringMarkers() {
    317         final IFile inputFile = getInputFile();
    318         if (inputFile != null) {
    319             updateFromExistingMarkers(inputFile);
    320 
    321             mMarkerMonitor = new IFileListener() {
    322                 @Override
    323                 public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas,
    324                         int kind, @Nullable String extension, int flags, boolean isAndroidProject) {
    325                     if (isAndroidProject && file.equals(inputFile)) {
    326                         processMarkerChanges(markerDeltas);
    327                     }
    328                 }
    329             };
    330 
    331             GlobalProjectMonitor.getMonitor().addFileListener(
    332                     mMarkerMonitor, IResourceDelta.CHANGED);
    333         }
    334     }
    335 
    336     /**
    337      * Processes the markers of the specified {@link IFile} and updates the error status of
    338      * {@link UiElementNode}s and {@link UiAttributeNode}s.
    339      * @param inputFile the file being edited.
    340      */
    341     private void updateFromExistingMarkers(IFile inputFile) {
    342         try {
    343             // get the markers for the file
    344             IMarker[] markers = inputFile.findMarkers(
    345                     AdtConstants.MARKER_ANDROID, true, IResource.DEPTH_ZERO);
    346 
    347             AndroidManifestDescriptors desc = getManifestDescriptors();
    348             if (desc != null) {
    349                 ElementDescriptor appElement = desc.getApplicationElement();
    350 
    351                 if (appElement != null && mUiManifestNode != null) {
    352                     UiElementNode appUiNode = mUiManifestNode.findUiChildNode(
    353                             appElement.getXmlName());
    354                     List<UiElementNode> children = appUiNode.getUiChildren();
    355 
    356                     for (IMarker marker : markers) {
    357                         processMarker(marker, children, IResourceDelta.ADDED);
    358                     }
    359                 }
    360             }
    361 
    362         } catch (CoreException e) {
    363             // findMarkers can throw an exception, in which case, we'll do nothing.
    364         }
    365     }
    366 
    367     /**
    368      * Processes a {@link IMarker} change.
    369      * @param markerDeltas the list of {@link IMarkerDelta}
    370      */
    371     private void processMarkerChanges(IMarkerDelta[] markerDeltas) {
    372         AndroidManifestDescriptors descriptors = getManifestDescriptors();
    373         if (descriptors != null && descriptors.getApplicationElement() != null) {
    374             UiElementNode app_ui_node = mUiManifestNode.findUiChildNode(
    375                     descriptors.getApplicationElement().getXmlName());
    376             List<UiElementNode> children = app_ui_node.getUiChildren();
    377 
    378             for (IMarkerDelta markerDelta : markerDeltas) {
    379                 processMarker(markerDelta.getMarker(), children, markerDelta.getKind());
    380             }
    381         }
    382     }
    383 
    384     /**
    385      * Processes a new/old/updated marker.
    386      * @param marker The marker being added/removed/changed
    387      * @param nodeList the list of activity/service/provider/receiver nodes.
    388      * @param kind the change kind. Can be {@link IResourceDelta#ADDED},
    389      * {@link IResourceDelta#REMOVED}, or {@link IResourceDelta#CHANGED}
    390      */
    391     private void processMarker(IMarker marker, List<UiElementNode> nodeList, int kind) {
    392         // get the data from the marker
    393         String nodeType = marker.getAttribute(AdtConstants.MARKER_ATTR_TYPE, EMPTY);
    394         if (nodeType == EMPTY) {
    395             return;
    396         }
    397 
    398         String className = marker.getAttribute(AdtConstants.MARKER_ATTR_CLASS, EMPTY);
    399         if (className == EMPTY) {
    400             return;
    401         }
    402 
    403         for (UiElementNode ui_node : nodeList) {
    404             if (ui_node.getDescriptor().getXmlName().equals(nodeType)) {
    405                 for (UiAttributeNode attr : ui_node.getAllUiAttributes()) {
    406                     if (attr.getDescriptor().getXmlLocalName().equals(
    407                             AndroidManifestDescriptors.ANDROID_NAME_ATTR)) {
    408                         if (attr.getCurrentValue().equals(className)) {
    409                             if (kind == IResourceDelta.REMOVED) {
    410                                 attr.setHasError(false);
    411                             } else {
    412                                 attr.setHasError(true);
    413                             }
    414                             return;
    415                         }
    416                     }
    417                 }
    418             }
    419         }
    420     }
    421 
    422     /**
    423      * Creates the initial UI Root Node, including the known mandatory elements.
    424      * @param force if true, a new UiManifestNode is recreated even if it already exists.
    425      */
    426     @Override
    427     protected void initUiRootNode(boolean force) {
    428         // The manifest UI node is always created, even if there's no corresponding XML node.
    429         if (mUiManifestNode != null && force == false) {
    430             return;
    431         }
    432 
    433         AndroidManifestDescriptors manifestDescriptor = getManifestDescriptors();
    434 
    435         if (manifestDescriptor != null) {
    436             ElementDescriptor manifestElement = manifestDescriptor.getManifestElement();
    437             mUiManifestNode = manifestElement.createUiNode();
    438             mUiManifestNode.setEditor(this);
    439 
    440             // Similarly, always create the /manifest/uses-sdk followed by /manifest/application
    441             // (order of the elements now matters)
    442             ElementDescriptor element = manifestDescriptor.getUsesSdkElement();
    443             boolean present = false;
    444             for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) {
    445                 if (ui_node.getDescriptor() == element) {
    446                     present = true;
    447                     break;
    448                 }
    449             }
    450             if (!present) {
    451                 mUiManifestNode.appendNewUiChild(element);
    452             }
    453 
    454             element = manifestDescriptor.getApplicationElement();
    455             present = false;
    456             for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) {
    457                 if (ui_node.getDescriptor() == element) {
    458                     present = true;
    459                     break;
    460                 }
    461             }
    462             if (!present) {
    463                 mUiManifestNode.appendNewUiChild(element);
    464             }
    465 
    466             onDescriptorsChanged();
    467         } else {
    468             // create a dummy descriptor/uinode until we have real descriptors
    469             ElementDescriptor desc = new ElementDescriptor("manifest", //$NON-NLS-1$
    470                     "temporary descriptors due to missing decriptors", //$NON-NLS-1$
    471                     null /*tooltip*/, null /*sdk_url*/, null /*attributes*/,
    472                     null /*children*/, false /*mandatory*/);
    473             mUiManifestNode = desc.createUiNode();
    474             mUiManifestNode.setEditor(this);
    475         }
    476     }
    477 
    478     /**
    479      * Adds the given set of permissions into the manifest file in the suitable
    480      * location
    481      *
    482      * @param permissions permission fqcn's to be added
    483      * @param show if true, show one or more of the newly added permissions
    484      */
    485     public void addPermissions(@NonNull final List<String> permissions, final boolean show) {
    486         wrapUndoEditXmlModel("Add permissions", new Runnable() {
    487             @Override
    488             public void run() {
    489                 // Ensure that the model is current:
    490                 initUiRootNode(true /*force*/);
    491                 UiElementNode root = getUiRootNode();
    492 
    493                 ElementDescriptor descriptor = getManifestDescriptors().getUsesPermissionElement();
    494                 boolean shown = false;
    495                 for (String permission : permissions) {
    496                     // Find the first permission which sorts alphabetically laster than
    497                     // this permission (or the last permission, if none are after in the alphabet)
    498                     // and insert it there
    499                     int lastPermissionIndex = -1;
    500                     int nextPermissionIndex = -1;
    501                     int index = 0;
    502                     for (UiElementNode sibling : root.getUiChildren()) {
    503                         Node node = sibling.getXmlNode();
    504                         if (node.getNodeName().equals(USES_PERMISSION)) {
    505                             lastPermissionIndex = index;
    506                             String name = ((Element) node).getAttributeNS(ANDROID_URI, ATTR_NAME);
    507                             if (permission.compareTo(name) < 0) {
    508                                 nextPermissionIndex = index;
    509                                 break;
    510                             }
    511                         } else if (node.getNodeName().equals("application")) { //$NON-NLS-1$
    512                             // permissions should come before the application element
    513                             nextPermissionIndex = index;
    514                             break;
    515                         }
    516                         index++;
    517                     }
    518 
    519                     if (nextPermissionIndex != -1) {
    520                         index = nextPermissionIndex;
    521                     } else if (lastPermissionIndex != -1) {
    522                         index = lastPermissionIndex + 1;
    523                     } else {
    524                         index = root.getUiChildren().size();
    525                     }
    526                     UiElementNode usesPermission = root.insertNewUiChild(index, descriptor);
    527                     usesPermission.setAttributeValue(ATTR_NAME, ANDROID_URI, permission,
    528                             true /*override*/);
    529                     Node node = usesPermission.createXmlNode();
    530                     if (show && !shown) {
    531                         shown = true;
    532                         if (node instanceof IndexedRegion && getInputFile() != null) {
    533                             IndexedRegion indexedRegion = (IndexedRegion) node;
    534                             IRegion region = new Region(indexedRegion.getStartOffset(),
    535                                     indexedRegion.getEndOffset() - indexedRegion.getStartOffset());
    536                             try {
    537                                 AdtPlugin.openFile(getInputFile(), region, true /*show*/);
    538                             } catch (PartInitException e) {
    539                                 AdtPlugin.log(e, null);
    540                             }
    541                         } else {
    542                             show(node);
    543                         }
    544                     }
    545                 }
    546             }
    547         });
    548     }
    549 
    550     /**
    551      * Removes the permissions from the manifest editor
    552      *
    553      * @param permissions the permission fqcn's to be removed
    554      */
    555     public void removePermissions(@NonNull final Collection<String> permissions) {
    556         wrapUndoEditXmlModel("Remove permissions", new Runnable() {
    557             @Override
    558             public void run() {
    559                 // Ensure that the model is current:
    560                 initUiRootNode(true /*force*/);
    561                 UiElementNode root = getUiRootNode();
    562 
    563                 for (String permission : permissions) {
    564                     for (UiElementNode sibling : root.getUiChildren()) {
    565                         Node node = sibling.getXmlNode();
    566                         if (node.getNodeName().equals(USES_PERMISSION)) {
    567                             String name = ((Element) node).getAttributeNS(ANDROID_URI, ATTR_NAME);
    568                             if (name.equals(permission)) {
    569                                 sibling.deleteXmlNode();
    570                                 break;
    571                             }
    572                         }
    573                     }
    574                 }
    575             }
    576         });
    577     }
    578 }
    579