• 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.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