• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
ManifestEditor()90     public ManifestEditor() {
91         super();
92         addDefaultTargetListener();
93     }
94 
95     @Override
dispose()96     public void dispose() {
97         super.dispose();
98 
99         GlobalProjectMonitor.getMonitor().removeFileListener(mMarkerMonitor);
100     }
101 
102     @Override
activated()103     public void activated() {
104         super.activated();
105         clearActionBindings(false);
106     }
107 
108     @Override
deactivated()109     public void deactivated() {
110         super.deactivated();
111         updateActionBindings();
112     }
113 
114     @Override
pageChange(int newPageIndex)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
getPersistenceCategory()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
getUiRootNode()134     public UiElementNode getUiRootNode() {
135         return mUiManifestNode;
136     }
137 
138     /**
139      * Returns the Manifest descriptors for the file being edited.
140      */
getManifestDescriptors()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
isSaveAsAllowed()161     public boolean isSaveAsAllowed() {
162         return true;
163     }
164 
165     @Override
doSave(IProgressMonitor monitor)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
createFormPages()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
setInput(IEditorInput input)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
xmlModelChanged(Document xml_doc)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 
loadFromXml(Document xmlDoc)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 
getManifestXmlNode(Document xmlDoc)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 
onDescriptorsChanged()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      */
startMonitoringMarkers()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      */
updateFromExistingMarkers(IFile inputFile)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      */
processMarkerChanges(IMarkerDelta[] markerDeltas)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      */
processMarker(IMarker marker, List<UiElementNode> nodeList, int kind)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
initUiRootNode(boolean force)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      */
addPermissions(@onNull final List<String> permissions, final boolean show)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      */
removePermissions(@onNull final Collection<String> permissions)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