Home | History | Annotate | Download | only in gre
      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      */
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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      */
    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 
    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      */
    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      */
    548     public InsertType getInsertType() {
    549         return mInsertType;
    550     }
    551 
    552     // ---- Deletion ----
    553 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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