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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 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 */ 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 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 */ 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 */ 576 public InsertType getInsertType() { 577 return mInsertType; 578 } 579 580 // ---- Deletion ---- 581 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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