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