1 /*
2  * Copyright (C) 2008 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;
18 
19 import static com.android.SdkConstants.ANDROID_URI;
20 import static com.android.SdkConstants.ATTR_LAYOUT;
21 import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
22 import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
23 import static com.android.SdkConstants.ATTR_PADDING;
24 import static com.android.SdkConstants.AUTO_URI;
25 import static com.android.SdkConstants.UNIT_DIP;
26 import static com.android.SdkConstants.UNIT_DP;
27 import static com.android.SdkConstants.UNIT_IN;
28 import static com.android.SdkConstants.UNIT_MM;
29 import static com.android.SdkConstants.UNIT_PT;
30 import static com.android.SdkConstants.UNIT_PX;
31 import static com.android.SdkConstants.UNIT_SP;
32 import static com.android.SdkConstants.VALUE_FILL_PARENT;
33 import static com.android.SdkConstants.VALUE_MATCH_PARENT;
34 import static com.android.SdkConstants.VIEW_FRAGMENT;
35 import static com.android.SdkConstants.VIEW_INCLUDE;
36 
37 import com.android.ide.common.rendering.api.ILayoutPullParser;
38 import com.android.ide.common.rendering.api.ViewInfo;
39 import com.android.ide.common.res2.ValueXmlHelper;
40 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
41 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
42 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.FragmentMenu;
43 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
44 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
45 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
46 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
47 import com.android.resources.Density;
48 import com.android.sdklib.IAndroidTarget;
49 
50 import org.eclipse.core.resources.IProject;
51 import org.w3c.dom.Document;
52 import org.w3c.dom.NamedNodeMap;
53 import org.w3c.dom.Node;
54 import org.xmlpull.v1.XmlPullParserException;
55 
56 import java.util.ArrayList;
57 import java.util.Collection;
58 import java.util.List;
59 import java.util.Set;
60 import java.util.regex.Matcher;
61 import java.util.regex.Pattern;
62 
63 /**
64  * {@link ILayoutPullParser} implementation on top of {@link UiElementNode}.
65  * <p/>
66  * It's designed to work on layout files, and will most likely not work on other resource files.
67  * <p/>
68  * This pull parser generates {@link ViewInfo}s which key is a {@link UiElementNode}.
69  */
70 public class UiElementPullParser extends BasePullParser {
71     private final static Pattern FLOAT_PATTERN = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); //$NON-NLS-1$
72 
73     private final int[] sIntOut = new int[1];
74 
75     private final ArrayList<UiElementNode> mNodeStack = new ArrayList<UiElementNode>();
76     private UiElementNode mRoot;
77     private final boolean mExplodedRendering;
78     private boolean mZeroAttributeIsPadding = false;
79     private boolean mIncreaseExistingPadding = false;
80     private LayoutDescriptors mDescriptors;
81     private final Density mDensity;
82 
83     /**
84      * Number of pixels to pad views with in exploded-rendering mode.
85      */
86     private static final String DEFAULT_PADDING_VALUE =
87         ExplodedRenderingHelper.PADDING_VALUE + UNIT_PX;
88 
89     /**
90      * Number of pixels to pad exploded individual views with. (This is HALF the width of the
91      * rectangle since padding is repeated on both sides of the empty content.)
92      */
93     private static final String FIXED_PADDING_VALUE = "20px"; //$NON-NLS-1$
94 
95     /**
96      * Set of nodes that we want to auto-pad using {@link #FIXED_PADDING_VALUE} as the padding
97      * attribute value. Can be null, which is the case when we don't want to perform any
98      * <b>individual</b> node exploding.
99      */
100     private final Set<UiElementNode> mExplodeNodes;
101 
102     /**
103      * Constructs a new {@link UiElementPullParser}, a parser dedicated to the special case of
104      * parsing a layout resource files, and handling "exploded rendering" - adding padding on views
105      * to make them easier to see and operate on.
106      *
107      * @param top The {@link UiElementNode} for the root node.
108      * @param explodeRendering When true, add padding to <b>all</b> nodes in the hierarchy. This
109      *            will add rather than replace padding of a node.
110      * @param explodeNodes A set of individual nodes that should be assigned a fixed amount of
111      *            padding ({@link #FIXED_PADDING_VALUE}). This is intended for use with nodes that
112      *            (without padding) would be invisible. This parameter can be null, in which case
113      *            nodes are not individually exploded (but they may all be exploded with the
114      *            explodeRendering parameter.
115      * @param density the density factor for the screen.
116      * @param project Project containing this layout.
117      */
UiElementPullParser(UiElementNode top, boolean explodeRendering, Set<UiElementNode> explodeNodes, Density density, IProject project)118     public UiElementPullParser(UiElementNode top, boolean explodeRendering,
119             Set<UiElementNode> explodeNodes,
120             Density density, IProject project) {
121         super();
122         mRoot = top;
123         mExplodedRendering = explodeRendering;
124         mExplodeNodes = explodeNodes;
125         mDensity = density;
126         if (mExplodedRendering) {
127             // get the layout descriptor
128             IAndroidTarget target = Sdk.getCurrent().getTarget(project);
129             AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
130             mDescriptors = data.getLayoutDescriptors();
131         }
132         push(mRoot);
133     }
134 
getCurrentNode()135     protected UiElementNode getCurrentNode() {
136         if (mNodeStack.size() > 0) {
137             return mNodeStack.get(mNodeStack.size()-1);
138         }
139 
140         return null;
141     }
142 
getAttribute(int i)143     private Node getAttribute(int i) {
144         if (mParsingState != START_TAG) {
145             throw new IndexOutOfBoundsException();
146         }
147 
148         // get the current uiNode
149         UiElementNode uiNode = getCurrentNode();
150 
151         // get its xml node
152         Node xmlNode = uiNode.getXmlNode();
153 
154         if (xmlNode != null) {
155             return xmlNode.getAttributes().item(i);
156         }
157 
158         return null;
159     }
160 
push(UiElementNode node)161     private void push(UiElementNode node) {
162         mNodeStack.add(node);
163 
164         mZeroAttributeIsPadding = false;
165         mIncreaseExistingPadding = false;
166 
167         if (mExplodedRendering) {
168             // first get the node name
169             String xml = node.getDescriptor().getXmlLocalName();
170             ViewElementDescriptor descriptor = mDescriptors.findDescriptorByTag(xml);
171             if (descriptor != null) {
172                 NamedNodeMap attributes = node.getXmlNode().getAttributes();
173                 Node padding = attributes.getNamedItemNS(ANDROID_URI, ATTR_PADDING);
174                 if (padding == null) {
175                     // we'll return an extra padding
176                     mZeroAttributeIsPadding = true;
177                 } else {
178                     mIncreaseExistingPadding = true;
179                 }
180             }
181         }
182     }
183 
pop()184     private UiElementNode pop() {
185         return mNodeStack.remove(mNodeStack.size()-1);
186     }
187 
188     // ------------- IXmlPullParser --------
189 
190     /**
191      * {@inheritDoc}
192      * <p/>
193      * This implementation returns the underlying DOM node of type {@link UiElementNode}.
194      * Note that the link between the GLE and the parsing code depends on this being the actual
195      * type returned, so you can't just randomly change it here.
196      * <p/>
197      * Currently used by:
198      * - private method GraphicalLayoutEditor#updateNodeWithBounds(ILayoutViewInfo).
199      * - private constructor of LayoutCanvas.CanvasViewInfo.
200      */
201     @Override
getViewCookie()202     public Object getViewCookie() {
203         return getCurrentNode();
204     }
205 
206     /**
207      * Legacy method required by {@link com.android.layoutlib.api.IXmlPullParser}
208      */
209     @Override
getViewKey()210     public Object getViewKey() {
211         return getViewCookie();
212     }
213 
214     /**
215      * This implementation does nothing for now as all the embedded XML will use a normal KXML
216      * parser.
217      */
218     @Override
getParser(String layoutName)219     public ILayoutPullParser getParser(String layoutName) {
220         return null;
221     }
222 
223     // ------------- XmlPullParser --------
224 
225     @Override
getPositionDescription()226     public String getPositionDescription() {
227         return "XML DOM element depth:" + mNodeStack.size();
228     }
229 
230     /*
231      * This does not seem to be called by the layoutlib, but we keep this (and maintain
232      * it) just in case.
233      */
234     @Override
getAttributeCount()235     public int getAttributeCount() {
236         UiElementNode node = getCurrentNode();
237 
238         if (node != null) {
239             Collection<UiAttributeNode> attributes = node.getAllUiAttributes();
240             int count = attributes.size();
241 
242             return count + (mZeroAttributeIsPadding ? 1 : 0);
243         }
244 
245         return 0;
246     }
247 
248     /*
249      * This does not seem to be called by the layoutlib, but we keep this (and maintain
250      * it) just in case.
251      */
252     @Override
getAttributeName(int i)253     public String getAttributeName(int i) {
254         if (mZeroAttributeIsPadding) {
255             if (i == 0) {
256                 return ATTR_PADDING;
257             } else {
258                 i--;
259             }
260         }
261 
262         Node attribute = getAttribute(i);
263         if (attribute != null) {
264             return attribute.getLocalName();
265         }
266 
267         return null;
268     }
269 
270     /*
271      * This does not seem to be called by the layoutlib, but we keep this (and maintain
272      * it) just in case.
273      */
274     @Override
getAttributeNamespace(int i)275     public String getAttributeNamespace(int i) {
276         if (mZeroAttributeIsPadding) {
277             if (i == 0) {
278                 return ANDROID_URI;
279             } else {
280                 i--;
281             }
282         }
283 
284         Node attribute = getAttribute(i);
285         if (attribute != null) {
286             return attribute.getNamespaceURI();
287         }
288         return ""; //$NON-NLS-1$
289     }
290 
291     /*
292      * This does not seem to be called by the layoutlib, but we keep this (and maintain
293      * it) just in case.
294      */
295     @Override
getAttributePrefix(int i)296     public String getAttributePrefix(int i) {
297         if (mZeroAttributeIsPadding) {
298             if (i == 0) {
299                 // figure out the prefix associated with the android namespace.
300                 Document doc = mRoot.getXmlDocument();
301                 return doc.lookupPrefix(ANDROID_URI);
302             } else {
303                 i--;
304             }
305         }
306 
307         Node attribute = getAttribute(i);
308         if (attribute != null) {
309             return attribute.getPrefix();
310         }
311         return null;
312     }
313 
314     /*
315      * This does not seem to be called by the layoutlib, but we keep this (and maintain
316      * it) just in case.
317      */
318     @Override
getAttributeValue(int i)319     public String getAttributeValue(int i) {
320         if (mZeroAttributeIsPadding) {
321             if (i == 0) {
322                 return DEFAULT_PADDING_VALUE;
323             } else {
324                 i--;
325             }
326         }
327 
328         Node attribute = getAttribute(i);
329         if (attribute != null) {
330             String value = attribute.getNodeValue();
331             if (mIncreaseExistingPadding && ATTR_PADDING.equals(attribute.getLocalName()) &&
332                     ANDROID_URI.equals(attribute.getNamespaceURI())) {
333                 // add the padding and return the value
334                 return addPaddingToValue(value);
335             }
336             return value;
337         }
338 
339         return null;
340     }
341 
342     /*
343      * This is the main method used by the LayoutInflater to query for attributes.
344      */
345     @Override
getAttributeValue(String namespace, String localName)346     public String getAttributeValue(String namespace, String localName) {
347         if (mExplodeNodes != null && ATTR_PADDING.equals(localName) &&
348                 ANDROID_URI.equals(namespace)) {
349             UiElementNode node = getCurrentNode();
350             if (node != null && mExplodeNodes.contains(node)) {
351                 return FIXED_PADDING_VALUE;
352             }
353         }
354 
355         if (mZeroAttributeIsPadding && ATTR_PADDING.equals(localName) &&
356                 ANDROID_URI.equals(namespace)) {
357             return DEFAULT_PADDING_VALUE;
358         }
359 
360         // get the current uiNode
361         UiElementNode uiNode = getCurrentNode();
362 
363         // get its xml node
364         Node xmlNode = uiNode.getXmlNode();
365 
366         if (xmlNode != null) {
367             if (ATTR_LAYOUT.equals(localName) && VIEW_FRAGMENT.equals(xmlNode.getNodeName())) {
368                 String layout = FragmentMenu.getFragmentLayout(xmlNode);
369                 if (layout != null) {
370                     return layout;
371                 }
372             }
373 
374             Node attribute = xmlNode.getAttributes().getNamedItemNS(namespace, localName);
375 
376             // Auto-convert http://schemas.android.com/apk/res-auto resources. The lookup
377             // will be for the current application's resource package, e.g.
378             // http://schemas.android.com/apk/res/foo.bar, but the XML document will
379             // be using http://schemas.android.com/apk/res-auto in library projects:
380             if (attribute == null && namespace != null && !namespace.equals(ANDROID_URI)) {
381                 attribute = xmlNode.getAttributes().getNamedItemNS(AUTO_URI, localName);
382             }
383 
384             if (attribute != null) {
385                 String value = attribute.getNodeValue();
386                 if (mIncreaseExistingPadding && ATTR_PADDING.equals(localName) &&
387                         ANDROID_URI.equals(namespace)) {
388                     // add the padding and return the value
389                     return addPaddingToValue(value);
390                 }
391 
392                 // on the fly convert match_parent to fill_parent for compatibility with older
393                 // platforms.
394                 if (VALUE_MATCH_PARENT.equals(value) &&
395                         (ATTR_LAYOUT_WIDTH.equals(localName) ||
396                                 ATTR_LAYOUT_HEIGHT.equals(localName)) &&
397                         ANDROID_URI.equals(namespace)) {
398                     return VALUE_FILL_PARENT;
399                 }
400 
401                 // Handle unicode escapes etc
402                 value = ValueXmlHelper.unescapeResourceString(value, false, false);
403 
404                 return value;
405             }
406         }
407 
408         return null;
409     }
410 
411     @Override
getDepth()412     public int getDepth() {
413         return mNodeStack.size();
414     }
415 
416     @Override
getName()417     public String getName() {
418         if (mParsingState == START_TAG || mParsingState == END_TAG) {
419             String name = getCurrentNode().getDescriptor().getXmlLocalName();
420 
421             if (name.equals(VIEW_FRAGMENT)) {
422                 // Temporarily translate <fragment> to <include> (and in getAttribute
423                 // we will also provide a layout-attribute for the corresponding
424                 // fragment name attribute)
425                 String layout = FragmentMenu.getFragmentLayout(getCurrentNode().getXmlNode());
426                 if (layout != null) {
427                     return VIEW_INCLUDE;
428                 }
429             }
430 
431             return name;
432         }
433 
434         return null;
435     }
436 
437     @Override
getNamespace()438     public String getNamespace() {
439         if (mParsingState == START_TAG || mParsingState == END_TAG) {
440             return getCurrentNode().getDescriptor().getNamespace();
441         }
442 
443         return null;
444     }
445 
446     @Override
getPrefix()447     public String getPrefix() {
448         if (mParsingState == START_TAG || mParsingState == END_TAG) {
449             Document doc = mRoot.getXmlDocument();
450             return doc.lookupPrefix(getCurrentNode().getDescriptor().getNamespace());
451         }
452 
453         return null;
454     }
455 
456     @Override
isEmptyElementTag()457     public boolean isEmptyElementTag() throws XmlPullParserException {
458         if (mParsingState == START_TAG) {
459             return getCurrentNode().getUiChildren().size() == 0;
460         }
461 
462         throw new XmlPullParserException("Call to isEmptyElementTag while not in START_TAG",
463                 this, null);
464     }
465 
466     @Override
onNextFromStartDocument()467     public void onNextFromStartDocument() {
468         onNextFromStartTag();
469     }
470 
471     @Override
onNextFromStartTag()472     public void onNextFromStartTag() {
473         // get the current node, and look for text or children (children first)
474         UiElementNode node = getCurrentNode();
475         List<UiElementNode> children = node.getUiChildren();
476         if (children.size() > 0) {
477             // move to the new child, and don't change the state.
478             push(children.get(0));
479 
480             // in case the current state is CURRENT_DOC, we set the proper state.
481             mParsingState = START_TAG;
482         } else {
483             if (mParsingState == START_DOCUMENT) {
484                 // this handles the case where there's no node.
485                 mParsingState = END_DOCUMENT;
486             } else {
487                 mParsingState = END_TAG;
488             }
489         }
490     }
491 
492     @Override
onNextFromEndTag()493     public void onNextFromEndTag() {
494         // look for a sibling. if no sibling, go back to the parent
495         UiElementNode node = getCurrentNode();
496         node = node.getUiNextSibling();
497         if (node != null) {
498             // to go to the sibling, we need to remove the current node,
499             pop();
500             // and add its sibling.
501             push(node);
502             mParsingState = START_TAG;
503         } else {
504             // move back to the parent
505             pop();
506 
507             // we have only one element left (mRoot), then we're done with the document.
508             if (mNodeStack.size() == 1) {
509                 mParsingState = END_DOCUMENT;
510             } else {
511                 mParsingState = END_TAG;
512             }
513         }
514     }
515 
516     // ------- TypedValue stuff
517     // This is adapted from com.android.layoutlib.bridge.ResourceHelper
518     // (but modified to directly take the parsed value and convert it into pixel instead of
519     // storing it into a TypedValue)
520     // this was originally taken from platform/frameworks/base/libs/utils/ResourceTypes.cpp
521 
522     private static final class DimensionEntry {
523         String name;
524         int type;
525 
DimensionEntry(String name, int unit)526         DimensionEntry(String name, int unit) {
527             this.name = name;
528             this.type = unit;
529         }
530     }
531 
532     /** {@link DimensionEntry} complex unit: Value is raw pixels. */
533     private static final int COMPLEX_UNIT_PX = 0;
534     /** {@link DimensionEntry} complex unit: Value is Device Independent
535      *  Pixels. */
536     private static final int COMPLEX_UNIT_DIP = 1;
537     /** {@link DimensionEntry} complex unit: Value is a scaled pixel. */
538     private static final int COMPLEX_UNIT_SP = 2;
539     /** {@link DimensionEntry} complex unit: Value is in points. */
540     private static final int COMPLEX_UNIT_PT = 3;
541     /** {@link DimensionEntry} complex unit: Value is in inches. */
542     private static final int COMPLEX_UNIT_IN = 4;
543     /** {@link DimensionEntry} complex unit: Value is in millimeters. */
544     private static final int COMPLEX_UNIT_MM = 5;
545 
546     private final static DimensionEntry[] sDimensions = new DimensionEntry[] {
547         new DimensionEntry(UNIT_PX, COMPLEX_UNIT_PX),
548         new DimensionEntry(UNIT_DIP, COMPLEX_UNIT_DIP),
549         new DimensionEntry(UNIT_DP, COMPLEX_UNIT_DIP),
550         new DimensionEntry(UNIT_SP, COMPLEX_UNIT_SP),
551         new DimensionEntry(UNIT_PT, COMPLEX_UNIT_PT),
552         new DimensionEntry(UNIT_IN, COMPLEX_UNIT_IN),
553         new DimensionEntry(UNIT_MM, COMPLEX_UNIT_MM),
554     };
555 
556     /**
557      * Adds padding to an existing dimension.
558      * <p/>This will resolve the attribute value (which can be px, dip, dp, sp, pt, in, mm) to
559      * a pixel value, add the padding value ({@link ExplodedRenderingHelper#PADDING_VALUE}),
560      * and then return a string with the new value as a px string ("42px");
561      * If the conversion fails, only the special padding is returned.
562      */
addPaddingToValue(String s)563     private String addPaddingToValue(String s) {
564         int padding = ExplodedRenderingHelper.PADDING_VALUE;
565         if (stringToPixel(s)) {
566             padding += sIntOut[0];
567         }
568 
569         return padding + UNIT_PX;
570     }
571 
572     /**
573      * Convert the string into a pixel value, and puts it in {@link #sIntOut}
574      * @param s the dimension value from an XML attribute
575      * @return true if success.
576      */
stringToPixel(String s)577     private boolean stringToPixel(String s) {
578         // remove the space before and after
579         s = s.trim();
580         int len = s.length();
581 
582         if (len <= 0) {
583             return false;
584         }
585 
586         // check that there's no non ASCII characters.
587         char[] buf = s.toCharArray();
588         for (int i = 0 ; i < len ; i++) {
589             if (buf[i] > 255) {
590                 return false;
591             }
592         }
593 
594         // check the first character
595         if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.') {
596             return false;
597         }
598 
599         // now look for the string that is after the float...
600         Matcher m = FLOAT_PATTERN.matcher(s);
601         if (m.matches()) {
602             String f_str = m.group(1);
603             String end = m.group(2);
604 
605             float f;
606             try {
607                 f = Float.parseFloat(f_str);
608             } catch (NumberFormatException e) {
609                 // this shouldn't happen with the regexp above.
610                 return false;
611             }
612 
613             if (end.length() > 0 && end.charAt(0) != ' ') {
614                 // We only support dimension-type values, so try to parse the unit for dimension
615                 DimensionEntry dimension = parseDimension(end);
616                 if (dimension != null) {
617                     // convert the value into pixel based on the dimention type
618                     // This is similar to TypedValue.applyDimension()
619                     switch (dimension.type) {
620                         case COMPLEX_UNIT_PX:
621                             // do nothing, value is already in px
622                             break;
623                         case COMPLEX_UNIT_DIP:
624                         case COMPLEX_UNIT_SP: // intended fall-through since we don't
625                                               // adjust for font size
626                             f *= (float)mDensity.getDpiValue() / Density.DEFAULT_DENSITY;
627                             break;
628                         case COMPLEX_UNIT_PT:
629                             f *= mDensity.getDpiValue() * (1.0f / 72);
630                             break;
631                         case COMPLEX_UNIT_IN:
632                             f *= mDensity.getDpiValue();
633                             break;
634                         case COMPLEX_UNIT_MM:
635                             f *= mDensity.getDpiValue() * (1.0f / 25.4f);
636                             break;
637                     }
638 
639                     // store result (converted to int)
640                     sIntOut[0] = (int) (f + 0.5);
641 
642                     return true;
643                 }
644             }
645         }
646 
647         return false;
648     }
649 
parseDimension(String str)650     private static DimensionEntry parseDimension(String str) {
651         str = str.trim();
652 
653         for (DimensionEntry d : sDimensions) {
654             if (d.name.equals(str)) {
655                 return d;
656             }
657         }
658 
659         return null;
660     }
661 }
662