1 /* 2 * Copyright (C) 2009 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.layout.gre; 18 19 import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; 20 import static com.android.SdkConstants.VIEW_MERGE; 21 import static com.android.SdkConstants.VIEW_TAG; 22 23 import com.android.annotations.NonNull; 24 import com.android.annotations.Nullable; 25 import com.android.ide.common.api.DropFeedback; 26 import com.android.ide.common.api.IDragElement; 27 import com.android.ide.common.api.IGraphics; 28 import com.android.ide.common.api.INode; 29 import com.android.ide.common.api.IViewRule; 30 import com.android.ide.common.api.InsertType; 31 import com.android.ide.common.api.Point; 32 import com.android.ide.common.api.Rect; 33 import com.android.ide.common.api.RuleAction; 34 import com.android.ide.common.api.SegmentType; 35 import com.android.ide.common.layout.ViewRule; 36 import com.android.ide.eclipse.adt.AdtPlugin; 37 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 38 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 39 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 40 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GCWrapper; 41 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; 42 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleElement; 43 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; 44 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 45 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 46 import com.android.sdklib.IAndroidTarget; 47 48 import org.eclipse.core.resources.IProject; 49 50 import java.util.ArrayList; 51 import java.util.Collections; 52 import java.util.HashMap; 53 import java.util.HashSet; 54 import java.util.List; 55 import java.util.Map; 56 57 /** 58 * The rule engine manages the layout rules and interacts with them. 59 * There's one {@link RulesEngine} instance per layout editor. 60 * Each instance has 2 sets of rules: the static ADT rules (shared across all instances) 61 * and the project specific rules (local to the current instance / layout editor). 62 */ 63 public class RulesEngine { 64 private final IProject mProject; 65 private final Map<Object, IViewRule> mRulesCache = new HashMap<Object, IViewRule>(); 66 67 /** 68 * The type of any upcoming node manipulations performed by the {@link IViewRule}s. 69 * When actions are performed in the tool (like a paste action, or a drag from palette, 70 * or a drag move within the canvas, etc), these are different types of inserts, 71 * and we don't want to have the rules track them closely (and pass them back to us 72 * in the {@link INode#insertChildAt} methods etc), so instead we track the state 73 * here on behalf of the currently executing rule. 74 */ 75 private InsertType mInsertType = InsertType.CREATE; 76 77 /** 78 * Per-project loader for custom view rules 79 */ 80 private RuleLoader mRuleLoader; 81 private ClassLoader mUserClassLoader; 82 83 /** 84 * The editor which owns this {@link RulesEngine} 85 */ 86 private final GraphicalEditorPart mEditor; 87 88 /** 89 * Creates a new {@link RulesEngine} associated with the selected project. 90 * <p/> 91 * The rules engine will look in the project for a tools jar to load custom view rules. 92 * 93 * @param editor the editor which owns this {@link RulesEngine} 94 * @param project A non-null open project. 95 */ RulesEngine(GraphicalEditorPart editor, IProject project)96 public RulesEngine(GraphicalEditorPart editor, IProject project) { 97 mProject = project; 98 mEditor = editor; 99 100 mRuleLoader = RuleLoader.get(project); 101 } 102 103 /** 104 * Returns the {@link IProject} on which the {@link RulesEngine} was created. 105 */ getProject()106 public IProject getProject() { 107 return mProject; 108 } 109 110 /** 111 * Returns the {@link GraphicalEditorPart} for which the {@link RulesEngine} was 112 * created. 113 * 114 * @return the associated editor 115 */ getEditor()116 public GraphicalEditorPart getEditor() { 117 return mEditor; 118 } 119 120 /** 121 * Called by the owner of the {@link RulesEngine} when it is going to be disposed. 122 * This frees some resources, such as the project's folder monitor. 123 */ dispose()124 public void dispose() { 125 clearCache(); 126 } 127 128 /** 129 * Invokes {@link IViewRule#getDisplayName()} on the rule matching the specified element. 130 * 131 * @param element The view element to target. Can be null. 132 * @return Null if the rule failed, there's no rule or the rule does not want to override 133 * the display name. Otherwise, a string as returned by the rule. 134 */ callGetDisplayName(UiViewElementNode element)135 public String callGetDisplayName(UiViewElementNode element) { 136 // try to find a rule for this element's FQCN 137 IViewRule rule = loadRule(element); 138 139 if (rule != null) { 140 try { 141 return rule.getDisplayName(); 142 143 } catch (Exception e) { 144 AdtPlugin.log(e, "%s.getDisplayName() failed: %s", 145 rule.getClass().getSimpleName(), 146 e.toString()); 147 } 148 } 149 150 return null; 151 } 152 153 /** 154 * Invokes {@link IViewRule#addContextMenuActions(List, INode)} on the rule matching the specified element. 155 * 156 * @param selectedNode The node selected. Never null. 157 * @return Null if the rule failed, there's no rule or the rule does not provide 158 * any custom menu actions. Otherwise, a list of {@link RuleAction}. 159 */ 160 @Nullable callGetContextMenu(NodeProxy selectedNode)161 public List<RuleAction> callGetContextMenu(NodeProxy selectedNode) { 162 // try to find a rule for this element's FQCN 163 IViewRule rule = loadRule(selectedNode.getNode()); 164 165 if (rule != null) { 166 try { 167 mInsertType = InsertType.CREATE; 168 List<RuleAction> actions = new ArrayList<RuleAction>(); 169 rule.addContextMenuActions(actions, selectedNode); 170 Collections.sort(actions); 171 172 return actions; 173 } catch (Exception e) { 174 AdtPlugin.log(e, "%s.getContextMenu() failed: %s", 175 rule.getClass().getSimpleName(), 176 e.toString()); 177 } 178 } 179 180 return null; 181 } 182 183 /** 184 * Calls the selected node to return its default action 185 * 186 * @param selectedNode the node to apply the action to 187 * @return the default action id 188 */ callGetDefaultActionId(@onNull NodeProxy selectedNode)189 public String callGetDefaultActionId(@NonNull NodeProxy selectedNode) { 190 // try to find a rule for this element's FQCN 191 IViewRule rule = loadRule(selectedNode.getNode()); 192 193 if (rule != null) { 194 try { 195 mInsertType = InsertType.CREATE; 196 return rule.getDefaultActionId(selectedNode); 197 } catch (Exception e) { 198 AdtPlugin.log(e, "%s.getDefaultAction() failed: %s", 199 rule.getClass().getSimpleName(), 200 e.toString()); 201 } 202 } 203 204 return null; 205 } 206 207 /** 208 * Invokes {@link IViewRule#addLayoutActions(List, INode, List)} on the rule 209 * matching the specified element. 210 * 211 * @param actions The list of actions to add layout actions into 212 * @param parentNode The layout node 213 * @param children The selected children of the node, if any (used to 214 * initialize values of child layout controls, if applicable) 215 * @return Null if the rule failed, there's no rule or the rule does not 216 * provide any custom menu actions. Otherwise, a list of 217 * {@link RuleAction}. 218 */ callAddLayoutActions(List<RuleAction> actions, NodeProxy parentNode, List<NodeProxy> children )219 public List<RuleAction> callAddLayoutActions(List<RuleAction> actions, 220 NodeProxy parentNode, List<NodeProxy> children ) { 221 // try to find a rule for this element's FQCN 222 IViewRule rule = loadRule(parentNode.getNode()); 223 224 if (rule != null) { 225 try { 226 mInsertType = InsertType.CREATE; 227 rule.addLayoutActions(actions, parentNode, children); 228 } catch (Exception e) { 229 AdtPlugin.log(e, "%s.getContextMenu() failed: %s", 230 rule.getClass().getSimpleName(), 231 e.toString()); 232 } 233 } 234 235 return null; 236 } 237 238 /** 239 * Invokes {@link IViewRule#getSelectionHint(INode, INode)} 240 * on the rule matching the specified element. 241 * 242 * @param parentNode The parent of the node selected. Never null. 243 * @param childNode The child node that was selected. Never null. 244 * @return a list of strings to be displayed, or null or empty to display nothing 245 */ callGetSelectionHint(NodeProxy parentNode, NodeProxy childNode)246 public List<String> callGetSelectionHint(NodeProxy parentNode, NodeProxy childNode) { 247 // try to find a rule for this element's FQCN 248 IViewRule rule = loadRule(parentNode.getNode()); 249 250 if (rule != null) { 251 try { 252 return rule.getSelectionHint(parentNode, childNode); 253 254 } catch (Exception e) { 255 AdtPlugin.log(e, "%s.getSelectionHint() failed: %s", 256 rule.getClass().getSimpleName(), 257 e.toString()); 258 } 259 } 260 261 return null; 262 } 263 callPaintSelectionFeedback(GCWrapper gcWrapper, NodeProxy parentNode, List<? extends INode> childNodes, Object view)264 public void callPaintSelectionFeedback(GCWrapper gcWrapper, NodeProxy parentNode, 265 List<? extends INode> childNodes, Object view) { 266 // try to find a rule for this element's FQCN 267 IViewRule rule = loadRule(parentNode.getNode()); 268 269 if (rule != null) { 270 try { 271 rule.paintSelectionFeedback(gcWrapper, parentNode, childNodes, view); 272 273 } catch (Exception e) { 274 AdtPlugin.log(e, "%s.callPaintSelectionFeedback() failed: %s", 275 rule.getClass().getSimpleName(), 276 e.toString()); 277 } 278 } 279 } 280 281 /** 282 * Called when the d'n'd starts dragging over the target node. 283 * If interested, returns a DropFeedback passed to onDrop/Move/Leave/Paint. 284 * If not interested in drop, return false. 285 * Followed by a paint. 286 */ callOnDropEnter(NodeProxy targetNode, Object targetView, IDragElement[] elements)287 public DropFeedback callOnDropEnter(NodeProxy targetNode, 288 Object targetView, IDragElement[] elements) { 289 // try to find a rule for this element's FQCN 290 IViewRule rule = loadRule(targetNode.getNode()); 291 292 if (rule != null) { 293 try { 294 return rule.onDropEnter(targetNode, targetView, elements); 295 296 } catch (Exception e) { 297 AdtPlugin.log(e, "%s.onDropEnter() failed: %s", 298 rule.getClass().getSimpleName(), 299 e.toString()); 300 } 301 } 302 303 return null; 304 } 305 306 /** 307 * Called after onDropEnter. 308 * Returns a DropFeedback passed to onDrop/Move/Leave/Paint (typically same 309 * as input one). 310 */ callOnDropMove(NodeProxy targetNode, IDragElement[] elements, DropFeedback feedback, Point where)311 public DropFeedback callOnDropMove(NodeProxy targetNode, 312 IDragElement[] elements, 313 DropFeedback feedback, 314 Point where) { 315 // try to find a rule for this element's FQCN 316 IViewRule rule = loadRule(targetNode.getNode()); 317 318 if (rule != null) { 319 try { 320 return rule.onDropMove(targetNode, elements, feedback, where); 321 322 } catch (Exception e) { 323 AdtPlugin.log(e, "%s.onDropMove() failed: %s", 324 rule.getClass().getSimpleName(), 325 e.toString()); 326 } 327 } 328 329 return null; 330 } 331 332 /** 333 * Called when drop leaves the target without actually dropping 334 */ callOnDropLeave(NodeProxy targetNode, IDragElement[] elements, DropFeedback feedback)335 public void callOnDropLeave(NodeProxy targetNode, 336 IDragElement[] elements, 337 DropFeedback feedback) { 338 // try to find a rule for this element's FQCN 339 IViewRule rule = loadRule(targetNode.getNode()); 340 341 if (rule != null) { 342 try { 343 rule.onDropLeave(targetNode, elements, feedback); 344 345 } catch (Exception e) { 346 AdtPlugin.log(e, "%s.onDropLeave() failed: %s", 347 rule.getClass().getSimpleName(), 348 e.toString()); 349 } 350 } 351 } 352 353 /** 354 * Called when drop is released over the target to perform the actual drop. 355 */ callOnDropped(NodeProxy targetNode, IDragElement[] elements, DropFeedback feedback, Point where, InsertType insertType)356 public void callOnDropped(NodeProxy targetNode, 357 IDragElement[] elements, 358 DropFeedback feedback, 359 Point where, 360 InsertType insertType) { 361 // try to find a rule for this element's FQCN 362 IViewRule rule = loadRule(targetNode.getNode()); 363 364 if (rule != null) { 365 try { 366 mInsertType = insertType; 367 rule.onDropped(targetNode, elements, feedback, where); 368 369 } catch (Exception e) { 370 AdtPlugin.log(e, "%s.onDropped() failed: %s", 371 rule.getClass().getSimpleName(), 372 e.toString()); 373 } 374 } 375 } 376 377 /** 378 * Called when a paint has been requested via DropFeedback. 379 */ callDropFeedbackPaint(IGraphics gc, NodeProxy targetNode, DropFeedback feedback)380 public void callDropFeedbackPaint(IGraphics gc, 381 NodeProxy targetNode, 382 DropFeedback feedback) { 383 if (gc != null && feedback != null && feedback.painter != null) { 384 try { 385 feedback.painter.paint(gc, targetNode, feedback); 386 } catch (Exception e) { 387 AdtPlugin.log(e, "DropFeedback.painter failed: %s", 388 e.toString()); 389 } 390 } 391 } 392 393 /** 394 * Called when pasting elements in an existing document on the selected target. 395 * 396 * @param targetNode The first node selected. 397 * @param targetView The view object for the target node, or null if not known 398 * @param pastedElements The elements being pasted. 399 * @return the parent node the paste was applied into 400 */ callOnPaste(NodeProxy targetNode, Object targetView, SimpleElement[] pastedElements)401 public NodeProxy callOnPaste(NodeProxy targetNode, Object targetView, 402 SimpleElement[] pastedElements) { 403 404 // Find a target which accepts children. If you for example select a button 405 // and attempt to paste, this will reselect the parent of the button as the paste 406 // target. (This is a loop rather than just checking the direct parent since 407 // we will soon ask each child whether they are *willing* to accept the new child. 408 // A ScrollView for example, which only accepts one child, might also say no 409 // and delegate to its parent in turn. 410 INode parent = targetNode; 411 while (parent instanceof NodeProxy) { 412 NodeProxy np = (NodeProxy) parent; 413 if (np.getNode() != null && np.getNode().getDescriptor() != null) { 414 ElementDescriptor descriptor = np.getNode().getDescriptor(); 415 if (descriptor.hasChildren()) { 416 targetNode = np; 417 break; 418 } 419 } 420 parent = parent.getParent(); 421 } 422 423 // try to find a rule for this element's FQCN 424 IViewRule rule = loadRule(targetNode.getNode()); 425 426 if (rule != null) { 427 try { 428 mInsertType = InsertType.PASTE; 429 rule.onPaste(targetNode, targetView, pastedElements); 430 431 } catch (Exception e) { 432 AdtPlugin.log(e, "%s.onPaste() failed: %s", 433 rule.getClass().getSimpleName(), 434 e.toString()); 435 } 436 } 437 438 return targetNode; 439 } 440 441 // ---- Resize operations ---- 442 callOnResizeBegin(NodeProxy child, NodeProxy parent, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge, Object childView, Object parentView)443 public DropFeedback callOnResizeBegin(NodeProxy child, NodeProxy parent, Rect newBounds, 444 SegmentType horizontalEdge, SegmentType verticalEdge, Object childView, 445 Object parentView) { 446 IViewRule rule = loadRule(parent.getNode()); 447 448 if (rule != null) { 449 try { 450 return rule.onResizeBegin(child, parent, horizontalEdge, verticalEdge, 451 childView, parentView); 452 } catch (Exception e) { 453 AdtPlugin.log(e, "%s.onResizeBegin() failed: %s", rule.getClass().getSimpleName(), 454 e.toString()); 455 } 456 } 457 458 return null; 459 } 460 callOnResizeUpdate(DropFeedback feedback, NodeProxy child, NodeProxy parent, Rect newBounds, int modifierMask)461 public void callOnResizeUpdate(DropFeedback feedback, NodeProxy child, NodeProxy parent, 462 Rect newBounds, int modifierMask) { 463 IViewRule rule = loadRule(parent.getNode()); 464 465 if (rule != null) { 466 try { 467 rule.onResizeUpdate(feedback, child, parent, newBounds, modifierMask); 468 } catch (Exception e) { 469 AdtPlugin.log(e, "%s.onResizeUpdate() failed: %s", rule.getClass().getSimpleName(), 470 e.toString()); 471 } 472 } 473 } 474 callOnResizeEnd(DropFeedback feedback, NodeProxy child, NodeProxy parent, Rect newBounds)475 public void callOnResizeEnd(DropFeedback feedback, NodeProxy child, NodeProxy parent, 476 Rect newBounds) { 477 IViewRule rule = loadRule(parent.getNode()); 478 479 if (rule != null) { 480 try { 481 rule.onResizeEnd(feedback, child, parent, newBounds); 482 } catch (Exception e) { 483 AdtPlugin.log(e, "%s.onResizeEnd() failed: %s", rule.getClass().getSimpleName(), 484 e.toString()); 485 } 486 } 487 } 488 489 // ---- Creation customizations ---- 490 491 /** 492 * Invokes the create hooks ({@link IViewRule#onCreate}, 493 * {@link IViewRule#onChildInserted} when a new child has been created/pasted/moved, and 494 * is inserted into a given parent. The parent may be null (for example when rendering 495 * top level items for preview). 496 * 497 * @param editor the XML editor to apply edits to the model for (performed by view 498 * rules) 499 * @param parentNode the parent XML node, or null if unknown 500 * @param childNode the XML node of the new node, never null 501 * @param overrideInsertType If not null, specifies an explicit insert type to use for 502 * edits made during the customization 503 */ callCreateHooks( AndroidXmlEditor editor, NodeProxy parentNode, NodeProxy childNode, InsertType overrideInsertType)504 public void callCreateHooks( 505 AndroidXmlEditor editor, 506 NodeProxy parentNode, NodeProxy childNode, 507 InsertType overrideInsertType) { 508 IViewRule parentRule = null; 509 510 if (parentNode != null) { 511 UiViewElementNode parentUiNode = parentNode.getNode(); 512 parentRule = loadRule(parentUiNode); 513 } 514 515 if (overrideInsertType != null) { 516 mInsertType = overrideInsertType; 517 } 518 519 UiViewElementNode newUiNode = childNode.getNode(); 520 IViewRule childRule = loadRule(newUiNode); 521 if (childRule != null || parentRule != null) { 522 callCreateHooks(editor, mInsertType, parentRule, parentNode, 523 childRule, childNode); 524 } 525 } 526 callCreateHooks( final AndroidXmlEditor editor, final InsertType insertType, final IViewRule parentRule, final INode parentNode, final IViewRule childRule, final INode newNode)527 private static void callCreateHooks( 528 final AndroidXmlEditor editor, final InsertType insertType, 529 final IViewRule parentRule, final INode parentNode, 530 final IViewRule childRule, final INode newNode) { 531 // Notify the parent about the new child in case it wants to customize it 532 // (For example, a ScrollView parent can go and set all its children's layout params to 533 // fill the parent.) 534 if (!editor.isEditXmlModelPending()) { 535 editor.wrapEditXmlModel(new Runnable() { 536 @Override 537 public void run() { 538 callCreateHooks(editor, insertType, 539 parentRule, parentNode, childRule, newNode); 540 } 541 }); 542 return; 543 } 544 545 if (parentRule != null) { 546 parentRule.onChildInserted(newNode, parentNode, insertType); 547 } 548 549 // Look up corresponding IViewRule, and notify the rule about 550 // this create action in case it wants to customize the new object. 551 // (For example, a rule for TabHosts can go and create a default child tab 552 // when you create it.) 553 if (childRule != null) { 554 childRule.onCreate(newNode, parentNode, insertType); 555 } 556 557 if (parentNode != null) { 558 ((NodeProxy) parentNode).applyPendingChanges(); 559 } 560 } 561 562 /** 563 * Set the type of insert currently in progress 564 * 565 * @param insertType the insert type to use for the next operation 566 */ setInsertType(InsertType insertType)567 public void setInsertType(InsertType insertType) { 568 mInsertType = insertType; 569 } 570 571 /** 572 * Return the type of insert currently in progress 573 * 574 * @return the type of insert currently in progress 575 */ getInsertType()576 public InsertType getInsertType() { 577 return mInsertType; 578 } 579 580 // ---- Deletion ---- 581 callOnRemovingChildren(NodeProxy parentNode, List<INode> children)582 public void callOnRemovingChildren(NodeProxy parentNode, 583 List<INode> children) { 584 if (parentNode != null) { 585 UiViewElementNode parentUiNode = parentNode.getNode(); 586 IViewRule parentRule = loadRule(parentUiNode); 587 if (parentRule != null) { 588 try { 589 parentRule.onRemovingChildren(children, parentNode, 590 mInsertType == InsertType.MOVE_WITHIN); 591 } catch (Exception e) { 592 AdtPlugin.log(e, "%s.onDispose() failed: %s", 593 parentRule.getClass().getSimpleName(), 594 e.toString()); 595 } 596 } 597 } 598 } 599 600 // ---- private --- 601 602 /** 603 * Returns the descriptor for the base View class. 604 * This could be null if the SDK or the given platform target hasn't loaded yet. 605 */ getBaseViewDescriptor()606 private ViewElementDescriptor getBaseViewDescriptor() { 607 Sdk currentSdk = Sdk.getCurrent(); 608 if (currentSdk != null) { 609 IAndroidTarget target = currentSdk.getTarget(mProject); 610 if (target != null) { 611 AndroidTargetData data = currentSdk.getTargetData(target); 612 return data.getLayoutDescriptors().getBaseViewDescriptor(); 613 } 614 } 615 return null; 616 } 617 618 /** 619 * Clear the Rules cache. Calls onDispose() on each rule. 620 */ clearCache()621 private void clearCache() { 622 // The cache can contain multiple times the same rule instance for different 623 // keys (e.g. the UiViewElementNode key vs. the FQCN string key.) So transfer 624 // all values to a unique set. 625 HashSet<IViewRule> rules = new HashSet<IViewRule>(mRulesCache.values()); 626 627 mRulesCache.clear(); 628 629 for (IViewRule rule : rules) { 630 if (rule != null) { 631 try { 632 rule.onDispose(); 633 } catch (Exception e) { 634 AdtPlugin.log(e, "%s.onDispose() failed: %s", 635 rule.getClass().getSimpleName(), 636 e.toString()); 637 } 638 } 639 } 640 } 641 642 /** 643 * Checks whether the project class loader has changed, and if so 644 * unregisters any view rules that use classes from the old class loader. It 645 * then returns the class loader to be used. 646 */ updateClassLoader()647 private ClassLoader updateClassLoader() { 648 ClassLoader classLoader = mRuleLoader.getClassLoader(); 649 if (mUserClassLoader != null && classLoader != mUserClassLoader) { 650 // We have to unload all the IViewRules from the old class 651 List<Object> dispose = new ArrayList<Object>(); 652 for (Map.Entry<Object, IViewRule> entry : mRulesCache.entrySet()) { 653 IViewRule rule = entry.getValue(); 654 if (rule.getClass().getClassLoader() == mUserClassLoader) { 655 dispose.add(entry.getKey()); 656 } 657 } 658 for (Object object : dispose) { 659 mRulesCache.remove(object); 660 } 661 } 662 663 mUserClassLoader = classLoader; 664 return mUserClassLoader; 665 } 666 667 /** 668 * Load a rule using its descriptor. This will try to first load the rule using its 669 * actual FQCN and if that fails will find the first parent that works in the view 670 * hierarchy. 671 */ loadRule(UiViewElementNode element)672 private IViewRule loadRule(UiViewElementNode element) { 673 if (element == null) { 674 return null; 675 } 676 677 String targetFqcn = null; 678 ViewElementDescriptor targetDesc = null; 679 680 ElementDescriptor d = element.getDescriptor(); 681 if (d instanceof ViewElementDescriptor) { 682 targetDesc = (ViewElementDescriptor) d; 683 } 684 if (d == null || !(d instanceof ViewElementDescriptor)) { 685 // This should not happen. All views should have some kind of *view* element 686 // descriptor. Maybe the project is not complete and doesn't build or something. 687 // In this case, we'll use the descriptor of the base android View class. 688 targetDesc = getBaseViewDescriptor(); 689 } 690 691 // Check whether any of the custom view .jar files have changed and if so 692 // unregister previously cached view rules to force a new view rule to be loaded. 693 updateClassLoader(); 694 695 // Return the rule if we find it in the cache, even if it was stored as null 696 // (which means we didn't find it earlier, so don't look for it again) 697 IViewRule rule = mRulesCache.get(targetDesc); 698 if (rule != null || mRulesCache.containsKey(targetDesc)) { 699 return rule; 700 } 701 702 // Get the descriptor and loop through the super class hierarchy 703 for (ViewElementDescriptor desc = targetDesc; 704 desc != null; 705 desc = desc.getSuperClassDesc()) { 706 707 // Get the FQCN of this View 708 String fqcn = desc.getFullClassName(); 709 if (fqcn == null) { 710 // Shouldn't be happening. 711 return null; 712 } 713 714 // The first time we keep the FQCN around as it's the target class we were 715 // initially trying to load. After, as we move through the hierarchy, the 716 // target FQCN remains constant. 717 if (targetFqcn == null) { 718 targetFqcn = fqcn; 719 } 720 721 if (fqcn.indexOf('.') == -1) { 722 // Deal with unknown descriptors; these lack the full qualified path and 723 // elements in the layout without a package are taken to be in the 724 // android.widget package. 725 fqcn = ANDROID_WIDGET_PREFIX + fqcn; 726 } 727 728 // Try to find a rule matching the "real" FQCN. If we find it, we're done. 729 // If not, the for loop will move to the parent descriptor. 730 rule = loadRule(fqcn, targetFqcn); 731 if (rule != null) { 732 // We found one. 733 // As a side effect, loadRule() also cached the rule using the target FQCN. 734 return rule; 735 } 736 } 737 738 // Memorize in the cache that we couldn't find a rule for this descriptor 739 mRulesCache.put(targetDesc, null); 740 return null; 741 } 742 743 /** 744 * Try to load a rule given a specific FQCN. This looks for an exact match in either 745 * the ADT scripts or the project scripts and does not look at parent hierarchy. 746 * <p/> 747 * Once a rule is found (or not), it is stored in a cache using its target FQCN 748 * so we don't try to reload it. 749 * <p/> 750 * The real FQCN is the actual rule class we're loading, e.g. "android.view.View" 751 * where target FQCN is the class we were initially looking for, which might be the same as 752 * the real FQCN or might be a derived class, e.g. "android.widget.TextView". 753 * 754 * @param realFqcn The FQCN of the rule class actually being loaded. 755 * @param targetFqcn The FQCN of the class actually processed, which might be different from 756 * the FQCN of the rule being loaded. 757 */ loadRule(String realFqcn, String targetFqcn)758 IViewRule loadRule(String realFqcn, String targetFqcn) { 759 if (realFqcn == null || targetFqcn == null) { 760 return null; 761 } 762 763 // Return the rule if we find it in the cache, even if it was stored as null 764 // (which means we didn't find it earlier, so don't look for it again) 765 IViewRule rule = mRulesCache.get(realFqcn); 766 if (rule != null || mRulesCache.containsKey(realFqcn)) { 767 return rule; 768 } 769 770 // Look for class via reflection 771 try { 772 // For now, we package view rules for the builtin Android views and 773 // widgets with the tool in a special package, so look there rather 774 // than in the same package as the widgets. 775 String ruleClassName; 776 ClassLoader classLoader; 777 if (realFqcn.startsWith("android.") || //$NON-NLS-1$ 778 realFqcn.equals(VIEW_MERGE) || 779 realFqcn.endsWith(".GridLayout") || //$NON-NLS-1$ // Temporary special case 780 // FIXME: Remove this special case as soon as we pull 781 // the MapViewRule out of this code base and bundle it 782 // with the add ons 783 realFqcn.startsWith("com.google.android.maps.")) { //$NON-NLS-1$ 784 // This doesn't handle a case where there are name conflicts 785 // (e.g. where there are multiple different views with the same 786 // class name and only differing in package names, but that's a 787 // really bad practice in the first place, and if that situation 788 // should come up in the API we can enhance this algorithm. 789 String packageName = ViewRule.class.getName(); 790 packageName = packageName.substring(0, packageName.lastIndexOf('.')); 791 classLoader = RulesEngine.class.getClassLoader(); 792 int dotIndex = realFqcn.lastIndexOf('.'); 793 String baseName = realFqcn.substring(dotIndex+1); 794 // Capitalize rule class name to match naming conventions, if necessary (<merge>) 795 if (Character.isLowerCase(baseName.charAt(0))) { 796 if (baseName.equals(VIEW_TAG)) { 797 // Hack: ViewRule is generic for the "View" class, so we can't use it 798 // for the special XML "view" tag (lowercase); instead, the rule is 799 // named "ViewTagRule" instead. 800 baseName = "ViewTag"; //$NON-NLS-1$ 801 } 802 baseName = Character.toUpperCase(baseName.charAt(0)) + baseName.substring(1); 803 } 804 ruleClassName = packageName + "." + //$NON-NLS-1$ 805 baseName + "Rule"; //$NON-NLS-1$ 806 } else { 807 // Initialize the user-classpath for 3rd party IViewRules, if necessary 808 classLoader = updateClassLoader(); 809 if (classLoader == null) { 810 // The mUserClassLoader can be null; this is the typical scenario, 811 // when the user is only using builtin layout rules. 812 // This means however we can't resolve this fqcn since it's not 813 // in the name space of the builtin rules. 814 mRulesCache.put(realFqcn, null); 815 return null; 816 } 817 818 // For other (3rd party) widgets, look in the same package (though most 819 // likely not in the same jar!) 820 ruleClassName = realFqcn + "Rule"; //$NON-NLS-1$ 821 } 822 823 Class<?> clz = Class.forName(ruleClassName, true, classLoader); 824 rule = (IViewRule) clz.newInstance(); 825 return initializeRule(rule, targetFqcn); 826 } catch (ClassNotFoundException ex) { 827 // Not an unexpected error - this means that there isn't a helper for this 828 // class. 829 } catch (InstantiationException e) { 830 // This is NOT an expected error: fail. 831 AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString()); 832 } catch (IllegalAccessException e) { 833 // This is NOT an expected error: fail. 834 AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString()); 835 } 836 837 // Memorize in the cache that we couldn't find a rule for this real FQCN 838 mRulesCache.put(realFqcn, null); 839 return null; 840 } 841 842 /** 843 * Initialize a rule we just loaded. The rule has a chance to examine the target FQCN 844 * and bail out. 845 * <p/> 846 * Contract: the rule is not in the {@link #mRulesCache} yet and this method will 847 * cache it using the target FQCN if the rule is accepted. 848 * <p/> 849 * The real FQCN is the actual rule class we're loading, e.g. "android.view.View" 850 * where target FQCN is the class we were initially looking for, which might be the same as 851 * the real FQCN or might be a derived class, e.g. "android.widget.TextView". 852 * 853 * @param rule A rule freshly loaded. 854 * @param targetFqcn The FQCN of the class actually processed, which might be different from 855 * the FQCN of the rule being loaded. 856 * @return The rule if accepted, or null if the rule can't handle that FQCN. 857 */ initializeRule(IViewRule rule, String targetFqcn)858 private IViewRule initializeRule(IViewRule rule, String targetFqcn) { 859 860 try { 861 if (rule.onInitialize(targetFqcn, new ClientRulesEngine(this, targetFqcn))) { 862 // Add it to the cache and return it 863 mRulesCache.put(targetFqcn, rule); 864 return rule; 865 } else { 866 rule.onDispose(); 867 } 868 } catch (Exception e) { 869 AdtPlugin.log(e, "%s.onInit() failed: %s", 870 rule.getClass().getSimpleName(), 871 e.toString()); 872 } 873 874 return null; 875 } 876 } 877