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