1 /* 2 * Copyright (C) 2011 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.resources; 18 19 import static com.android.SdkConstants.ANDROID_PREFIX; 20 import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; 21 import static com.android.SdkConstants.ANDROID_URI; 22 import static com.android.SdkConstants.ATTR_COLOR; 23 import static com.android.SdkConstants.ATTR_NAME; 24 import static com.android.SdkConstants.ATTR_TYPE; 25 import static com.android.SdkConstants.DOT_XML; 26 import static com.android.SdkConstants.EXT_XML; 27 import static com.android.SdkConstants.FD_RESOURCES; 28 import static com.android.SdkConstants.FD_RES_VALUES; 29 import static com.android.SdkConstants.PREFIX_RESOURCE_REF; 30 import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; 31 import static com.android.SdkConstants.TAG_ITEM; 32 import static com.android.SdkConstants.TAG_RESOURCES; 33 import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP; 34 35 import com.android.ide.common.rendering.api.ResourceValue; 36 import com.android.ide.common.resources.ResourceDeltaKind; 37 import com.android.ide.common.resources.ResourceRepository; 38 import com.android.ide.common.resources.ResourceResolver; 39 import com.android.ide.common.resources.configuration.CountryCodeQualifier; 40 import com.android.ide.common.resources.configuration.DensityQualifier; 41 import com.android.ide.common.resources.configuration.FolderConfiguration; 42 import com.android.ide.common.resources.configuration.KeyboardStateQualifier; 43 import com.android.ide.common.resources.configuration.LanguageQualifier; 44 import com.android.ide.common.resources.configuration.LayoutDirectionQualifier; 45 import com.android.ide.common.resources.configuration.NavigationMethodQualifier; 46 import com.android.ide.common.resources.configuration.NavigationStateQualifier; 47 import com.android.ide.common.resources.configuration.NetworkCodeQualifier; 48 import com.android.ide.common.resources.configuration.NightModeQualifier; 49 import com.android.ide.common.resources.configuration.RegionQualifier; 50 import com.android.ide.common.resources.configuration.ResourceQualifier; 51 import com.android.ide.common.resources.configuration.ScreenDimensionQualifier; 52 import com.android.ide.common.resources.configuration.ScreenHeightQualifier; 53 import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; 54 import com.android.ide.common.resources.configuration.ScreenRatioQualifier; 55 import com.android.ide.common.resources.configuration.ScreenSizeQualifier; 56 import com.android.ide.common.resources.configuration.ScreenWidthQualifier; 57 import com.android.ide.common.resources.configuration.SmallestScreenWidthQualifier; 58 import com.android.ide.common.resources.configuration.TextInputMethodQualifier; 59 import com.android.ide.common.resources.configuration.TouchScreenQualifier; 60 import com.android.ide.common.resources.configuration.UiModeQualifier; 61 import com.android.ide.common.resources.configuration.VersionQualifier; 62 import com.android.ide.eclipse.adt.AdtPlugin; 63 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 64 import com.android.ide.eclipse.adt.internal.editors.Hyperlinks; 65 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 66 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils; 67 import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.VisualRefactoring; 68 import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard; 69 import com.android.resources.FolderTypeRelationship; 70 import com.android.resources.ResourceFolderType; 71 import com.android.resources.ResourceType; 72 import com.android.utils.Pair; 73 74 import org.eclipse.core.resources.IFile; 75 import org.eclipse.core.resources.IProject; 76 import org.eclipse.core.resources.IResource; 77 import org.eclipse.core.resources.IResourceDelta; 78 import org.eclipse.core.runtime.CoreException; 79 import org.eclipse.core.runtime.IPath; 80 import org.eclipse.core.runtime.Path; 81 import org.eclipse.jface.text.IRegion; 82 import org.eclipse.jface.text.Region; 83 import org.eclipse.swt.graphics.Image; 84 import org.eclipse.swt.graphics.RGB; 85 import org.eclipse.wst.sse.core.StructuredModelManager; 86 import org.eclipse.wst.sse.core.internal.provisional.IModelManager; 87 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 88 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 89 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 90 import org.eclipse.wst.xml.core.internal.document.ElementImpl; 91 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; 92 import org.w3c.dom.Attr; 93 import org.w3c.dom.Document; 94 import org.w3c.dom.Element; 95 import org.w3c.dom.NamedNodeMap; 96 import org.w3c.dom.Node; 97 import org.w3c.dom.NodeList; 98 import org.w3c.dom.Text; 99 import org.xml.sax.InputSource; 100 101 import java.io.BufferedInputStream; 102 import java.io.ByteArrayInputStream; 103 import java.io.File; 104 import java.io.FileInputStream; 105 import java.io.IOException; 106 import java.io.InputStream; 107 import java.io.UnsupportedEncodingException; 108 import java.util.HashMap; 109 import java.util.List; 110 import java.util.Map; 111 import java.util.Set; 112 113 import javax.xml.parsers.DocumentBuilder; 114 import javax.xml.parsers.DocumentBuilderFactory; 115 116 /** 117 * Helper class to deal with SWT specifics for the resources. 118 */ 119 @SuppressWarnings("restriction") // XML model 120 public class ResourceHelper { 121 122 private final static Map<Class<?>, Image> sIconMap = new HashMap<Class<?>, Image>( 123 FolderConfiguration.getQualifierCount()); 124 125 static { 126 try { 127 IconFactory factory = IconFactory.getInstance(); 128 sIconMap.put(CountryCodeQualifier.class, factory.getIcon("mcc")); //$NON-NLS-1$ 129 sIconMap.put(NetworkCodeQualifier.class, factory.getIcon("mnc")); //$NON-NLS-1$ 130 sIconMap.put(LanguageQualifier.class, factory.getIcon("language")); //$NON-NLS-1$ 131 sIconMap.put(RegionQualifier.class, factory.getIcon("region")); //$NON-NLS-1$ 132 sIconMap.put(LayoutDirectionQualifier.class, factory.getIcon("bidi")); //$NON-NLS-1$ 133 sIconMap.put(ScreenSizeQualifier.class, factory.getIcon("size")); //$NON-NLS-1$ 134 sIconMap.put(ScreenRatioQualifier.class, factory.getIcon("ratio")); //$NON-NLS-1$ 135 sIconMap.put(ScreenOrientationQualifier.class, factory.getIcon("orientation")); //$NON-NLS-1$ 136 sIconMap.put(UiModeQualifier.class, factory.getIcon("dockmode")); //$NON-NLS-1$ 137 sIconMap.put(NightModeQualifier.class, factory.getIcon("nightmode")); //$NON-NLS-1$ 138 sIconMap.put(DensityQualifier.class, factory.getIcon("dpi")); //$NON-NLS-1$ 139 sIconMap.put(TouchScreenQualifier.class, factory.getIcon("touch")); //$NON-NLS-1$ 140 sIconMap.put(KeyboardStateQualifier.class, factory.getIcon("keyboard")); //$NON-NLS-1$ 141 sIconMap.put(TextInputMethodQualifier.class, factory.getIcon("text_input")); //$NON-NLS-1$ 142 sIconMap.put(NavigationStateQualifier.class, factory.getIcon("navpad")); //$NON-NLS-1$ 143 sIconMap.put(NavigationMethodQualifier.class, factory.getIcon("navpad")); //$NON-NLS-1$ 144 sIconMap.put(ScreenDimensionQualifier.class, factory.getIcon("dimension")); //$NON-NLS-1$ 145 sIconMap.put(VersionQualifier.class, factory.getIcon("version")); //$NON-NLS-1$ 146 sIconMap.put(ScreenWidthQualifier.class, factory.getIcon("width")); //$NON-NLS-1$ 147 sIconMap.put(ScreenHeightQualifier.class, factory.getIcon("height")); //$NON-NLS-1$ 148 sIconMap.put(SmallestScreenWidthQualifier.class,factory.getIcon("swidth")); //$NON-NLS-1$ 149 } catch (Throwable t) { 150 AdtPlugin.log(t , null); 151 } 152 } 153 154 /** 155 * Returns the icon for the qualifier. 156 */ 157 public static Image getIcon(Class<? extends ResourceQualifier> theClass) { 158 return sIconMap.get(theClass); 159 } 160 161 /** 162 * Returns a {@link ResourceDeltaKind} from an {@link IResourceDelta} value. 163 * @param kind a {@link IResourceDelta} integer constant. 164 * @return a matching {@link ResourceDeltaKind} or null. 165 * 166 * @see IResourceDelta#ADDED 167 * @see IResourceDelta#REMOVED 168 * @see IResourceDelta#CHANGED 169 */ 170 public static ResourceDeltaKind getResourceDeltaKind(int kind) { 171 switch (kind) { 172 case IResourceDelta.ADDED: 173 return ResourceDeltaKind.ADDED; 174 case IResourceDelta.REMOVED: 175 return ResourceDeltaKind.REMOVED; 176 case IResourceDelta.CHANGED: 177 return ResourceDeltaKind.CHANGED; 178 } 179 180 return null; 181 } 182 183 /** 184 * Is this a resource that can be defined in any file within the "values" folder? 185 * <p> 186 * Some resource types can be defined <b>both</b> as a separate XML file as well 187 * as defined within a value XML file. This method will return true for these types 188 * as well. In other words, a ResourceType can return true for both 189 * {@link #isValueBasedResourceType} and {@link #isFileBasedResourceType}. 190 * 191 * @param type the resource type to check 192 * @return true if the given resource type can be represented as a value under the 193 * values/ folder 194 */ 195 public static boolean isValueBasedResourceType(ResourceType type) { 196 List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type); 197 for (ResourceFolderType folderType : folderTypes) { 198 if (folderType == ResourceFolderType.VALUES) { 199 return true; 200 } 201 } 202 203 return false; 204 } 205 206 /** 207 * Is this a resource that is defined in a file named by the resource plus the XML 208 * extension? 209 * <p> 210 * Some resource types can be defined <b>both</b> as a separate XML file as well as 211 * defined within a value XML file along with other properties. This method will 212 * return true for these resource types as well. In other words, a ResourceType can 213 * return true for both {@link #isValueBasedResourceType} and 214 * {@link #isFileBasedResourceType}. 215 * 216 * @param type the resource type to check 217 * @return true if the given resource type is stored in a file named by the resource 218 */ 219 public static boolean isFileBasedResourceType(ResourceType type) { 220 List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type); 221 for (ResourceFolderType folderType : folderTypes) { 222 if (folderType != ResourceFolderType.VALUES) { 223 224 if (type == ResourceType.ID) { 225 // The folder types for ID is not only VALUES but also 226 // LAYOUT and MENU. However, unlike resources, they are only defined 227 // inline there so for the purposes of isFileBasedResourceType 228 // (where the intent is to figure out files that are uniquely identified 229 // by a resource's name) this method should return false anyway. 230 return false; 231 } 232 233 return true; 234 } 235 } 236 237 return false; 238 } 239 240 /** 241 * Returns true if this class can create the given resource 242 * 243 * @param resource the resource to be created 244 * @return true if the {@link #createResource} method can create this resource 245 */ 246 public static boolean canCreateResource(String resource) { 247 // Cannot create framework resources 248 if (resource.startsWith(ANDROID_PREFIX)) { 249 return false; 250 } 251 252 Pair<ResourceType,String> parsed = ResourceRepository.parseResource(resource); 253 if (parsed != null) { 254 ResourceType type = parsed.getFirst(); 255 String name = parsed.getSecond(); 256 257 // Make sure the name is valid 258 ResourceNameValidator validator = 259 ResourceNameValidator.create(false, (Set<String>) null /* existing */, type); 260 if (validator.isValid(name) != null) { 261 return false; 262 } 263 264 return canCreateResourceType(type); 265 } 266 267 return false; 268 } 269 270 /** 271 * Returns true if this class can create resources of the given resource 272 * type 273 * 274 * @param type the type of resource to be created 275 * @return true if the {@link #createResource} method can create resources 276 * of this type (provided the name parameter is also valid) 277 */ 278 public static boolean canCreateResourceType(ResourceType type) { 279 // We can create all value types 280 if (isValueBasedResourceType(type)) { 281 return true; 282 } 283 284 // We can create -some- file-based types - those supported by the New XML wizard: 285 for (ResourceFolderType folderType : FolderTypeRelationship.getRelatedFolders(type)) { 286 if (NewXmlFileWizard.canCreateXmlFile(folderType)) { 287 return true; 288 } 289 } 290 291 return false; 292 } 293 294 /** Creates a file-based resource, like a layout. Used by {@link #createResource} */ 295 private static Pair<IFile,IRegion> createFileResource(IProject project, ResourceType type, 296 String name) { 297 298 ResourceFolderType folderType = null; 299 for (ResourceFolderType f : FolderTypeRelationship.getRelatedFolders(type)) { 300 if (NewXmlFileWizard.canCreateXmlFile(f)) { 301 folderType = f; 302 break; 303 } 304 } 305 if (folderType == null) { 306 return null; 307 } 308 309 // Find "dimens.xml" file in res/values/ (or corresponding name for other 310 // value types) 311 IPath projectPath = new Path(FD_RESOURCES + WS_SEP + folderType.getName() + WS_SEP 312 + name + '.' + EXT_XML); 313 IFile file = project.getFile(projectPath); 314 return NewXmlFileWizard.createXmlFile(project, file, folderType); 315 } 316 317 /** 318 * Creates a resource of a given type, name and (if applicable) value 319 * 320 * @param project the project to contain the resource 321 * @param type the type of resource 322 * @param name the name of the resource 323 * @param value the value of the resource, if it is a value-type resource 324 * @return a pair of the file containing the resource and a region where the value 325 * appears 326 */ 327 public static Pair<IFile,IRegion> createResource(IProject project, ResourceType type, 328 String name, String value) { 329 if (!isValueBasedResourceType(type)) { 330 return createFileResource(project, type, name); 331 } 332 333 // Find "dimens.xml" file in res/values/ (or corresponding name for other 334 // value types) 335 String typeName = type.getName(); 336 String fileName = typeName + 's'; 337 String projectPath = FD_RESOURCES + WS_SEP + FD_RES_VALUES + WS_SEP 338 + fileName + '.' + EXT_XML; 339 Object editRequester = project; 340 IResource member = project.findMember(projectPath); 341 String tagName = Hyperlinks.getTagName(type); 342 boolean createEmptyTag = type == ResourceType.ID; 343 if (member != null) { 344 if (member instanceof IFile) { 345 IFile file = (IFile) member; 346 // File exists: Must add item to the XML 347 IModelManager manager = StructuredModelManager.getModelManager(); 348 IStructuredModel model = null; 349 try { 350 model = manager.getExistingModelForEdit(file); 351 if (model == null) { 352 model = manager.getModelForEdit(file); 353 } 354 if (model instanceof IDOMModel) { 355 model.beginRecording(editRequester, String.format("Add %1$s", 356 type.getDisplayName())); 357 IDOMModel domModel = (IDOMModel) model; 358 Document document = domModel.getDocument(); 359 Element root = document.getDocumentElement(); 360 IStructuredDocument structuredDocument = model.getStructuredDocument(); 361 Node lastElement = null; 362 NodeList childNodes = root.getChildNodes(); 363 String indent = null; 364 for (int i = childNodes.getLength() - 1; i >= 0; i--) { 365 Node node = childNodes.item(i); 366 if (node.getNodeType() == Node.ELEMENT_NODE) { 367 lastElement = node; 368 indent = AndroidXmlEditor.getIndent(structuredDocument, node); 369 break; 370 } 371 } 372 if (indent == null || indent.length() == 0) { 373 indent = " "; //$NON-NLS-1$ 374 } 375 Node nextChild = lastElement != null ? lastElement.getNextSibling() : null; 376 Text indentNode = document.createTextNode('\n' + indent); 377 root.insertBefore(indentNode, nextChild); 378 Element element = document.createElement(tagName); 379 if (createEmptyTag) { 380 if (element instanceof ElementImpl) { 381 ElementImpl elementImpl = (ElementImpl) element; 382 elementImpl.setEmptyTag(true); 383 } 384 } 385 element.setAttribute(ATTR_NAME, name); 386 if (!tagName.equals(typeName)) { 387 element.setAttribute(ATTR_TYPE, typeName); 388 } 389 root.insertBefore(element, nextChild); 390 IRegion region = null; 391 392 if (createEmptyTag) { 393 IndexedRegion domRegion = VisualRefactoring.getRegion(element); 394 int endOffset = domRegion.getEndOffset(); 395 region = new Region(endOffset, 0); 396 } else { 397 Node valueNode = document.createTextNode(value); 398 element.appendChild(valueNode); 399 400 IndexedRegion domRegion = VisualRefactoring.getRegion(valueNode); 401 int startOffset = domRegion.getStartOffset(); 402 int length = domRegion.getLength(); 403 region = new Region(startOffset, length); 404 } 405 model.save(); 406 return Pair.of(file, region); 407 } 408 } catch (Exception e) { 409 AdtPlugin.log(e, "Cannot access XML value model"); 410 } finally { 411 if (model != null) { 412 model.endRecording(editRequester); 413 model.releaseFromEdit(); 414 } 415 } 416 } 417 418 return null; 419 } else { 420 // No such file exists: just create it 421 String prolog = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; //$NON-NLS-1$ 422 StringBuilder sb = new StringBuilder(prolog); 423 424 String root = TAG_RESOURCES; 425 sb.append('<').append(root).append('>').append('\n'); 426 sb.append(" "); //$NON-NLS-1$ 427 sb.append('<'); 428 sb.append(tagName); 429 sb.append(" name=\""); //$NON-NLS-1$ 430 sb.append(name); 431 sb.append('"'); 432 if (!tagName.equals(typeName)) { 433 sb.append(" type=\""); //$NON-NLS-1$ 434 sb.append(typeName); 435 sb.append('"'); 436 } 437 int start, end; 438 if (createEmptyTag) { 439 sb.append("/>"); //$NON-NLS-1$ 440 start = sb.length(); 441 end = sb.length(); 442 } else { 443 sb.append('>'); 444 start = sb.length(); 445 sb.append(value); 446 end = sb.length(); 447 sb.append('<').append('/'); 448 sb.append(tagName); 449 sb.append('>'); 450 } 451 sb.append('\n').append('<').append('/').append(root).append('>').append('\n'); 452 String result = sb.toString(); 453 // TODO: Pretty print string (wait until that CL is integrated) 454 String error = null; 455 try { 456 byte[] buf = result.getBytes("UTF8"); //$NON-NLS-1$ 457 InputStream stream = new ByteArrayInputStream(buf); 458 IFile file = project.getFile(new Path(projectPath)); 459 file.create(stream, true /*force*/, null /*progress*/); 460 IRegion region = new Region(start, end - start); 461 return Pair.of(file, region); 462 } catch (UnsupportedEncodingException e) { 463 error = e.getMessage(); 464 } catch (CoreException e) { 465 error = e.getMessage(); 466 } 467 468 error = String.format("Failed to generate %1$s: %2$s", name, error); 469 AdtPlugin.displayError("New Android XML File", error); 470 } 471 return null; 472 } 473 474 /** 475 * Returns the theme name to be shown for theme styles, e.g. for "@style/Theme" it 476 * returns "Theme" 477 * 478 * @param style a theme style string 479 * @return the user visible theme name 480 */ 481 public static String styleToTheme(String style) { 482 if (style.startsWith(STYLE_RESOURCE_PREFIX)) { 483 style = style.substring(STYLE_RESOURCE_PREFIX.length()); 484 } else if (style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) { 485 style = style.substring(ANDROID_STYLE_RESOURCE_PREFIX.length()); 486 } else if (style.startsWith(PREFIX_RESOURCE_REF)) { 487 // @package:style/foo 488 int index = style.indexOf('/'); 489 if (index != -1) { 490 style = style.substring(index + 1); 491 } 492 } 493 return style; 494 } 495 496 /** 497 * Returns true if the given style represents a project theme 498 * 499 * @param style a theme style string 500 * @return true if the style string represents a project theme, as opposed 501 * to a framework theme 502 */ 503 public static boolean isProjectStyle(String style) { 504 assert style.startsWith(STYLE_RESOURCE_PREFIX) 505 || style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : style; 506 507 return style.startsWith(STYLE_RESOURCE_PREFIX); 508 } 509 510 /** 511 * Returns the layout resource name for the given layout file, e.g. for 512 * /res/layout/foo.xml returns foo. 513 * 514 * @param layoutFile the layout file whose name we want to look up 515 * @return the layout name 516 */ 517 public static String getLayoutName(IFile layoutFile) { 518 String layoutName = layoutFile.getName(); 519 int dotIndex = layoutName.indexOf('.'); 520 if (dotIndex != -1) { 521 layoutName = layoutName.substring(0, dotIndex); 522 } 523 return layoutName; 524 } 525 526 /** 527 * Tries to resolve the given resource value to an actual RGB color. For state lists 528 * it will pick the simplest/fallback color. 529 * 530 * @param resources the resource resolver to use to follow color references 531 * @param color the color to resolve 532 * @return the corresponding {@link RGB} color, or null 533 */ 534 public static RGB resolveColor(ResourceResolver resources, ResourceValue color) { 535 color = resources.resolveResValue(color); 536 if (color == null) { 537 return null; 538 } 539 String value = color.getValue(); 540 541 while (value != null) { 542 if (value.startsWith("#")) { //$NON-NLS-1$ 543 try { 544 int rgba = ImageUtils.getColor(value); 545 // Drop alpha channel 546 return ImageUtils.intToRgb(rgba); 547 } catch (NumberFormatException nfe) { 548 // Pass 549 } 550 return null; 551 } 552 if (value.startsWith(PREFIX_RESOURCE_REF)) { 553 boolean isFramework = color.isFramework(); 554 color = resources.findResValue(value, isFramework); 555 if (color != null) { 556 value = color.getValue(); 557 } else { 558 break; 559 } 560 } else { 561 File file = new File(value); 562 if (file.exists() && file.getName().endsWith(DOT_XML)) { 563 // Parse 564 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 565 BufferedInputStream bis = null; 566 try { 567 bis = new BufferedInputStream(new FileInputStream(file)); 568 InputSource is = new InputSource(bis); 569 factory.setNamespaceAware(true); 570 factory.setValidating(false); 571 DocumentBuilder builder = factory.newDocumentBuilder(); 572 Document document = builder.parse(is); 573 NodeList items = document.getElementsByTagName(TAG_ITEM); 574 575 value = findColorValue(items); 576 continue; 577 } catch (Exception e) { 578 AdtPlugin.log(e, "Failed parsing color file %1$s", file.getName()); 579 } finally { 580 if (bis != null) { 581 try { 582 bis.close(); 583 } catch (IOException e) { 584 // Nothing useful can be done here 585 } 586 } 587 } 588 } 589 590 return null; 591 } 592 } 593 594 return null; 595 } 596 597 /** 598 * Searches a color XML file for the color definition element that does not 599 * have an associated state and returns its color 600 */ 601 private static String findColorValue(NodeList items) { 602 for (int i = 0, n = items.getLength(); i < n; i++) { 603 // Find non-state color definition 604 Node item = items.item(i); 605 boolean hasState = false; 606 if (item.getNodeType() == Node.ELEMENT_NODE) { 607 Element element = (Element) item; 608 if (element.hasAttributeNS(ANDROID_URI, ATTR_COLOR)) { 609 NamedNodeMap attributes = element.getAttributes(); 610 for (int j = 0, m = attributes.getLength(); j < m; j++) { 611 Attr attribute = (Attr) attributes.item(j); 612 if (attribute.getLocalName().startsWith("state_")) { //$NON-NLS-1$ 613 hasState = true; 614 break; 615 } 616 } 617 618 if (!hasState) { 619 return element.getAttributeNS(ANDROID_URI, ATTR_COLOR); 620 } 621 } 622 } 623 } 624 625 return null; 626 } 627 } 628