| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ide.eclipse.adt.internal.editors.layout.gre; |
| |
| import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; |
| import static com.android.SdkConstants.VIEW_MERGE; |
| import static com.android.SdkConstants.VIEW_TAG; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.api.DropFeedback; |
| import com.android.ide.common.api.IDragElement; |
| import com.android.ide.common.api.IGraphics; |
| import com.android.ide.common.api.INode; |
| import com.android.ide.common.api.IViewRule; |
| import com.android.ide.common.api.InsertType; |
| import com.android.ide.common.api.Point; |
| import com.android.ide.common.api.Rect; |
| import com.android.ide.common.api.RuleAction; |
| import com.android.ide.common.api.SegmentType; |
| import com.android.ide.common.layout.ViewRule; |
| import com.android.ide.eclipse.adt.AdtPlugin; |
| import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; |
| import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GCWrapper; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleElement; |
| import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; |
| import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; |
| import com.android.ide.eclipse.adt.internal.sdk.Sdk; |
| import com.android.sdklib.IAndroidTarget; |
| |
| import org.eclipse.core.resources.IProject; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * The rule engine manages the layout rules and interacts with them. |
| * There's one {@link RulesEngine} instance per layout editor. |
| * Each instance has 2 sets of rules: the static ADT rules (shared across all instances) |
| * and the project specific rules (local to the current instance / layout editor). |
| */ |
| public class RulesEngine { |
| private final IProject mProject; |
| private final Map<Object, IViewRule> mRulesCache = new HashMap<Object, IViewRule>(); |
| |
| /** |
| * The type of any upcoming node manipulations performed by the {@link IViewRule}s. |
| * When actions are performed in the tool (like a paste action, or a drag from palette, |
| * or a drag move within the canvas, etc), these are different types of inserts, |
| * and we don't want to have the rules track them closely (and pass them back to us |
| * in the {@link INode#insertChildAt} methods etc), so instead we track the state |
| * here on behalf of the currently executing rule. |
| */ |
| private InsertType mInsertType = InsertType.CREATE; |
| |
| /** |
| * Per-project loader for custom view rules |
| */ |
| private RuleLoader mRuleLoader; |
| private ClassLoader mUserClassLoader; |
| |
| /** |
| * The editor which owns this {@link RulesEngine} |
| */ |
| private final GraphicalEditorPart mEditor; |
| |
| /** |
| * Creates a new {@link RulesEngine} associated with the selected project. |
| * <p/> |
| * The rules engine will look in the project for a tools jar to load custom view rules. |
| * |
| * @param editor the editor which owns this {@link RulesEngine} |
| * @param project A non-null open project. |
| */ |
| public RulesEngine(GraphicalEditorPart editor, IProject project) { |
| mProject = project; |
| mEditor = editor; |
| |
| mRuleLoader = RuleLoader.get(project); |
| } |
| |
| /** |
| * Returns the {@link IProject} on which the {@link RulesEngine} was created. |
| */ |
| public IProject getProject() { |
| return mProject; |
| } |
| |
| /** |
| * Returns the {@link GraphicalEditorPart} for which the {@link RulesEngine} was |
| * created. |
| * |
| * @return the associated editor |
| */ |
| public GraphicalEditorPart getEditor() { |
| return mEditor; |
| } |
| |
| /** |
| * Called by the owner of the {@link RulesEngine} when it is going to be disposed. |
| * This frees some resources, such as the project's folder monitor. |
| */ |
| public void dispose() { |
| clearCache(); |
| } |
| |
| /** |
| * Invokes {@link IViewRule#getDisplayName()} on the rule matching the specified element. |
| * |
| * @param element The view element to target. Can be null. |
| * @return Null if the rule failed, there's no rule or the rule does not want to override |
| * the display name. Otherwise, a string as returned by the rule. |
| */ |
| public String callGetDisplayName(UiViewElementNode element) { |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(element); |
| |
| if (rule != null) { |
| try { |
| return rule.getDisplayName(); |
| |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.getDisplayName() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Invokes {@link IViewRule#addContextMenuActions(List, INode)} on the rule matching the specified element. |
| * |
| * @param selectedNode The node selected. Never null. |
| * @return Null if the rule failed, there's no rule or the rule does not provide |
| * any custom menu actions. Otherwise, a list of {@link RuleAction}. |
| */ |
| @Nullable |
| public List<RuleAction> callGetContextMenu(NodeProxy selectedNode) { |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(selectedNode.getNode()); |
| |
| if (rule != null) { |
| try { |
| mInsertType = InsertType.CREATE; |
| List<RuleAction> actions = new ArrayList<RuleAction>(); |
| rule.addContextMenuActions(actions, selectedNode); |
| Collections.sort(actions); |
| |
| return actions; |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.getContextMenu() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Calls the selected node to return its default action |
| * |
| * @param selectedNode the node to apply the action to |
| * @return the default action id |
| */ |
| public String callGetDefaultActionId(@NonNull NodeProxy selectedNode) { |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(selectedNode.getNode()); |
| |
| if (rule != null) { |
| try { |
| mInsertType = InsertType.CREATE; |
| return rule.getDefaultActionId(selectedNode); |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.getDefaultAction() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Invokes {@link IViewRule#addLayoutActions(List, INode, List)} on the rule |
| * matching the specified element. |
| * |
| * @param actions The list of actions to add layout actions into |
| * @param parentNode The layout node |
| * @param children The selected children of the node, if any (used to |
| * initialize values of child layout controls, if applicable) |
| * @return Null if the rule failed, there's no rule or the rule does not |
| * provide any custom menu actions. Otherwise, a list of |
| * {@link RuleAction}. |
| */ |
| public List<RuleAction> callAddLayoutActions(List<RuleAction> actions, |
| NodeProxy parentNode, List<NodeProxy> children ) { |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(parentNode.getNode()); |
| |
| if (rule != null) { |
| try { |
| mInsertType = InsertType.CREATE; |
| rule.addLayoutActions(actions, parentNode, children); |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.getContextMenu() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Invokes {@link IViewRule#getSelectionHint(INode, INode)} |
| * on the rule matching the specified element. |
| * |
| * @param parentNode The parent of the node selected. Never null. |
| * @param childNode The child node that was selected. Never null. |
| * @return a list of strings to be displayed, or null or empty to display nothing |
| */ |
| public List<String> callGetSelectionHint(NodeProxy parentNode, NodeProxy childNode) { |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(parentNode.getNode()); |
| |
| if (rule != null) { |
| try { |
| return rule.getSelectionHint(parentNode, childNode); |
| |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.getSelectionHint() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| |
| return null; |
| } |
| |
| public void callPaintSelectionFeedback(GCWrapper gcWrapper, NodeProxy parentNode, |
| List<? extends INode> childNodes, Object view) { |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(parentNode.getNode()); |
| |
| if (rule != null) { |
| try { |
| rule.paintSelectionFeedback(gcWrapper, parentNode, childNodes, view); |
| |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.callPaintSelectionFeedback() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| } |
| |
| /** |
| * Called when the d'n'd starts dragging over the target node. |
| * If interested, returns a DropFeedback passed to onDrop/Move/Leave/Paint. |
| * If not interested in drop, return false. |
| * Followed by a paint. |
| */ |
| public DropFeedback callOnDropEnter(NodeProxy targetNode, |
| Object targetView, IDragElement[] elements) { |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(targetNode.getNode()); |
| |
| if (rule != null) { |
| try { |
| return rule.onDropEnter(targetNode, targetView, elements); |
| |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.onDropEnter() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Called after onDropEnter. |
| * Returns a DropFeedback passed to onDrop/Move/Leave/Paint (typically same |
| * as input one). |
| */ |
| public DropFeedback callOnDropMove(NodeProxy targetNode, |
| IDragElement[] elements, |
| DropFeedback feedback, |
| Point where) { |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(targetNode.getNode()); |
| |
| if (rule != null) { |
| try { |
| return rule.onDropMove(targetNode, elements, feedback, where); |
| |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.onDropMove() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Called when drop leaves the target without actually dropping |
| */ |
| public void callOnDropLeave(NodeProxy targetNode, |
| IDragElement[] elements, |
| DropFeedback feedback) { |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(targetNode.getNode()); |
| |
| if (rule != null) { |
| try { |
| rule.onDropLeave(targetNode, elements, feedback); |
| |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.onDropLeave() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| } |
| |
| /** |
| * Called when drop is released over the target to perform the actual drop. |
| */ |
| public void callOnDropped(NodeProxy targetNode, |
| IDragElement[] elements, |
| DropFeedback feedback, |
| Point where, |
| InsertType insertType) { |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(targetNode.getNode()); |
| |
| if (rule != null) { |
| try { |
| mInsertType = insertType; |
| rule.onDropped(targetNode, elements, feedback, where); |
| |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.onDropped() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| } |
| |
| /** |
| * Called when a paint has been requested via DropFeedback. |
| */ |
| public void callDropFeedbackPaint(IGraphics gc, |
| NodeProxy targetNode, |
| DropFeedback feedback) { |
| if (gc != null && feedback != null && feedback.painter != null) { |
| try { |
| feedback.painter.paint(gc, targetNode, feedback); |
| } catch (Exception e) { |
| AdtPlugin.log(e, "DropFeedback.painter failed: %s", |
| e.toString()); |
| } |
| } |
| } |
| |
| /** |
| * Called when pasting elements in an existing document on the selected target. |
| * |
| * @param targetNode The first node selected. |
| * @param targetView The view object for the target node, or null if not known |
| * @param pastedElements The elements being pasted. |
| * @return the parent node the paste was applied into |
| */ |
| public NodeProxy callOnPaste(NodeProxy targetNode, Object targetView, |
| SimpleElement[] pastedElements) { |
| |
| // Find a target which accepts children. If you for example select a button |
| // and attempt to paste, this will reselect the parent of the button as the paste |
| // target. (This is a loop rather than just checking the direct parent since |
| // we will soon ask each child whether they are *willing* to accept the new child. |
| // A ScrollView for example, which only accepts one child, might also say no |
| // and delegate to its parent in turn. |
| INode parent = targetNode; |
| while (parent instanceof NodeProxy) { |
| NodeProxy np = (NodeProxy) parent; |
| if (np.getNode() != null && np.getNode().getDescriptor() != null) { |
| ElementDescriptor descriptor = np.getNode().getDescriptor(); |
| if (descriptor.hasChildren()) { |
| targetNode = np; |
| break; |
| } |
| } |
| parent = parent.getParent(); |
| } |
| |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(targetNode.getNode()); |
| |
| if (rule != null) { |
| try { |
| mInsertType = InsertType.PASTE; |
| rule.onPaste(targetNode, targetView, pastedElements); |
| |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.onPaste() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| |
| return targetNode; |
| } |
| |
| // ---- Resize operations ---- |
| |
| public DropFeedback callOnResizeBegin(NodeProxy child, NodeProxy parent, Rect newBounds, |
| SegmentType horizontalEdge, SegmentType verticalEdge, Object childView, |
| Object parentView) { |
| IViewRule rule = loadRule(parent.getNode()); |
| |
| if (rule != null) { |
| try { |
| return rule.onResizeBegin(child, parent, horizontalEdge, verticalEdge, |
| childView, parentView); |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.onResizeBegin() failed: %s", rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| |
| return null; |
| } |
| |
| public void callOnResizeUpdate(DropFeedback feedback, NodeProxy child, NodeProxy parent, |
| Rect newBounds, int modifierMask) { |
| IViewRule rule = loadRule(parent.getNode()); |
| |
| if (rule != null) { |
| try { |
| rule.onResizeUpdate(feedback, child, parent, newBounds, modifierMask); |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.onResizeUpdate() failed: %s", rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| } |
| |
| public void callOnResizeEnd(DropFeedback feedback, NodeProxy child, NodeProxy parent, |
| Rect newBounds) { |
| IViewRule rule = loadRule(parent.getNode()); |
| |
| if (rule != null) { |
| try { |
| rule.onResizeEnd(feedback, child, parent, newBounds); |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.onResizeEnd() failed: %s", rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| } |
| |
| // ---- Creation customizations ---- |
| |
| /** |
| * Invokes the create hooks ({@link IViewRule#onCreate}, |
| * {@link IViewRule#onChildInserted} when a new child has been created/pasted/moved, and |
| * is inserted into a given parent. The parent may be null (for example when rendering |
| * top level items for preview). |
| * |
| * @param editor the XML editor to apply edits to the model for (performed by view |
| * rules) |
| * @param parentNode the parent XML node, or null if unknown |
| * @param childNode the XML node of the new node, never null |
| * @param overrideInsertType If not null, specifies an explicit insert type to use for |
| * edits made during the customization |
| */ |
| public void callCreateHooks( |
| AndroidXmlEditor editor, |
| NodeProxy parentNode, NodeProxy childNode, |
| InsertType overrideInsertType) { |
| IViewRule parentRule = null; |
| |
| if (parentNode != null) { |
| UiViewElementNode parentUiNode = parentNode.getNode(); |
| parentRule = loadRule(parentUiNode); |
| } |
| |
| if (overrideInsertType != null) { |
| mInsertType = overrideInsertType; |
| } |
| |
| UiViewElementNode newUiNode = childNode.getNode(); |
| IViewRule childRule = loadRule(newUiNode); |
| if (childRule != null || parentRule != null) { |
| callCreateHooks(editor, mInsertType, parentRule, parentNode, |
| childRule, childNode); |
| } |
| } |
| |
| private static void callCreateHooks( |
| final AndroidXmlEditor editor, final InsertType insertType, |
| final IViewRule parentRule, final INode parentNode, |
| final IViewRule childRule, final INode newNode) { |
| // Notify the parent about the new child in case it wants to customize it |
| // (For example, a ScrollView parent can go and set all its children's layout params to |
| // fill the parent.) |
| if (!editor.isEditXmlModelPending()) { |
| editor.wrapEditXmlModel(new Runnable() { |
| @Override |
| public void run() { |
| callCreateHooks(editor, insertType, |
| parentRule, parentNode, childRule, newNode); |
| } |
| }); |
| return; |
| } |
| |
| if (parentRule != null) { |
| parentRule.onChildInserted(newNode, parentNode, insertType); |
| } |
| |
| // Look up corresponding IViewRule, and notify the rule about |
| // this create action in case it wants to customize the new object. |
| // (For example, a rule for TabHosts can go and create a default child tab |
| // when you create it.) |
| if (childRule != null) { |
| childRule.onCreate(newNode, parentNode, insertType); |
| } |
| |
| if (parentNode != null) { |
| ((NodeProxy) parentNode).applyPendingChanges(); |
| } |
| } |
| |
| /** |
| * Set the type of insert currently in progress |
| * |
| * @param insertType the insert type to use for the next operation |
| */ |
| public void setInsertType(InsertType insertType) { |
| mInsertType = insertType; |
| } |
| |
| /** |
| * Return the type of insert currently in progress |
| * |
| * @return the type of insert currently in progress |
| */ |
| public InsertType getInsertType() { |
| return mInsertType; |
| } |
| |
| // ---- Deletion ---- |
| |
| public void callOnRemovingChildren(NodeProxy parentNode, |
| List<INode> children) { |
| if (parentNode != null) { |
| UiViewElementNode parentUiNode = parentNode.getNode(); |
| IViewRule parentRule = loadRule(parentUiNode); |
| if (parentRule != null) { |
| try { |
| parentRule.onRemovingChildren(children, parentNode, |
| mInsertType == InsertType.MOVE_WITHIN); |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.onDispose() failed: %s", |
| parentRule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| } |
| } |
| |
| // ---- private --- |
| |
| /** |
| * Returns the descriptor for the base View class. |
| * This could be null if the SDK or the given platform target hasn't loaded yet. |
| */ |
| private ViewElementDescriptor getBaseViewDescriptor() { |
| Sdk currentSdk = Sdk.getCurrent(); |
| if (currentSdk != null) { |
| IAndroidTarget target = currentSdk.getTarget(mProject); |
| if (target != null) { |
| AndroidTargetData data = currentSdk.getTargetData(target); |
| return data.getLayoutDescriptors().getBaseViewDescriptor(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Clear the Rules cache. Calls onDispose() on each rule. |
| */ |
| private void clearCache() { |
| // The cache can contain multiple times the same rule instance for different |
| // keys (e.g. the UiViewElementNode key vs. the FQCN string key.) So transfer |
| // all values to a unique set. |
| HashSet<IViewRule> rules = new HashSet<IViewRule>(mRulesCache.values()); |
| |
| mRulesCache.clear(); |
| |
| for (IViewRule rule : rules) { |
| if (rule != null) { |
| try { |
| rule.onDispose(); |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.onDispose() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Checks whether the project class loader has changed, and if so |
| * unregisters any view rules that use classes from the old class loader. It |
| * then returns the class loader to be used. |
| */ |
| private ClassLoader updateClassLoader() { |
| ClassLoader classLoader = mRuleLoader.getClassLoader(); |
| if (mUserClassLoader != null && classLoader != mUserClassLoader) { |
| // We have to unload all the IViewRules from the old class |
| List<Object> dispose = new ArrayList<Object>(); |
| for (Map.Entry<Object, IViewRule> entry : mRulesCache.entrySet()) { |
| IViewRule rule = entry.getValue(); |
| if (rule.getClass().getClassLoader() == mUserClassLoader) { |
| dispose.add(entry.getKey()); |
| } |
| } |
| for (Object object : dispose) { |
| mRulesCache.remove(object); |
| } |
| } |
| |
| mUserClassLoader = classLoader; |
| return mUserClassLoader; |
| } |
| |
| /** |
| * Load a rule using its descriptor. This will try to first load the rule using its |
| * actual FQCN and if that fails will find the first parent that works in the view |
| * hierarchy. |
| */ |
| private IViewRule loadRule(UiViewElementNode element) { |
| if (element == null) { |
| return null; |
| } |
| |
| String targetFqcn = null; |
| ViewElementDescriptor targetDesc = null; |
| |
| ElementDescriptor d = element.getDescriptor(); |
| if (d instanceof ViewElementDescriptor) { |
| targetDesc = (ViewElementDescriptor) d; |
| } |
| if (d == null || !(d instanceof ViewElementDescriptor)) { |
| // This should not happen. All views should have some kind of *view* element |
| // descriptor. Maybe the project is not complete and doesn't build or something. |
| // In this case, we'll use the descriptor of the base android View class. |
| targetDesc = getBaseViewDescriptor(); |
| } |
| |
| // Check whether any of the custom view .jar files have changed and if so |
| // unregister previously cached view rules to force a new view rule to be loaded. |
| updateClassLoader(); |
| |
| // Return the rule if we find it in the cache, even if it was stored as null |
| // (which means we didn't find it earlier, so don't look for it again) |
| IViewRule rule = mRulesCache.get(targetDesc); |
| if (rule != null || mRulesCache.containsKey(targetDesc)) { |
| return rule; |
| } |
| |
| // Get the descriptor and loop through the super class hierarchy |
| for (ViewElementDescriptor desc = targetDesc; |
| desc != null; |
| desc = desc.getSuperClassDesc()) { |
| |
| // Get the FQCN of this View |
| String fqcn = desc.getFullClassName(); |
| if (fqcn == null) { |
| // Shouldn't be happening. |
| return null; |
| } |
| |
| // The first time we keep the FQCN around as it's the target class we were |
| // initially trying to load. After, as we move through the hierarchy, the |
| // target FQCN remains constant. |
| if (targetFqcn == null) { |
| targetFqcn = fqcn; |
| } |
| |
| if (fqcn.indexOf('.') == -1) { |
| // Deal with unknown descriptors; these lack the full qualified path and |
| // elements in the layout without a package are taken to be in the |
| // android.widget package. |
| fqcn = ANDROID_WIDGET_PREFIX + fqcn; |
| } |
| |
| // Try to find a rule matching the "real" FQCN. If we find it, we're done. |
| // If not, the for loop will move to the parent descriptor. |
| rule = loadRule(fqcn, targetFqcn); |
| if (rule != null) { |
| // We found one. |
| // As a side effect, loadRule() also cached the rule using the target FQCN. |
| return rule; |
| } |
| } |
| |
| // Memorize in the cache that we couldn't find a rule for this descriptor |
| mRulesCache.put(targetDesc, null); |
| return null; |
| } |
| |
| /** |
| * Try to load a rule given a specific FQCN. This looks for an exact match in either |
| * the ADT scripts or the project scripts and does not look at parent hierarchy. |
| * <p/> |
| * Once a rule is found (or not), it is stored in a cache using its target FQCN |
| * so we don't try to reload it. |
| * <p/> |
| * The real FQCN is the actual rule class we're loading, e.g. "android.view.View" |
| * where target FQCN is the class we were initially looking for, which might be the same as |
| * the real FQCN or might be a derived class, e.g. "android.widget.TextView". |
| * |
| * @param realFqcn The FQCN of the rule class actually being loaded. |
| * @param targetFqcn The FQCN of the class actually processed, which might be different from |
| * the FQCN of the rule being loaded. |
| */ |
| IViewRule loadRule(String realFqcn, String targetFqcn) { |
| if (realFqcn == null || targetFqcn == null) { |
| return null; |
| } |
| |
| // Return the rule if we find it in the cache, even if it was stored as null |
| // (which means we didn't find it earlier, so don't look for it again) |
| IViewRule rule = mRulesCache.get(realFqcn); |
| if (rule != null || mRulesCache.containsKey(realFqcn)) { |
| return rule; |
| } |
| |
| // Look for class via reflection |
| try { |
| // For now, we package view rules for the builtin Android views and |
| // widgets with the tool in a special package, so look there rather |
| // than in the same package as the widgets. |
| String ruleClassName; |
| ClassLoader classLoader; |
| if (realFqcn.startsWith("android.") || //$NON-NLS-1$ |
| realFqcn.equals(VIEW_MERGE) || |
| realFqcn.endsWith(".GridLayout") || //$NON-NLS-1$ // Temporary special case |
| // FIXME: Remove this special case as soon as we pull |
| // the MapViewRule out of this code base and bundle it |
| // with the add ons |
| realFqcn.startsWith("com.google.android.maps.")) { //$NON-NLS-1$ |
| // This doesn't handle a case where there are name conflicts |
| // (e.g. where there are multiple different views with the same |
| // class name and only differing in package names, but that's a |
| // really bad practice in the first place, and if that situation |
| // should come up in the API we can enhance this algorithm. |
| String packageName = ViewRule.class.getName(); |
| packageName = packageName.substring(0, packageName.lastIndexOf('.')); |
| classLoader = RulesEngine.class.getClassLoader(); |
| int dotIndex = realFqcn.lastIndexOf('.'); |
| String baseName = realFqcn.substring(dotIndex+1); |
| // Capitalize rule class name to match naming conventions, if necessary (<merge>) |
| if (Character.isLowerCase(baseName.charAt(0))) { |
| if (baseName.equals(VIEW_TAG)) { |
| // Hack: ViewRule is generic for the "View" class, so we can't use it |
| // for the special XML "view" tag (lowercase); instead, the rule is |
| // named "ViewTagRule" instead. |
| baseName = "ViewTag"; //$NON-NLS-1$ |
| } |
| baseName = Character.toUpperCase(baseName.charAt(0)) + baseName.substring(1); |
| } |
| ruleClassName = packageName + "." + //$NON-NLS-1$ |
| baseName + "Rule"; //$NON-NLS-1$ |
| } else { |
| // Initialize the user-classpath for 3rd party IViewRules, if necessary |
| classLoader = updateClassLoader(); |
| if (classLoader == null) { |
| // The mUserClassLoader can be null; this is the typical scenario, |
| // when the user is only using builtin layout rules. |
| // This means however we can't resolve this fqcn since it's not |
| // in the name space of the builtin rules. |
| mRulesCache.put(realFqcn, null); |
| return null; |
| } |
| |
| // For other (3rd party) widgets, look in the same package (though most |
| // likely not in the same jar!) |
| ruleClassName = realFqcn + "Rule"; //$NON-NLS-1$ |
| } |
| |
| Class<?> clz = Class.forName(ruleClassName, true, classLoader); |
| rule = (IViewRule) clz.newInstance(); |
| return initializeRule(rule, targetFqcn); |
| } catch (ClassNotFoundException ex) { |
| // Not an unexpected error - this means that there isn't a helper for this |
| // class. |
| } catch (InstantiationException e) { |
| // This is NOT an expected error: fail. |
| AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString()); |
| } catch (IllegalAccessException e) { |
| // This is NOT an expected error: fail. |
| AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString()); |
| } |
| |
| // Memorize in the cache that we couldn't find a rule for this real FQCN |
| mRulesCache.put(realFqcn, null); |
| return null; |
| } |
| |
| /** |
| * Initialize a rule we just loaded. The rule has a chance to examine the target FQCN |
| * and bail out. |
| * <p/> |
| * Contract: the rule is not in the {@link #mRulesCache} yet and this method will |
| * cache it using the target FQCN if the rule is accepted. |
| * <p/> |
| * The real FQCN is the actual rule class we're loading, e.g. "android.view.View" |
| * where target FQCN is the class we were initially looking for, which might be the same as |
| * the real FQCN or might be a derived class, e.g. "android.widget.TextView". |
| * |
| * @param rule A rule freshly loaded. |
| * @param targetFqcn The FQCN of the class actually processed, which might be different from |
| * the FQCN of the rule being loaded. |
| * @return The rule if accepted, or null if the rule can't handle that FQCN. |
| */ |
| private IViewRule initializeRule(IViewRule rule, String targetFqcn) { |
| |
| try { |
| if (rule.onInitialize(targetFqcn, new ClientRulesEngine(this, targetFqcn))) { |
| // Add it to the cache and return it |
| mRulesCache.put(targetFqcn, rule); |
| return rule; |
| } else { |
| rule.onDispose(); |
| } |
| } catch (Exception e) { |
| AdtPlugin.log(e, "%s.onInit() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| |
| return null; |
| } |
| } |