Home | History | Annotate | Download | only in layout
      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      */
    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 
    135     protected UiElementNode getCurrentNode() {
    136         if (mNodeStack.size() > 0) {
    137             return mNodeStack.get(mNodeStack.size()-1);
    138         }
    139 
    140         return null;
    141     }
    142 
    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 
    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 
    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
    202     public Object getViewCookie() {
    203         return getCurrentNode();
    204     }
    205 
    206     /**
    207      * Legacy method required by {@link com.android.layoutlib.api.IXmlPullParser}
    208      */
    209     @Override
    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
    219     public ILayoutPullParser getParser(String layoutName) {
    220         return null;
    221     }
    222 
    223     // ------------- XmlPullParser --------
    224 
    225     @Override
    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
    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
    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
    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
    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
    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
    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
    412     public int getDepth() {
    413         return mNodeStack.size();
    414     }
    415 
    416     @Override
    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
    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
    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
    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
    467     public void onNextFromStartDocument() {
    468         onNextFromStartTag();
    469     }
    470 
    471     @Override
    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
    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 
    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      */
    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      */
    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 
    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