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 */ RulesEngine(GraphicalEditorPart editor, IProject project)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 */ getProject()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 */ getEditor()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 */ dispose()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 */ callGetDisplayName(UiViewElementNode element)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 */ callGetContextMenu(NodeProxy selectedNode)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 */ callAddLayoutActions(List<RuleAction> actions, NodeProxy parentNode, List<NodeProxy> children )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 */ callGetSelectionHint(NodeProxy parentNode, NodeProxy childNode)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 callPaintSelectionFeedback(GCWrapper gcWrapper, NodeProxy parentNode, List<? extends INode> childNodes, Object view)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 */ callOnDropEnter(NodeProxy targetNode, Object targetView, IDragElement[] elements)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 */ callOnDropMove(NodeProxy targetNode, IDragElement[] elements, DropFeedback feedback, Point where)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 */ callOnDropLeave(NodeProxy targetNode, IDragElement[] elements, DropFeedback feedback)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 */ callOnDropped(NodeProxy targetNode, IDragElement[] elements, DropFeedback feedback, Point where, InsertType insertType)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 */ callDropFeedbackPaint(IGraphics gc, NodeProxy targetNode, DropFeedback feedback)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 */ callOnPaste(NodeProxy targetNode, Object targetView, SimpleElement[] pastedElements)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 callOnResizeBegin(NodeProxy child, NodeProxy parent, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge, Object childView, Object parentView)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 callOnResizeUpdate(DropFeedback feedback, NodeProxy child, NodeProxy parent, Rect newBounds, int modifierMask)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 callOnResizeEnd(DropFeedback feedback, NodeProxy child, NodeProxy parent, Rect newBounds)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 */ callCreateHooks( AndroidXmlEditor editor, NodeProxy parentNode, NodeProxy childNode, InsertType overrideInsertType)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 callCreateHooks( final AndroidXmlEditor editor, final InsertType insertType, final IViewRule parentRule, final INode parentNode, final IViewRule childRule, final INode newNode)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 */ setInsertType(InsertType insertType)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 */ getInsertType()548 public InsertType getInsertType() { 549 return mInsertType; 550 } 551 552 // ---- Deletion ---- 553 callOnRemovingChildren(NodeProxy parentNode, List<INode> children)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 */ getBaseViewDescriptor()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 */ clearCache()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 */ updateClassLoader()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 */ loadRule(UiViewElementNode element)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 */ loadRule(String realFqcn, String targetFqcn)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 */ initializeRule(IViewRule rule, String targetFqcn)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