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