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