Home | History | Annotate | Download | only in android
      1 package org.robolectric.android;
      2 
      3 import static org.robolectric.res.AttributeResource.ANDROID_RES_NS_PREFIX;
      4 import static org.robolectric.res.AttributeResource.RES_AUTO_NS_URI;
      5 
      6 import android.content.res.Resources;
      7 import android.content.res.XmlResourceParser;
      8 import com.android.internal.util.XmlUtils;
      9 import java.io.IOException;
     10 import java.io.InputStream;
     11 import java.io.Reader;
     12 import java.util.Arrays;
     13 import java.util.List;
     14 import org.robolectric.res.AttributeResource;
     15 import org.robolectric.res.ResName;
     16 import org.robolectric.res.ResourceTable;
     17 import org.robolectric.res.StringResources;
     18 import org.w3c.dom.Document;
     19 import org.w3c.dom.Element;
     20 import org.w3c.dom.NamedNodeMap;
     21 import org.w3c.dom.Node;
     22 import org.xmlpull.v1.XmlPullParserException;
     23 
     24 /**
     25  * Concrete implementation of the {@link XmlResourceParser}.
     26  *
     27  * Clients expects a pull parser while the resource loader
     28  * initialise this object with a {@link Document}.
     29  * This implementation navigates the dom and emulates a pull
     30  * parser by raising all the opportune events.
     31  *
     32  * Note that the original android implementation is based on
     33  * a set of native methods calls. Here those methods are
     34  * re-implemented in java when possible.
     35  */
     36 public class XmlResourceParserImpl implements XmlResourceParser {
     37 
     38   /**
     39    * All the parser features currently supported by Android.
     40    */
     41   public static final String[] AVAILABLE_FEATURES = {
     42       XmlResourceParser.FEATURE_PROCESS_NAMESPACES,
     43       XmlResourceParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES
     44   };
     45   /**
     46    * All the parser features currently NOT supported by Android.
     47    */
     48   public static final String[] UNAVAILABLE_FEATURES = {
     49       XmlResourceParser.FEATURE_PROCESS_DOCDECL,
     50       XmlResourceParser.FEATURE_VALIDATION
     51   };
     52 
     53   private final Document document;
     54   private final String fileName;
     55   private final String packageName;
     56   private final ResourceTable resourceTable;
     57   private final String applicationNamespace;
     58 
     59   private Node currentNode;
     60 
     61   private boolean mStarted = false;
     62   private boolean mDecNextDepth = false;
     63   private int mDepth = 0;
     64   private int mEventType = START_DOCUMENT;
     65 
     66   public XmlResourceParserImpl(Document document, String fileName, String packageName,
     67                                String applicationPackageName, ResourceTable resourceTable) {
     68     this.document = document;
     69     this.fileName = fileName;
     70     this.packageName = packageName;
     71     this.resourceTable = resourceTable;
     72     this.applicationNamespace = ANDROID_RES_NS_PREFIX + applicationPackageName;
     73   }
     74 
     75   @Override
     76   public void setFeature(String name, boolean state)
     77       throws XmlPullParserException {
     78     if (isAndroidSupportedFeature(name) && state) {
     79       return;
     80     }
     81     throw new XmlPullParserException("Unsupported feature: " + name);
     82   }
     83 
     84   @Override
     85   public boolean getFeature(String name) {
     86     return isAndroidSupportedFeature(name);
     87   }
     88 
     89   @Override
     90   public void setProperty(String name, Object value)
     91       throws XmlPullParserException {
     92     throw new XmlPullParserException("setProperty() not supported");
     93   }
     94 
     95   @Override
     96   public Object getProperty(String name) {
     97     // Properties are not supported. Android returns null
     98     // instead of throwing an XmlPullParserException.
     99     return null;
    100   }
    101 
    102   @Override
    103   public void setInput(Reader in) throws XmlPullParserException {
    104     throw new XmlPullParserException("setInput() not supported");
    105   }
    106 
    107   @Override
    108   public void setInput(InputStream inputStream, String inputEncoding)
    109       throws XmlPullParserException {
    110     throw new XmlPullParserException("setInput() not supported");
    111   }
    112 
    113   @Override
    114   public void defineEntityReplacementText(
    115       String entityName, String replacementText)
    116       throws XmlPullParserException {
    117     throw new XmlPullParserException(
    118         "defineEntityReplacementText() not supported");
    119   }
    120 
    121   @Override
    122   public String getNamespacePrefix(int pos)
    123       throws XmlPullParserException {
    124     throw new XmlPullParserException(
    125         "getNamespacePrefix() not supported");
    126   }
    127 
    128   @Override
    129   public String getInputEncoding() {
    130     return null;
    131   }
    132 
    133   @Override
    134   public String getNamespace(String prefix) {
    135     throw new RuntimeException(
    136         "getNamespaceCount() not supported");
    137   }
    138 
    139   @Override
    140   public int getNamespaceCount(int depth)
    141       throws XmlPullParserException {
    142     throw new XmlPullParserException(
    143         "getNamespaceCount() not supported");
    144   }
    145 
    146   @Override
    147   public String getPositionDescription() {
    148     return "XML file " + fileName + " line #" + getLineNumber() + " (sorry, not yet implemented)";
    149   }
    150 
    151   @Override
    152   public String getNamespaceUri(int pos)
    153       throws XmlPullParserException {
    154     throw new XmlPullParserException(
    155         "getNamespaceUri() not supported");
    156   }
    157 
    158   @Override
    159   public int getColumnNumber() {
    160     // Android always returns -1
    161     return -1;
    162   }
    163 
    164   @Override
    165   public int getDepth() {
    166     return mDepth;
    167   }
    168 
    169   @Override
    170   public String getText() {
    171     if (currentNode == null) {
    172       return "";
    173     }
    174     return StringResources.processStringResources(currentNode.getTextContent());
    175   }
    176 
    177   @Override
    178   public int getLineNumber() {
    179     // TODO(msama): The current implementation is
    180     //   unable to return line numbers.
    181     return -1;
    182   }
    183 
    184   @Override
    185   public int getEventType()
    186       throws XmlPullParserException {
    187     return mEventType;
    188   }
    189 
    190   /*package*/
    191   public boolean isWhitespace(String text)
    192       throws XmlPullParserException {
    193     if (text == null) {
    194       return false;
    195     }
    196     return text.split("\\s").length == 0;
    197   }
    198 
    199   @Override
    200   public boolean isWhitespace()
    201       throws XmlPullParserException {
    202     // Note: in android whitespaces are automatically stripped.
    203     // Here we have to skip them manually
    204     return isWhitespace(getText());
    205   }
    206 
    207   @Override
    208   public String getPrefix() {
    209     throw new RuntimeException("getPrefix not supported");
    210   }
    211 
    212   @Override
    213   public char[] getTextCharacters(int[] holderForStartAndLength) {
    214     String txt = getText();
    215     char[] chars = null;
    216     if (txt != null) {
    217       holderForStartAndLength[0] = 0;
    218       holderForStartAndLength[1] = txt.length();
    219       chars = new char[txt.length()];
    220       txt.getChars(0, txt.length(), chars, 0);
    221     }
    222     return chars;
    223   }
    224 
    225   @Override
    226   public String getNamespace() {
    227     String namespace = currentNode != null ? currentNode.getNamespaceURI() : null;
    228     if (namespace == null) {
    229       return "";
    230     }
    231 
    232     return maybeReplaceNamespace(namespace);
    233   }
    234 
    235   @Override
    236   public String getName() {
    237     if (currentNode == null) {
    238       return null;
    239     }
    240     return currentNode.getNodeName();
    241   }
    242 
    243   Node getAttributeAt(int index) {
    244     if (currentNode == null) {
    245       throw new IndexOutOfBoundsException(String.valueOf(index));
    246     }
    247     NamedNodeMap map = currentNode.getAttributes();
    248     if (index >= map.getLength()) {
    249       throw new IndexOutOfBoundsException(String.valueOf(index));
    250     }
    251     return map.item(index);
    252   }
    253 
    254   public String getAttribute(String namespace, String name) {
    255     if (currentNode == null) {
    256       return null;
    257     }
    258 
    259     Element element = (Element) currentNode;
    260     if (element.hasAttributeNS(namespace, name)) {
    261       return element.getAttributeNS(namespace, name).trim();
    262     } else if (applicationNamespace.equals(namespace)
    263         && element.hasAttributeNS(AttributeResource.RES_AUTO_NS_URI, name)) {
    264       return element.getAttributeNS(AttributeResource.RES_AUTO_NS_URI, name).trim();
    265     }
    266 
    267     return null;
    268   }
    269 
    270   @Override
    271   public String getAttributeNamespace(int index) {
    272     Node attr = getAttributeAt(index);
    273     if (attr == null) {
    274       return "";
    275     }
    276     return maybeReplaceNamespace(attr.getNamespaceURI());
    277   }
    278 
    279   private String maybeReplaceNamespace(String namespace) {
    280     if (namespace == null) {
    281       return "";
    282     } else if (namespace.equals(applicationNamespace)) {
    283       return AttributeResource.RES_AUTO_NS_URI;
    284     } else {
    285       return namespace;
    286     }
    287   }
    288 
    289   @Override
    290   public String getAttributeName(int index) {
    291     Node attr = getAttributeAt(index);
    292     String name = attr.getLocalName();
    293     return name == null ? attr.getNodeName() : name;
    294   }
    295 
    296   @Override
    297   public String getAttributePrefix(int index) {
    298     throw new RuntimeException("getAttributePrefix not supported");
    299   }
    300 
    301   @Override
    302   public boolean isEmptyElementTag() throws XmlPullParserException {
    303     // In Android this method is left unimplemented.
    304     // This implementation is mirroring that.
    305     return false;
    306   }
    307 
    308   @Override
    309   public int getAttributeCount() {
    310     if (currentNode == null) {
    311       return -1;
    312     }
    313     return currentNode.getAttributes().getLength();
    314   }
    315 
    316   @Override
    317   public String getAttributeValue(int index) {
    318     return qualify(getAttributeAt(index).getNodeValue());
    319   }
    320 
    321   // for testing only...
    322   public String qualify(String value) {
    323     if (value == null) return null;
    324     if (AttributeResource.isResourceReference(value)) {
    325       return "@" + ResName.qualifyResourceName(value.trim().substring(1).replace("+", ""), packageName, "attr");
    326     } else if (AttributeResource.isStyleReference(value)) {
    327       return "?" + ResName.qualifyResourceName(value.trim().substring(1), packageName, "attr");
    328     } else {
    329       return StringResources.processStringResources(value);
    330     }
    331   }
    332 
    333   @Override
    334   public String getAttributeType(int index) {
    335     // Android always returns CDATA even if the
    336     // node has no attribute.
    337     return "CDATA";
    338   }
    339 
    340   @Override
    341   public boolean isAttributeDefault(int index) {
    342     // The android implementation always returns false
    343     return false;
    344   }
    345 
    346   @Override
    347   public int nextToken() throws XmlPullParserException, IOException {
    348     return next();
    349   }
    350 
    351   @Override
    352   public String getAttributeValue(String namespace, String name) {
    353     return qualify(getAttribute(namespace, name));
    354   }
    355 
    356   @Override
    357   public int next() throws XmlPullParserException, IOException {
    358     if (!mStarted) {
    359       mStarted = true;
    360       return START_DOCUMENT;
    361     }
    362     if (mEventType == END_DOCUMENT) {
    363       return END_DOCUMENT;
    364     }
    365     int ev = nativeNext();
    366     if (mDecNextDepth) {
    367       mDepth--;
    368       mDecNextDepth = false;
    369     }
    370     switch (ev) {
    371       case START_TAG:
    372         mDepth++;
    373         break;
    374       case END_TAG:
    375         mDecNextDepth = true;
    376         break;
    377     }
    378     mEventType = ev;
    379     if (ev == END_DOCUMENT) {
    380       // Automatically close the parse when we reach the end of
    381       // a document, since the standard XmlPullParser interface
    382       // doesn't have such an API so most clients will leave us
    383       // dangling.
    384       close();
    385     }
    386     return ev;
    387   }
    388 
    389   /**
    390    * A twin implementation of the native android nativeNext(status)
    391    *
    392    * @throws XmlPullParserException
    393    */
    394   private int nativeNext() throws XmlPullParserException {
    395     switch (mEventType) {
    396       case (CDSECT): {
    397         throw new IllegalArgumentException(
    398             "CDSECT is not handled by Android");
    399       }
    400       case (COMMENT): {
    401         throw new IllegalArgumentException(
    402             "COMMENT is not handled by Android");
    403       }
    404       case (DOCDECL): {
    405         throw new IllegalArgumentException(
    406             "DOCDECL is not handled by Android");
    407       }
    408       case (ENTITY_REF): {
    409         throw new IllegalArgumentException(
    410             "ENTITY_REF is not handled by Android");
    411       }
    412       case (END_DOCUMENT): {
    413         // The end document event should have been filtered
    414         // from the invoker. This should never happen.
    415         throw new IllegalArgumentException(
    416             "END_DOCUMENT should not be found here.");
    417       }
    418       case (END_TAG): {
    419         return navigateToNextNode(currentNode);
    420       }
    421       case (IGNORABLE_WHITESPACE): {
    422         throw new IllegalArgumentException(
    423             "IGNORABLE_WHITESPACE");
    424       }
    425       case (PROCESSING_INSTRUCTION): {
    426         throw new IllegalArgumentException(
    427             "PROCESSING_INSTRUCTION");
    428       }
    429       case (START_DOCUMENT): {
    430         currentNode = document.getDocumentElement();
    431         return START_TAG;
    432       }
    433       case (START_TAG): {
    434         if (currentNode.hasChildNodes()) {
    435           // The node has children, navigate down
    436           return processNextNodeType(
    437               currentNode.getFirstChild());
    438         } else {
    439           // The node has no children
    440           return END_TAG;
    441         }
    442       }
    443       case (TEXT): {
    444         return navigateToNextNode(currentNode);
    445       }
    446       default: {
    447         // This can only happen if mEventType is
    448         // assigned with an unmapped integer.
    449         throw new RuntimeException(
    450             "Robolectric-> Uknown XML event type: " + mEventType);
    451       }
    452     }
    453 
    454   }
    455 
    456   /*protected*/ int processNextNodeType(Node node)
    457       throws XmlPullParserException {
    458     switch (node.getNodeType()) {
    459       case (Node.ATTRIBUTE_NODE): {
    460         throw new IllegalArgumentException("ATTRIBUTE_NODE");
    461       }
    462       case (Node.CDATA_SECTION_NODE): {
    463         return navigateToNextNode(node);
    464       }
    465       case (Node.COMMENT_NODE): {
    466         return navigateToNextNode(node);
    467       }
    468       case (Node.DOCUMENT_FRAGMENT_NODE): {
    469         throw new IllegalArgumentException("DOCUMENT_FRAGMENT_NODE");
    470       }
    471       case (Node.DOCUMENT_NODE): {
    472         throw new IllegalArgumentException("DOCUMENT_NODE");
    473       }
    474       case (Node.DOCUMENT_TYPE_NODE): {
    475         throw new IllegalArgumentException("DOCUMENT_TYPE_NODE");
    476       }
    477       case (Node.ELEMENT_NODE): {
    478         currentNode = node;
    479         return START_TAG;
    480       }
    481       case (Node.ENTITY_NODE): {
    482         throw new IllegalArgumentException("ENTITY_NODE");
    483       }
    484       case (Node.ENTITY_REFERENCE_NODE): {
    485         throw new IllegalArgumentException("ENTITY_REFERENCE_NODE");
    486       }
    487       case (Node.NOTATION_NODE): {
    488         throw new IllegalArgumentException("DOCUMENT_TYPE_NODE");
    489       }
    490       case (Node.PROCESSING_INSTRUCTION_NODE): {
    491         throw new IllegalArgumentException("DOCUMENT_TYPE_NODE");
    492       }
    493       case (Node.TEXT_NODE): {
    494         if (isWhitespace(node.getNodeValue())) {
    495           // Skip whitespaces
    496           return navigateToNextNode(node);
    497         } else {
    498           currentNode = node;
    499           return TEXT;
    500         }
    501       }
    502       default: {
    503         throw new RuntimeException(
    504             "Robolectric -> Unknown node type: " +
    505                 node.getNodeType() + ".");
    506       }
    507     }
    508   }
    509 
    510   /**
    511    * Navigate to the next node after a node and all of his
    512    * children have been explored.
    513    *
    514    * If the node has unexplored siblings navigate to the
    515    * next sibling. Otherwise return to its parent.
    516    *
    517    * @param node the node which was just explored.
    518    * @return {@link XmlPullParserException#START_TAG} if the given
    519    *         node has siblings, {@link XmlPullParserException#END_TAG}
    520    *         if the node has no unexplored siblings or
    521    *         {@link XmlPullParserException#END_DOCUMENT} if the explored
    522    *         was the root document.
    523    * @throws XmlPullParserException if the parser fails to
    524    *                                parse the next node.
    525    */
    526   int navigateToNextNode(Node node)
    527       throws XmlPullParserException {
    528     Node nextNode = node.getNextSibling();
    529     if (nextNode != null) {
    530       // Move to the next siblings
    531       return processNextNodeType(nextNode);
    532     } else {
    533       // Goes back to the parent
    534       if (document.getDocumentElement().equals(node)) {
    535         currentNode = null;
    536         return END_DOCUMENT;
    537       }
    538       currentNode = node.getParentNode();
    539       return END_TAG;
    540     }
    541   }
    542 
    543   @Override
    544   public void require(int type, String namespace, String name)
    545       throws XmlPullParserException, IOException {
    546     if (type != getEventType()
    547         || (namespace != null && !namespace.equals(getNamespace()))
    548         || (name != null && !name.equals(getName()))) {
    549       throw new XmlPullParserException(
    550           "expected " + TYPES[type] + getPositionDescription());
    551     }
    552   }
    553 
    554   @Override
    555   public String nextText() throws XmlPullParserException, IOException {
    556     if (getEventType() != START_TAG) {
    557       throw new XmlPullParserException(
    558           getPositionDescription()
    559               + ": parser must be on START_TAG to read next text", this, null);
    560     }
    561     int eventType = next();
    562     if (eventType == TEXT) {
    563       String result = getText();
    564       eventType = next();
    565       if (eventType != END_TAG) {
    566         throw new XmlPullParserException(
    567             getPositionDescription()
    568                 + ": event TEXT it must be immediately followed by END_TAG", this, null);
    569       }
    570       return result;
    571     } else if (eventType == END_TAG) {
    572       return "";
    573     } else {
    574       throw new XmlPullParserException(
    575           getPositionDescription()
    576               + ": parser must be on START_TAG or TEXT to read text", this, null);
    577     }
    578   }
    579 
    580   @Override
    581   public int nextTag() throws XmlPullParserException, IOException {
    582     int eventType = next();
    583     if (eventType == TEXT && isWhitespace()) { // skip whitespace
    584       eventType = next();
    585     }
    586     if (eventType != START_TAG && eventType != END_TAG) {
    587       throw new XmlPullParserException(
    588           "Expected start or end tag. Found: " + eventType, this, null);
    589     }
    590     return eventType;
    591   }
    592 
    593   @Override
    594   public int getAttributeNameResource(int index) {
    595     String attributeNamespace = getAttributeNamespace(index);
    596     if (attributeNamespace.equals(RES_AUTO_NS_URI)) {
    597       attributeNamespace = packageName;
    598     } else if (attributeNamespace.startsWith(ANDROID_RES_NS_PREFIX)) {
    599       attributeNamespace = attributeNamespace.substring(ANDROID_RES_NS_PREFIX.length());
    600     }
    601     return getResourceId(getAttributeName(index), attributeNamespace, "attr");
    602   }
    603 
    604   @Override
    605   public int getAttributeListValue(String namespace, String attribute,
    606       String[] options, int defaultValue) {
    607     String attr = getAttribute(namespace, attribute);
    608     if (attr == null) {
    609       return 0;
    610     }
    611     List<String> optList = Arrays.asList(options);
    612     int index = optList.indexOf(attr);
    613     if (index == -1) {
    614       return defaultValue;
    615     }
    616     return index;
    617   }
    618 
    619   @Override
    620   public boolean getAttributeBooleanValue(String namespace, String attribute,
    621       boolean defaultValue) {
    622     String attr = getAttribute(namespace, attribute);
    623     if (attr == null) {
    624       return defaultValue;
    625     }
    626     return Boolean.parseBoolean(attr);
    627   }
    628 
    629   @Override
    630   public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) {
    631     String attr = getAttribute(namespace, attribute);
    632     if (attr != null && attr.startsWith("@") && !AttributeResource.isNull(attr)) {
    633       return getResourceId(attr, packageName, null);
    634     }
    635     return defaultValue;
    636   }
    637 
    638   @Override
    639   public int getAttributeIntValue(String namespace, String attribute, int defaultValue) {
    640     return XmlUtils.convertValueToInt(this.getAttributeValue(namespace, attribute), defaultValue);
    641   }
    642 
    643   @Override
    644   public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) {
    645     int value = getAttributeIntValue(namespace, attribute, defaultValue);
    646     if (value < 0) {
    647       return defaultValue;
    648     }
    649     return value;
    650   }
    651 
    652   @Override
    653   public float getAttributeFloatValue(String namespace, String attribute,
    654       float defaultValue) {
    655     String attr = getAttribute(namespace, attribute);
    656     if (attr == null) {
    657       return defaultValue;
    658     }
    659     try {
    660       return Float.parseFloat(attr);
    661     } catch (NumberFormatException ex) {
    662       return defaultValue;
    663     }
    664   }
    665 
    666   @Override
    667   public int getAttributeListValue(
    668       int idx, String[] options, int defaultValue) {
    669     try {
    670       String value = getAttributeValue(idx);
    671       List<String> optList = Arrays.asList(options);
    672       int index = optList.indexOf(value);
    673       if (index == -1) {
    674         return defaultValue;
    675       }
    676       return index;
    677     } catch (IndexOutOfBoundsException ex) {
    678       return defaultValue;
    679     }
    680   }
    681 
    682   @Override
    683   public boolean getAttributeBooleanValue(
    684       int idx, boolean defaultValue) {
    685     try {
    686       return Boolean.parseBoolean(getAttributeValue(idx));
    687     } catch (IndexOutOfBoundsException ex) {
    688       return defaultValue;
    689     }
    690   }
    691 
    692   @Override
    693   public int getAttributeResourceValue(int idx, int defaultValue) {
    694     String attributeValue = getAttributeValue(idx);
    695     if (attributeValue != null && attributeValue.startsWith("@")) {
    696       int resourceId = getResourceId(attributeValue.substring(1), packageName, null);
    697       if (resourceId != 0) {
    698         return resourceId;
    699       }
    700     }
    701     return defaultValue;
    702   }
    703 
    704   @Override
    705   public int getAttributeIntValue(int idx, int defaultValue) {
    706     try {
    707       return Integer.parseInt(getAttributeValue(idx));
    708     } catch (NumberFormatException ex) {
    709       return defaultValue;
    710     } catch (IndexOutOfBoundsException ex) {
    711       return defaultValue;
    712     }
    713   }
    714 
    715   @Override
    716   public int getAttributeUnsignedIntValue(int idx, int defaultValue) {
    717     int value = getAttributeIntValue(idx, defaultValue);
    718     if (value < 0) {
    719       return defaultValue;
    720     }
    721     return value;
    722   }
    723 
    724   @Override
    725   public float getAttributeFloatValue(int idx, float defaultValue) {
    726     try {
    727       return Float.parseFloat(getAttributeValue(idx));
    728     } catch (NumberFormatException ex) {
    729       return defaultValue;
    730     } catch (IndexOutOfBoundsException ex) {
    731       return defaultValue;
    732     }
    733   }
    734 
    735   @Override
    736   public String getIdAttribute() {
    737     return getAttribute(null, "id");
    738   }
    739 
    740   @Override
    741   public String getClassAttribute() {
    742     return getAttribute(null, "class");
    743   }
    744 
    745   @Override
    746   public int getIdAttributeResourceValue(int defaultValue) {
    747     return getAttributeResourceValue(null, "id", defaultValue);
    748   }
    749 
    750   @Override
    751   public int getStyleAttribute() {
    752     String attr = getAttribute(null, "style");
    753     if (attr == null ||
    754         (!AttributeResource.isResourceReference(attr) && !AttributeResource.isStyleReference(attr))) {
    755       return 0;
    756     }
    757 
    758     int style = getResourceId(attr, packageName, "style");
    759     if (style == 0) {
    760       // try again with underscores...
    761       style = getResourceId(attr.replace('.', '_'), packageName, "style");
    762     }
    763     return style;
    764   }
    765 
    766   @Override
    767   public void close() {
    768     // Nothing to do
    769   }
    770 
    771   @Override
    772   protected void finalize() throws Throwable {
    773     close();
    774   }
    775 
    776   private int getResourceId(String possiblyQualifiedResourceName, String defaultPackageName, String defaultType) {
    777 
    778     if (AttributeResource.isNull(possiblyQualifiedResourceName)) return 0;
    779 
    780     if (AttributeResource.isStyleReference(possiblyQualifiedResourceName)) {
    781       ResName styleReference = AttributeResource.getStyleReference(possiblyQualifiedResourceName, defaultPackageName, "attr");
    782       Integer resourceId = resourceTable.getResourceId(styleReference);
    783       if (resourceId == null) {
    784         throw new Resources.NotFoundException(styleReference.getFullyQualifiedName());
    785       }
    786       return resourceId;
    787     }
    788 
    789     if (AttributeResource.isResourceReference(possiblyQualifiedResourceName)) {
    790       ResName resourceReference = AttributeResource.getResourceReference(possiblyQualifiedResourceName, defaultPackageName, defaultType);
    791       Integer resourceId = resourceTable.getResourceId(resourceReference);
    792       if (resourceId == null) {
    793         throw new Resources.NotFoundException(resourceReference.getFullyQualifiedName());
    794       }
    795       return resourceId;
    796     }
    797     possiblyQualifiedResourceName = removeLeadingSpecialCharsIfAny(possiblyQualifiedResourceName);
    798     ResName resName = ResName.qualifyResName(possiblyQualifiedResourceName, defaultPackageName, defaultType);
    799     Integer resourceId = resourceTable.getResourceId(resName);
    800     return resourceId == null ? 0 : resourceId;
    801   }
    802 
    803   private static String removeLeadingSpecialCharsIfAny(String name){
    804     if (name.startsWith("@+")) {
    805       return name.substring(2);
    806     }
    807     if (name.startsWith("@")) {
    808       return name.substring(1);
    809     }
    810     return name;
    811   }
    812 
    813   /**
    814    * Tell is a given feature is supported by android.
    815    *
    816    * @param name Feature name.
    817    * @return True if the feature is supported.
    818    */
    819   private static boolean isAndroidSupportedFeature(String name) {
    820     if (name == null) {
    821       return false;
    822     }
    823     for (String feature : AVAILABLE_FEATURES) {
    824       if (feature.equals(name)) {
    825         return true;
    826       }
    827     }
    828     return false;
    829   }
    830 }
    831