1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 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 17package com.android.layoutlib.bridge; 18 19import com.android.internal.util.XmlUtils; 20import com.android.layoutlib.api.IResourceValue; 21import com.android.layoutlib.api.IStyleResourceValue; 22 23import org.kxml2.io.KXmlParser; 24import org.xmlpull.v1.XmlPullParser; 25 26import android.content.res.ColorStateList; 27import android.content.res.Resources; 28import android.content.res.TypedArray; 29import android.graphics.drawable.Drawable; 30import android.util.DisplayMetrics; 31import android.util.TypedValue; 32import android.view.ViewGroup.LayoutParams; 33 34import java.io.File; 35import java.io.FileReader; 36import java.util.Map; 37 38/** 39 * TODO: describe. 40 */ 41public final class BridgeTypedArray extends TypedArray { 42 43 @SuppressWarnings("hiding") 44 private BridgeResources mResources; 45 private BridgeContext mContext; 46 @SuppressWarnings("hiding") 47 private IResourceValue[] mData; 48 private String[] mNames; 49 private final boolean mPlatformFile; 50 51 public BridgeTypedArray(BridgeResources resources, BridgeContext context, int len, 52 boolean platformFile) { 53 super(null, null, null, 0); 54 mResources = resources; 55 mContext = context; 56 mPlatformFile = platformFile; 57 mData = new IResourceValue[len]; 58 mNames = new String[len]; 59 } 60 61 /** A bridge-specific method that sets a value in the type array */ 62 public void bridgeSetValue(int index, String name, IResourceValue value) { 63 mData[index] = value; 64 mNames[index] = name; 65 } 66 67 /** 68 * Seals the array after all calls to {@link #bridgeSetValue(int, String, IResourceValue)} have 69 * been done. 70 * <p/>This allows to compute the list of non default values, permitting 71 * {@link #getIndexCount()} to return the proper value. 72 */ 73 public void sealArray() { 74 // fills TypedArray.mIndices which is used to implement getIndexCount/getIndexAt 75 // first count the array size 76 int count = 0; 77 for (IResourceValue data : mData) { 78 if (data != null) { 79 count++; 80 } 81 } 82 83 // allocate the table with an extra to store the size 84 mIndices = new int[count+1]; 85 mIndices[0] = count; 86 87 // fill the array with the indices. 88 int index = 1; 89 for (int i = 0 ; i < mData.length ; i++) { 90 if (mData[i] != null) { 91 mIndices[index++] = i; 92 } 93 } 94 } 95 96 /** 97 * Return the number of values in this array. 98 */ 99 @Override 100 public int length() { 101 return mData.length; 102 } 103 104 /** 105 * Return the Resources object this array was loaded from. 106 */ 107 @Override 108 public Resources getResources() { 109 return mResources; 110 } 111 112 /** 113 * Retrieve the styled string value for the attribute at <var>index</var>. 114 * 115 * @param index Index of attribute to retrieve. 116 * 117 * @return CharSequence holding string data. May be styled. Returns 118 * null if the attribute is not defined. 119 */ 120 @Override 121 public CharSequence getText(int index) { 122 if (mData[index] != null) { 123 // FIXME: handle styled strings! 124 return mData[index].getValue(); 125 } 126 127 return null; 128 } 129 130 /** 131 * Retrieve the string value for the attribute at <var>index</var>. 132 * 133 * @param index Index of attribute to retrieve. 134 * 135 * @return String holding string data. Any styling information is 136 * removed. Returns null if the attribute is not defined. 137 */ 138 @Override 139 public String getString(int index) { 140 if (mData[index] != null) { 141 return mData[index].getValue(); 142 } 143 144 return null; 145 } 146 147 /** 148 * Retrieve the boolean value for the attribute at <var>index</var>. 149 * 150 * @param index Index of attribute to retrieve. 151 * @param defValue Value to return if the attribute is not defined. 152 * 153 * @return Attribute boolean value, or defValue if not defined. 154 */ 155 @Override 156 public boolean getBoolean(int index, boolean defValue) { 157 if (mData[index] == null) { 158 return defValue; 159 } 160 161 String s = mData[index].getValue(); 162 if (s != null) { 163 return XmlUtils.convertValueToBoolean(s, defValue); 164 } 165 166 return defValue; 167 } 168 169 /** 170 * Retrieve the integer value for the attribute at <var>index</var>. 171 * 172 * @param index Index of attribute to retrieve. 173 * @param defValue Value to return if the attribute is not defined. 174 * 175 * @return Attribute int value, or defValue if not defined. 176 */ 177 @Override 178 public int getInt(int index, int defValue) { 179 if (mData[index] == null) { 180 return defValue; 181 } 182 183 String s = mData[index].getValue(); 184 185 try { 186 return (s == null) ? defValue : XmlUtils.convertValueToInt(s, defValue); 187 } catch (NumberFormatException e) { 188 // pass 189 } 190 191 // Field is not null and is not an integer. 192 // Check for possible constants and try to find them. 193 // Get the map of attribute-constant -> IntegerValue 194 Map<String, Integer> map = Bridge.getEnumValues(mNames[index]); 195 196 if (map != null) { 197 // accumulator to store the value of the 1+ constants. 198 int result = 0; 199 200 // split the value in case this is a mix of several flags. 201 String[] keywords = s.split("\\|"); 202 for (String keyword : keywords) { 203 Integer i = map.get(keyword.trim()); 204 if (i != null) { 205 result |= i.intValue(); 206 } else { 207 mContext.getLogger().warning(String.format( 208 "Unknown constant \"%s\" in attribute \"%2$s\"", 209 keyword, mNames[index])); 210 } 211 } 212 return result; 213 } 214 215 return defValue; 216 } 217 218 /** 219 * Retrieve the float value for the attribute at <var>index</var>. 220 * 221 * @param index Index of attribute to retrieve. 222 * 223 * @return Attribute float value, or defValue if not defined.. 224 */ 225 @Override 226 public float getFloat(int index, float defValue) { 227 if (mData[index] == null) { 228 return defValue; 229 } 230 231 String s = mData[index].getValue(); 232 233 if (s != null) { 234 try { 235 return Float.parseFloat(s); 236 } catch (NumberFormatException e) { 237 mContext.getLogger().warning(String.format( 238 "Unable to convert \"%s\" into a float in attribute \"%2$s\"", 239 s, mNames[index])); 240 241 // we'll return the default value below. 242 } 243 } 244 return defValue; 245 } 246 247 /** 248 * Retrieve the color value for the attribute at <var>index</var>. If 249 * the attribute references a color resource holding a complex 250 * {@link android.content.res.ColorStateList}, then the default color from 251 * the set is returned. 252 * 253 * @param index Index of attribute to retrieve. 254 * @param defValue Value to return if the attribute is not defined or 255 * not a resource. 256 * 257 * @return Attribute color value, or defValue if not defined. 258 */ 259 @Override 260 public int getColor(int index, int defValue) { 261 if (mData[index] == null) { 262 return defValue; 263 } 264 265 String s = mData[index].getValue(); 266 try { 267 return ResourceHelper.getColor(s); 268 } catch (NumberFormatException e) { 269 mContext.getLogger().warning(String.format( 270 "Unable to convert \"%s\" into a color in attribute \"%2$s\"", 271 s, mNames[index])); 272 273 // we'll return the default value below. 274 } 275 276 return defValue; 277 } 278 279 /** 280 * Retrieve the ColorStateList for the attribute at <var>index</var>. 281 * The value may be either a single solid color or a reference to 282 * a color or complex {@link android.content.res.ColorStateList} description. 283 * 284 * @param index Index of attribute to retrieve. 285 * 286 * @return ColorStateList for the attribute, or null if not defined. 287 */ 288 @Override 289 public ColorStateList getColorStateList(int index) { 290 if (mData[index] == null) { 291 return null; 292 } 293 294 String value = mData[index].getValue(); 295 296 if (value == null) { 297 return null; 298 } 299 300 try { 301 int color = ResourceHelper.getColor(value); 302 return ColorStateList.valueOf(color); 303 } catch (NumberFormatException e) { 304 // if it's not a color value, we'll attempt to read the xml based color below. 305 } 306 307 // let the framework inflate the ColorStateList from the XML file. 308 try { 309 File f = new File(value); 310 if (f.isFile()) { 311 KXmlParser parser = new KXmlParser(); 312 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); 313 parser.setInput(new FileReader(f)); 314 315 ColorStateList colorStateList = ColorStateList.createFromXml( 316 mContext.getResources(), 317 // FIXME: we need to know if this resource is platform or not 318 new BridgeXmlBlockParser(parser, mContext, false)); 319 return colorStateList; 320 } 321 } catch (Exception e) { 322 // this is an error and not warning since the file existence is checked before 323 // attempting to parse it. 324 mContext.getLogger().error(e); 325 326 // return null below. 327 } 328 329 // looks like were unable to resolve the color value. 330 mContext.getLogger().warning(String.format( 331 "Unable to resolve color value \"%1$s\" in attribute \"%2$s\"", 332 value, mNames[index])); 333 334 return null; 335 } 336 337 /** 338 * Retrieve the integer value for the attribute at <var>index</var>. 339 * 340 * @param index Index of attribute to retrieve. 341 * @param defValue Value to return if the attribute is not defined or 342 * not a resource. 343 * 344 * @return Attribute integer value, or defValue if not defined. 345 */ 346 @Override 347 public int getInteger(int index, int defValue) { 348 if (mData[index] == null) { 349 return defValue; 350 } 351 352 String s = mData[index].getValue(); 353 354 if (s != null) { 355 try { 356 return Integer.parseInt(s); 357 } catch (NumberFormatException e) { 358 mContext.getLogger().warning(String.format( 359 "Unable to convert \"%s\" into a integer in attribute \"%2$s\"", 360 s, mNames[index])); 361 362 // The default value is returned below. 363 } 364 } 365 366 return defValue; 367 } 368 369 /** 370 * Retrieve a dimensional unit attribute at <var>index</var>. Unit 371 * conversions are based on the current {@link DisplayMetrics} 372 * associated with the resources this {@link TypedArray} object 373 * came from. 374 * 375 * @param index Index of attribute to retrieve. 376 * @param defValue Value to return if the attribute is not defined or 377 * not a resource. 378 * 379 * @return Attribute dimension value multiplied by the appropriate 380 * metric, or defValue if not defined. 381 * 382 * @see #getDimensionPixelOffset 383 * @see #getDimensionPixelSize 384 */ 385 @Override 386 public float getDimension(int index, float defValue) { 387 if (mData[index] == null) { 388 return defValue; 389 } 390 391 String s = mData[index].getValue(); 392 393 if (s == null) { 394 return defValue; 395 } else if (s.equals(BridgeConstants.MATCH_PARENT) || 396 s.equals(BridgeConstants.FILL_PARENT)) { 397 return LayoutParams.MATCH_PARENT; 398 } else if (s.equals(BridgeConstants.WRAP_CONTENT)) { 399 return LayoutParams.WRAP_CONTENT; 400 } 401 402 if (ResourceHelper.stringToFloat(s, mValue)) { 403 return mValue.getDimension(mResources.mMetrics); 404 } 405 406 // looks like we were unable to resolve the dimension value 407 mContext.getLogger().warning(String.format( 408 "Unable to resolve dimension value \"%1$s\" in attribute \"%2$s\"", 409 s, mNames[index])); 410 411 return defValue; 412 } 413 414 /** 415 * Retrieve a dimensional unit attribute at <var>index</var> for use 416 * as an offset in raw pixels. This is the same as 417 * {@link #getDimension}, except the returned value is converted to 418 * integer pixels for you. An offset conversion involves simply 419 * truncating the base value to an integer. 420 * 421 * @param index Index of attribute to retrieve. 422 * @param defValue Value to return if the attribute is not defined or 423 * not a resource. 424 * 425 * @return Attribute dimension value multiplied by the appropriate 426 * metric and truncated to integer pixels, or defValue if not defined. 427 * 428 * @see #getDimension 429 * @see #getDimensionPixelSize 430 */ 431 @Override 432 public int getDimensionPixelOffset(int index, int defValue) { 433 return (int) getDimension(index, defValue); 434 } 435 436 /** 437 * Retrieve a dimensional unit attribute at <var>index</var> for use 438 * as a size in raw pixels. This is the same as 439 * {@link #getDimension}, except the returned value is converted to 440 * integer pixels for use as a size. A size conversion involves 441 * rounding the base value, and ensuring that a non-zero base value 442 * is at least one pixel in size. 443 * 444 * @param index Index of attribute to retrieve. 445 * @param defValue Value to return if the attribute is not defined or 446 * not a resource. 447 * 448 * @return Attribute dimension value multiplied by the appropriate 449 * metric and truncated to integer pixels, or defValue if not defined. 450 * 451 * @see #getDimension 452 * @see #getDimensionPixelOffset 453 */ 454 @Override 455 public int getDimensionPixelSize(int index, int defValue) { 456 if (mData[index] == null) { 457 return defValue; 458 } 459 460 String s = mData[index].getValue(); 461 462 if (s == null) { 463 return defValue; 464 } else if (s.equals(BridgeConstants.MATCH_PARENT) || 465 s.equals(BridgeConstants.FILL_PARENT)) { 466 return LayoutParams.MATCH_PARENT; 467 } else if (s.equals(BridgeConstants.WRAP_CONTENT)) { 468 return LayoutParams.WRAP_CONTENT; 469 } 470 471 // FIXME huh? 472 473 float f = getDimension(index, defValue); 474 final int res = (int)(f+0.5f); 475 if (res != 0) return res; 476 if (f == 0) return 0; 477 if (f > 0) return 1; 478 479 throw new UnsupportedOperationException("Can't convert to dimension: " + 480 Integer.toString(index)); 481 } 482 483 /** 484 * Special version of {@link #getDimensionPixelSize} for retrieving 485 * {@link android.view.ViewGroup}'s layout_width and layout_height 486 * attributes. This is only here for performance reasons; applications 487 * should use {@link #getDimensionPixelSize}. 488 * 489 * @param index Index of the attribute to retrieve. 490 * @param name Textual name of attribute for error reporting. 491 * 492 * @return Attribute dimension value multiplied by the appropriate 493 * metric and truncated to integer pixels. 494 */ 495 @Override 496 public int getLayoutDimension(int index, String name) { 497 return getDimensionPixelSize(index, 0); 498 } 499 500 @Override 501 public int getLayoutDimension(int index, int defValue) { 502 return getDimensionPixelSize(index, defValue); 503 } 504 505 /** 506 * Retrieve a fractional unit attribute at <var>index</var>. 507 * 508 * @param index Index of attribute to retrieve. 509 * @param base The base value of this fraction. In other words, a 510 * standard fraction is multiplied by this value. 511 * @param pbase The parent base value of this fraction. In other 512 * words, a parent fraction (nn%p) is multiplied by this 513 * value. 514 * @param defValue Value to return if the attribute is not defined or 515 * not a resource. 516 * 517 * @return Attribute fractional value multiplied by the appropriate 518 * base value, or defValue if not defined. 519 */ 520 @Override 521 public float getFraction(int index, int base, int pbase, float defValue) { 522 if (mData[index] == null) { 523 return defValue; 524 } 525 526 String value = mData[index].getValue(); 527 if (value == null) { 528 return defValue; 529 } 530 531 if (ResourceHelper.stringToFloat(value, mValue)) { 532 return mValue.getFraction(base, pbase); 533 } 534 535 // looks like we were unable to resolve the fraction value 536 mContext.getLogger().warning(String.format( 537 "Unable to resolve fraction value \"%1$s\" in attribute \"%2$s\"", 538 value, mNames[index])); 539 540 return defValue; 541 } 542 543 /** 544 * Retrieve the resource identifier for the attribute at 545 * <var>index</var>. Note that attribute resource as resolved when 546 * the overall {@link TypedArray} object is retrieved. As a 547 * result, this function will return the resource identifier of the 548 * final resource value that was found, <em>not</em> necessarily the 549 * original resource that was specified by the attribute. 550 * 551 * @param index Index of attribute to retrieve. 552 * @param defValue Value to return if the attribute is not defined or 553 * not a resource. 554 * 555 * @return Attribute resource identifier, or defValue if not defined. 556 */ 557 @Override 558 public int getResourceId(int index, int defValue) { 559 // get the IResource for this index 560 IResourceValue resValue = mData[index]; 561 562 // no data, return the default value. 563 if (resValue == null) { 564 return defValue; 565 } 566 567 // check if this is a style resource 568 if (resValue instanceof IStyleResourceValue) { 569 // get the id that will represent this style. 570 return mContext.getDynamicIdByStyle((IStyleResourceValue)resValue); 571 } 572 573 // if the attribute was a reference to an id, and not a declaration of an id (@+id), then 574 // the xml attribute value was "resolved" which leads us to a IResourceValue with 575 // getType() returning "id" and getName() returning the id name 576 // (and getValue() returning null!). We need to handle this! 577 if (resValue.getType() != null && resValue.getType().equals(BridgeConstants.RES_ID)) { 578 // if this is a framework id 579 if (mPlatformFile || resValue.isFramework()) { 580 // look for idName in the android R classes 581 return mContext.getFrameworkIdValue(resValue.getName(), defValue); 582 } 583 584 // look for idName in the project R class. 585 return mContext.getProjectIdValue(resValue.getName(), defValue); 586 } 587 588 // else, try to get the value, and resolve it somehow. 589 String value = resValue.getValue(); 590 if (value == null) { 591 return defValue; 592 } 593 594 // if the value is just an integer, return it. 595 try { 596 int i = Integer.parseInt(value); 597 if (Integer.toString(i).equals(value)) { 598 return i; 599 } 600 } catch (NumberFormatException e) { 601 // pass 602 } 603 604 // Handle the @id/<name>, @+id/<name> and @android:id/<name> 605 // We need to return the exact value that was compiled (from the various R classes), 606 // as these values can be reused internally with calls to findViewById(). 607 // There's a trick with platform layouts that not use "android:" but their IDs are in 608 // fact in the android.R and com.android.internal.R classes. 609 // The field mPlatformFile will indicate that all IDs are to be looked up in the android R 610 // classes exclusively. 611 612 // if this is a reference to an id, find it. 613 if (value.startsWith("@id/") || value.startsWith("@+") || 614 value.startsWith("@android:id/")) { 615 616 int pos = value.indexOf('/'); 617 String idName = value.substring(pos + 1); 618 619 // if this is a framework id 620 if (mPlatformFile || value.startsWith("@android") || value.startsWith("@+android")) { 621 // look for idName in the android R classes 622 return mContext.getFrameworkIdValue(idName, defValue); 623 } 624 625 // look for idName in the project R class. 626 return mContext.getProjectIdValue(idName, defValue); 627 } 628 629 // not a direct id valid reference? resolve it 630 Integer idValue = null; 631 632 if (resValue.isFramework()) { 633 idValue = Bridge.getResourceValue(resValue.getType(), resValue.getName()); 634 } else { 635 idValue = mContext.getProjectCallback().getResourceValue( 636 resValue.getType(), resValue.getName()); 637 } 638 639 if (idValue != null) { 640 return idValue.intValue(); 641 } 642 643 mContext.getLogger().warning(String.format( 644 "Unable to resolve id \"%1$s\" for attribute \"%2$s\"", value, mNames[index])); 645 return defValue; 646 } 647 648 /** 649 * Retrieve the Drawable for the attribute at <var>index</var>. This 650 * gets the resource ID of the selected attribute, and uses 651 * {@link Resources#getDrawable Resources.getDrawable} of the owning 652 * Resources object to retrieve its Drawable. 653 * 654 * @param index Index of attribute to retrieve. 655 * 656 * @return Drawable for the attribute, or null if not defined. 657 */ 658 @Override 659 public Drawable getDrawable(int index) { 660 if (mData[index] == null) { 661 return null; 662 } 663 664 IResourceValue value = mData[index]; 665 String stringValue = value.getValue(); 666 if (stringValue == null || BridgeConstants.REFERENCE_NULL.equals(stringValue)) { 667 return null; 668 } 669 670 Drawable d = ResourceHelper.getDrawable(value, mContext, mData[index].isFramework()); 671 672 if (d != null) { 673 return d; 674 } 675 676 // looks like we were unable to resolve the drawable 677 mContext.getLogger().warning(String.format( 678 "Unable to resolve drawable \"%1$s\" in attribute \"%2$s\"", stringValue, 679 mNames[index])); 680 681 return null; 682 } 683 684 685 /** 686 * Retrieve the CharSequence[] for the attribute at <var>index</var>. 687 * This gets the resource ID of the selected attribute, and uses 688 * {@link Resources#getTextArray Resources.getTextArray} of the owning 689 * Resources object to retrieve its String[]. 690 * 691 * @param index Index of attribute to retrieve. 692 * 693 * @return CharSequence[] for the attribute, or null if not defined. 694 */ 695 @Override 696 public CharSequence[] getTextArray(int index) { 697 if (mData[index] == null) { 698 return null; 699 } 700 701 String value = mData[index].getValue(); 702 if (value != null) { 703 return new CharSequence[] { value }; 704 } 705 706 mContext.getLogger().warning(String.format( 707 String.format("Unknown value for getTextArray(%d) => %s", //DEBUG 708 index, mData[index].getName()))); 709 710 return null; 711 } 712 713 /** 714 * Retrieve the raw TypedValue for the attribute at <var>index</var>. 715 * 716 * @param index Index of attribute to retrieve. 717 * @param outValue TypedValue object in which to place the attribute's 718 * data. 719 * 720 * @return Returns true if the value was retrieved, else false. 721 */ 722 @Override 723 public boolean getValue(int index, TypedValue outValue) { 724 if (mData[index] == null) { 725 return false; 726 } 727 728 String s = mData[index].getValue(); 729 730 return ResourceHelper.stringToFloat(s, outValue); 731 } 732 733 /** 734 * Determines whether there is an attribute at <var>index</var>. 735 * 736 * @param index Index of attribute to retrieve. 737 * 738 * @return True if the attribute has a value, false otherwise. 739 */ 740 @Override 741 public boolean hasValue(int index) { 742 return mData[index] != null; 743 } 744 745 /** 746 * Retrieve the raw TypedValue for the attribute at <var>index</var> 747 * and return a temporary object holding its data. This object is only 748 * valid until the next call on to {@link TypedArray}. 749 * 750 * @param index Index of attribute to retrieve. 751 * 752 * @return Returns a TypedValue object if the attribute is defined, 753 * containing its data; otherwise returns null. (You will not 754 * receive a TypedValue whose type is TYPE_NULL.) 755 */ 756 @Override 757 public TypedValue peekValue(int index) { 758 if (getValue(index, mValue)) { 759 return mValue; 760 } 761 762 return null; 763 } 764 765 /** 766 * Returns a message about the parser state suitable for printing error messages. 767 */ 768 @Override 769 public String getPositionDescription() { 770 return "<internal -- stub if needed>"; 771 } 772 773 /** 774 * Give back a previously retrieved StyledAttributes, for later re-use. 775 */ 776 @Override 777 public void recycle() { 778 // pass 779 } 780 781 @Override 782 public boolean getValueAt(int index, TypedValue outValue) { 783 // pass 784 return false; 785 } 786 787 @Override 788 public String toString() { 789 return mData.toString(); 790 } 791 } 792