• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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