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.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      */
     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      */
    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      */
    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      */
    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      */
    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
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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      */
    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 
    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      */
    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      */
    576     public InsertType getInsertType() {
    577         return mInsertType;
    578     }
    579 
    580     // ---- Deletion ----
    581 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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