1 package org.robolectric.res.android; 2 3 import static org.robolectric.res.android.ApkAssetsCookie.K_INVALID_COOKIE; 4 import static org.robolectric.res.android.ApkAssetsCookie.kInvalidCookie; 5 import static org.robolectric.res.android.Util.ALOGI; 6 7 import java.util.Arrays; 8 import org.robolectric.res.android.CppAssetManager2.ResolvedBag; 9 import org.robolectric.res.android.CppAssetManager2.ResolvedBag.Entry; 10 import org.robolectric.res.android.CppAssetManager2.Theme; 11 import org.robolectric.res.android.ResourceTypes.Res_value; 12 13 // transliterated from 14 // https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/AttributeResolution.cpp and 15 // https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/include/androidfw/AttributeResolution.h 16 17 public class AttributeResolution9 { 18 public static final boolean kThrowOnBadId = false; 19 private static final boolean kDebugStyles = false; 20 21 // Offsets into the outValues array populated by the methods below. outValues is a uint32_t 22 // array, but each logical element takes up 6 uint32_t-sized physical elements. 23 public static final int STYLE_NUM_ENTRIES = 6; 24 public static final int STYLE_TYPE = 0; 25 public static final int STYLE_DATA = 1; 26 public static final int STYLE_ASSET_COOKIE = 2; 27 public static final int STYLE_RESOURCE_ID = 3; 28 public static final int STYLE_CHANGING_CONFIGURATIONS = 4; 29 public static final int STYLE_DENSITY = 5; 30 31 // Java asset cookies have 0 as an invalid cookie, but TypedArray expects < 0. ApkAssetsCookieToJavaCookie(ApkAssetsCookie cookie)32 private static int ApkAssetsCookieToJavaCookie(ApkAssetsCookie cookie) { 33 return cookie.intValue() != kInvalidCookie ? (cookie.intValue() + 1) : -1; 34 } 35 36 public static class XmlAttributeFinder { 37 38 private ResXMLParser xmlParser; 39 XmlAttributeFinder(ResXMLParser xmlParser)40 XmlAttributeFinder(ResXMLParser xmlParser) { 41 this.xmlParser = xmlParser; 42 } 43 Find(int curIdent)44 public int Find(int curIdent) { 45 if (xmlParser == null) { 46 return -1; 47 } 48 49 int attributeCount = xmlParser.getAttributeCount(); 50 for (int i = 0; i < attributeCount; i++) { 51 if (xmlParser.getAttributeNameResID(i) == curIdent) { 52 return i; 53 } 54 } 55 return -1; 56 } 57 } 58 59 public static class BagAttributeFinder { 60 private final Entry[] bagEntries; 61 BagAttributeFinder(ResolvedBag bag)62 BagAttributeFinder(ResolvedBag bag) { 63 this.bagEntries = bag == null ? null : bag.entries; 64 } 65 66 // Robolectric: unoptimized relative to Android impl Find(int ident)67 Entry Find(int ident) { 68 Entry needle = new Entry(); 69 needle.key = ident; 70 71 if (bagEntries == null) { 72 return null; 73 } 74 75 int i = Arrays.binarySearch(bagEntries, needle, (o1, o2) -> o1.key - o2.key); 76 return i < 0 ? null : bagEntries[i]; 77 } 78 } 79 80 // These are all variations of the same method. They each perform the exact same operation, 81 // but on various data sources. I *think* they are re-written to avoid an extra branch 82 // in the inner loop, but after one branch miss (some pointer != null), the branch predictor should 83 // predict the rest of the iterations' branch correctly. 84 // TODO(adamlesinski): Run performance tests against these methods and a new, single method 85 // that uses all the sources and branches to the right ones within the inner loop. 86 87 // `out_values` must NOT be nullptr. 88 // `out_indices` may be nullptr. ResolveAttrs(Theme theme, int def_style_attr, int def_style_res, int[] src_values, int src_values_length, int[] attrs, int attrs_length, int[] out_values, int[] out_indices)89 public static boolean ResolveAttrs(Theme theme, int def_style_attr, 90 int def_style_res, int[] src_values, 91 int src_values_length, int[] attrs, 92 int attrs_length, int[] out_values, int[] out_indices) { 93 if (kDebugStyles) { 94 ALOGI("APPLY STYLE: theme=0x%p defStyleAttr=0x%x defStyleRes=0x%x", theme, 95 def_style_attr, def_style_res); 96 } 97 98 CppAssetManager2 assetmanager = theme.GetAssetManager(); 99 ResTable_config config = new ResTable_config(); 100 Res_value value; 101 102 int indicesIdx = 0; 103 104 // Load default style from attribute, if specified... 105 final Ref<Integer> def_style_flags = new Ref<>(0); 106 if (def_style_attr != 0) { 107 final Ref<Res_value> valueRef = new Ref<>(null); 108 if (theme.GetAttribute(def_style_attr, valueRef, def_style_flags).intValue() != kInvalidCookie) { 109 value = valueRef.get(); 110 if (value.dataType == Res_value.TYPE_REFERENCE) { 111 def_style_res = value.data; 112 } 113 } 114 } 115 116 // Retrieve the default style bag, if requested. 117 ResolvedBag default_style_bag = null; 118 if (def_style_res != 0) { 119 default_style_bag = assetmanager.GetBag(def_style_res); 120 if (default_style_bag != null) { 121 def_style_flags.set(def_style_flags.get() | default_style_bag.type_spec_flags); 122 } 123 } 124 BagAttributeFinder def_style_attr_finder = new BagAttributeFinder(default_style_bag); 125 126 // Now iterate through all of the attributes that the client has requested, 127 // filling in each with whatever data we can find. 128 int destOffset = 0; 129 for (int ii=0; ii<attrs_length; ii++) { 130 final int cur_ident = attrs[ii]; 131 132 if (kDebugStyles) { 133 ALOGI("RETRIEVING ATTR 0x%08x...", cur_ident); 134 } 135 136 ApkAssetsCookie cookie = K_INVALID_COOKIE; 137 int type_set_flags = 0; 138 139 value = Res_value.NULL_VALUE; 140 config.density = 0; 141 142 // Try to find a value for this attribute... we prioritize values 143 // coming from, first XML attributes, then XML style, then default 144 // style, and finally the theme. 145 146 // Retrieve the current input value if available. 147 if (src_values_length > 0 && src_values[ii] != 0) { 148 value = new Res_value((byte) Res_value.TYPE_ATTRIBUTE, src_values[ii]); 149 if (kDebugStyles) { 150 ALOGI("-> From values: type=0x%x, data=0x%08x", value.dataType, value.data); 151 } 152 } else { 153 final Entry entry = def_style_attr_finder.Find(cur_ident); 154 if (entry != null) { 155 cookie = entry.cookie; 156 type_set_flags = def_style_flags.get(); 157 value = entry.value; 158 if (kDebugStyles) { 159 ALOGI("-> From def style: type=0x%x, data=0x%08x", value.dataType, value.data); 160 } 161 } 162 } 163 164 int resid = 0; 165 final Ref<Res_value> valueRef = new Ref<>(value); 166 final Ref<Integer> residRef = new Ref<>(resid); 167 final Ref<Integer> type_set_flagsRef = new Ref<>(type_set_flags); 168 final Ref<ResTable_config> configRef = new Ref<>(config); 169 if (value.dataType != Res_value.TYPE_NULL) { 170 // Take care of resolving the found resource to its final value. 171 ApkAssetsCookie new_cookie = 172 theme.ResolveAttributeReference(cookie, valueRef, configRef, type_set_flagsRef, residRef); 173 if (new_cookie.intValue() != kInvalidCookie) { 174 cookie = new_cookie; 175 } 176 if (kDebugStyles) { 177 ALOGI("-> Resolved attr: type=0x%x, data=0x%08x", value.dataType, value.data); 178 } 179 } else if (value.data != Res_value.DATA_NULL_EMPTY) { 180 // If we still don't have a value for this attribute, try to find it in the theme! 181 ApkAssetsCookie new_cookie = theme.GetAttribute(cur_ident, valueRef, type_set_flagsRef); 182 if (new_cookie.intValue() != kInvalidCookie) { 183 if (kDebugStyles) { 184 ALOGI("-> From theme: type=0x%x, data=0x%08x", value.dataType, value.data); 185 } 186 new_cookie = 187 assetmanager.ResolveReference(new_cookie, valueRef, configRef, type_set_flagsRef, residRef); 188 if (new_cookie.intValue() != kInvalidCookie) { 189 cookie = new_cookie; 190 } 191 if (kDebugStyles) { 192 ALOGI("-> Resolved theme: type=0x%x, data=0x%08x", value.dataType, value.data); 193 } 194 } 195 } 196 value = valueRef.get(); 197 resid = residRef.get(); 198 type_set_flags = type_set_flagsRef.get(); 199 config = configRef.get(); 200 201 // Deal with the special @null value -- it turns back to TYPE_NULL. 202 if (value.dataType == Res_value.TYPE_REFERENCE && value.data == 0) { 203 if (kDebugStyles) { 204 ALOGI("-> Setting to @null!"); 205 } 206 value = Res_value.NULL_VALUE; 207 cookie = K_INVALID_COOKIE; 208 } 209 210 if (kDebugStyles) { 211 ALOGI("Attribute 0x%08x: type=0x%x, data=0x%08x", cur_ident, value.dataType, 212 value.data); 213 } 214 215 // Write the final value back to Java. 216 out_values[destOffset + STYLE_TYPE] = value.dataType; 217 out_values[destOffset + STYLE_DATA] = value.data; 218 out_values[destOffset + STYLE_ASSET_COOKIE] = ApkAssetsCookieToJavaCookie(cookie); 219 out_values[destOffset + STYLE_RESOURCE_ID] = resid; 220 out_values[destOffset + STYLE_CHANGING_CONFIGURATIONS] = type_set_flags; 221 out_values[destOffset + STYLE_DENSITY] = config.density; 222 223 if (out_indices != null && value.dataType != Res_value.TYPE_NULL) { 224 indicesIdx++; 225 out_indices[indicesIdx] = ii; 226 } 227 228 destOffset += STYLE_NUM_ENTRIES; 229 } 230 231 if (out_indices != null) { 232 out_indices[0] = indicesIdx; 233 } 234 return true; 235 } 236 ApplyStyle(Theme theme, ResXMLParser xml_parser, int def_style_attr, int def_style_resid, int[] attrs, int attrs_length, int[] out_values, int[] out_indices)237 public static void ApplyStyle(Theme theme, ResXMLParser xml_parser, int def_style_attr, 238 int def_style_resid, int[] attrs, int attrs_length, 239 int[] out_values, int[] out_indices) { 240 if (kDebugStyles) { 241 ALOGI("APPLY STYLE: theme=%s defStyleAttr=0x%x defStyleRes=0x%x xml=%s", 242 theme, def_style_attr, def_style_resid, xml_parser); 243 } 244 245 CppAssetManager2 assetmanager = theme.GetAssetManager(); 246 final Ref<ResTable_config> config = new Ref<>(new ResTable_config()); 247 final Ref<Res_value> value = new Ref<>(new Res_value()); 248 249 int indices_idx = 0; 250 251 // Load default style from attribute, if specified... 252 final Ref<Integer> def_style_flags = new Ref<>(0); 253 if (def_style_attr != 0) { 254 if (theme.GetAttribute(def_style_attr, value, def_style_flags).intValue() != kInvalidCookie) { 255 if (value.get().dataType == DataType.REFERENCE.code()) { 256 def_style_resid = value.get().data; 257 } 258 } 259 } 260 261 // Retrieve the style resource ID associated with the current XML tag's style attribute. 262 int style_resid = 0; 263 final Ref<Integer> style_flags = new Ref<>(0); 264 if (xml_parser != null) { 265 int idx = xml_parser.indexOfStyle(); 266 if (idx >= 0 && xml_parser.getAttributeValue(idx, value) >= 0) { 267 if (value.get().dataType == DataType.ATTRIBUTE.code()) { 268 // Resolve the attribute with out theme. 269 if (theme.GetAttribute(value.get().data, value, style_flags).intValue() == kInvalidCookie) { 270 value.set(value.get().withType(DataType.NULL.code())); 271 } 272 } 273 274 if (value.get().dataType == DataType.REFERENCE.code()) { 275 style_resid = value.get().data; 276 } 277 } 278 } 279 280 // Retrieve the default style bag, if requested. 281 ResolvedBag default_style_bag = null; 282 if (def_style_resid != 0) { 283 default_style_bag = assetmanager.GetBag(def_style_resid); 284 if (default_style_bag != null) { 285 def_style_flags.set(def_style_flags.get() | default_style_bag.type_spec_flags); 286 } 287 } 288 289 BagAttributeFinder def_style_attr_finder = new BagAttributeFinder(default_style_bag); 290 291 // Retrieve the style class bag, if requested. 292 ResolvedBag xml_style_bag = null; 293 if (style_resid != 0) { 294 xml_style_bag = assetmanager.GetBag(style_resid); 295 if (xml_style_bag != null) { 296 style_flags.set(style_flags.get() | xml_style_bag.type_spec_flags); 297 } 298 } 299 300 BagAttributeFinder xml_style_attr_finder = new BagAttributeFinder(xml_style_bag); 301 302 // Retrieve the XML attributes, if requested. 303 XmlAttributeFinder xml_attr_finder = new XmlAttributeFinder(xml_parser); 304 305 // Now iterate through all of the attributes that the client has requested, 306 // filling in each with whatever data we can find. 307 for (int ii = 0; ii < attrs_length; ii++) { 308 final int cur_ident = attrs[ii]; 309 310 if (kDebugStyles) { 311 ALOGI("RETRIEVING ATTR 0x%08x...", cur_ident); 312 } 313 314 ApkAssetsCookie cookie = K_INVALID_COOKIE; 315 final Ref<Integer> type_set_flags = new Ref<>(0); 316 317 value.set(Res_value.NULL_VALUE); 318 config.get().density = 0; 319 320 // Try to find a value for this attribute... we prioritize values 321 // coming from, first XML attributes, then XML style, then default 322 // style, and finally the theme. 323 324 // Walk through the xml attributes looking for the requested attribute. 325 int xml_attr_idx = xml_attr_finder.Find(cur_ident); 326 if (xml_attr_idx != -1) { 327 // We found the attribute we were looking for. 328 xml_parser.getAttributeValue(xml_attr_idx, value); 329 type_set_flags.set(style_flags.get()); 330 if (kDebugStyles) { 331 ALOGI("-> From XML: type=0x%x, data=0x%08x", value.get().dataType, value.get().data); 332 } 333 } 334 335 if (value.get().dataType == DataType.NULL.code() && value.get().data != Res_value.DATA_NULL_EMPTY) { 336 // Walk through the style class values looking for the requested attribute. 337 Entry entry = xml_style_attr_finder.Find(cur_ident); 338 if (entry != null) { 339 // We found the attribute we were looking for. 340 cookie = entry.cookie; 341 type_set_flags.set(style_flags.get()); 342 value.set(entry.value); 343 if (kDebugStyles) { 344 ALOGI("-> From style: type=0x%x, data=0x%08x", value.get().dataType, value.get().data); 345 } 346 } 347 } 348 349 if (value.get().dataType == DataType.NULL.code() && value.get().data != Res_value.DATA_NULL_EMPTY) { 350 // Walk through the default style values looking for the requested attribute. 351 Entry entry = def_style_attr_finder.Find(cur_ident); 352 if (entry != null) { 353 // We found the attribute we were looking for. 354 cookie = entry.cookie; 355 type_set_flags.set(def_style_flags.get()); 356 357 value.set(entry.value); 358 if (kDebugStyles) { 359 ALOGI("-> From def style: type=0x%x, data=0x%08x", value.get().dataType, value.get().data); 360 } 361 } 362 } 363 364 final Ref<Integer> resid = new Ref<>(0); 365 if (value.get().dataType != DataType.NULL.code()) { 366 // Take care of resolving the found resource to its final value. 367 ApkAssetsCookie new_cookie = 368 theme.ResolveAttributeReference(cookie, value, config, type_set_flags, resid); 369 if (new_cookie.intValue() != kInvalidCookie) { 370 cookie = new_cookie; 371 } 372 373 if (kDebugStyles) { 374 ALOGI("-> Resolved attr: type=0x%x, data=0x%08x", value.get().dataType, value.get().data); 375 } 376 } else if (value.get().data != Res_value.DATA_NULL_EMPTY) { 377 // If we still don't have a value for this attribute, try to find it in the theme! 378 ApkAssetsCookie new_cookie = theme.GetAttribute(cur_ident, value, type_set_flags); 379 if (new_cookie.intValue() != kInvalidCookie) { 380 if (kDebugStyles) { 381 ALOGI("-> From theme: type=0x%x, data=0x%08x", value.get().dataType, value.get().data); 382 } 383 new_cookie = 384 assetmanager.ResolveReference(new_cookie, value, config, type_set_flags, resid); 385 if (new_cookie.intValue() != kInvalidCookie) { 386 cookie = new_cookie; 387 } 388 389 if (kDebugStyles) { 390 ALOGI("-> Resolved theme: type=0x%x, data=0x%08x", value.get().dataType, value.get().data); 391 } 392 } 393 } 394 395 // Deal with the special @null value -- it turns back to TYPE_NULL. 396 if (value.get().dataType == DataType.REFERENCE.code() && value.get().data == 0) { 397 if (kDebugStyles) { 398 ALOGI(". Setting to @null!"); 399 } 400 value.set(Res_value.NULL_VALUE); 401 cookie = K_INVALID_COOKIE; 402 } 403 404 if (kDebugStyles) { 405 ALOGI("Attribute 0x%08x: type=0x%x, data=0x%08x", cur_ident, value.get().dataType, value.get().data); 406 } 407 408 // Write the final value back to Java. 409 int destIndex = ii * STYLE_NUM_ENTRIES; 410 Res_value res_value = value.get(); 411 out_values[destIndex + STYLE_TYPE] = res_value.dataType; 412 out_values[destIndex + STYLE_DATA] = res_value.data; 413 out_values[destIndex + STYLE_ASSET_COOKIE] = ApkAssetsCookieToJavaCookie(cookie); 414 out_values[destIndex + STYLE_RESOURCE_ID] = resid.get(); 415 out_values[destIndex + STYLE_CHANGING_CONFIGURATIONS] = type_set_flags.get(); 416 out_values[destIndex + STYLE_DENSITY] = config.get().density; 417 418 if (res_value.dataType != DataType.NULL.code() || res_value.data == Res_value.DATA_NULL_EMPTY) { 419 indices_idx++; 420 421 // out_indices must NOT be nullptr. 422 out_indices[indices_idx] = ii; 423 } 424 425 // Robolectric-custom: 426 // if (false && res_value.dataType == DataType.ATTRIBUTE.code()) { 427 // final Ref<ResourceName> attrName = new Ref<>(null); 428 // final Ref<ResourceName> attrRefName = new Ref<>(null); 429 // boolean gotName = assetmanager.GetResourceName(cur_ident, attrName); 430 // boolean gotRefName = assetmanager.GetResourceName(res_value.data, attrRefName); 431 // Logger.warn( 432 // "Failed to resolve attribute lookup: %s=\"?%s\"; theme: %s", 433 // gotName ? attrName.get() : "unknown", gotRefName ? attrRefName.get() : "unknown", 434 // theme); 435 // } 436 437 // out_values += STYLE_NUM_ENTRIES; 438 } 439 440 // out_indices must NOT be nullptr. 441 out_indices[0] = indices_idx; 442 } 443 RetrieveAttributes(CppAssetManager2 assetmanager, ResXMLParser xml_parser, int[] attrs, int attrs_length, int[] out_values, int[] out_indices)444 public static boolean RetrieveAttributes(CppAssetManager2 assetmanager, ResXMLParser xml_parser, int[] attrs, 445 int attrs_length, int[] out_values, int[] out_indices) { 446 final Ref<ResTable_config> config = new Ref<>(new ResTable_config()); 447 final Ref<Res_value> value = new Ref<>(null); 448 449 int indices_idx = 0; 450 451 // Retrieve the XML attributes, if requested. 452 final int xml_attr_count = xml_parser.getAttributeCount(); 453 int ix = 0; 454 int cur_xml_attr = xml_parser.getAttributeNameResID(ix); 455 456 // Now iterate through all of the attributes that the client has requested, 457 // filling in each with whatever data we can find. 458 int baseDest = 0; 459 for (int ii = 0; ii < attrs_length; ii++) { 460 final int cur_ident = attrs[ii]; 461 ApkAssetsCookie cookie = K_INVALID_COOKIE; 462 final Ref<Integer> type_set_flags = new Ref<>(0); 463 464 value.set(Res_value.NULL_VALUE); 465 config.get().density = 0; 466 467 // Try to find a value for this attribute... 468 // Skip through XML attributes until the end or the next possible match. 469 while (ix < xml_attr_count && cur_ident > cur_xml_attr) { 470 ix++; 471 cur_xml_attr = xml_parser.getAttributeNameResID(ix); 472 } 473 // Retrieve the current XML attribute if it matches, and step to next. 474 if (ix < xml_attr_count && cur_ident == cur_xml_attr) { 475 xml_parser.getAttributeValue(ix, value); 476 ix++; 477 cur_xml_attr = xml_parser.getAttributeNameResID(ix); 478 } 479 480 final Ref<Integer> resid = new Ref<>(0); 481 if (value.get().dataType != Res_value.TYPE_NULL) { 482 // Take care of resolving the found resource to its final value. 483 ApkAssetsCookie new_cookie = 484 assetmanager.ResolveReference(cookie, value, config, type_set_flags, resid); 485 if (new_cookie.intValue() != kInvalidCookie) { 486 cookie = new_cookie; 487 } 488 } 489 490 // Deal with the special @null value -- it turns back to TYPE_NULL. 491 if (value.get().dataType == Res_value.TYPE_REFERENCE && value.get().data == 0) { 492 value.set(Res_value.NULL_VALUE); 493 cookie = K_INVALID_COOKIE; 494 } 495 496 // Write the final value back to Java. 497 out_values[baseDest + STYLE_TYPE] = value.get().dataType; 498 out_values[baseDest + STYLE_DATA] = value.get().data; 499 out_values[baseDest + STYLE_ASSET_COOKIE] = ApkAssetsCookieToJavaCookie(cookie); 500 out_values[baseDest + STYLE_RESOURCE_ID] = resid.get(); 501 out_values[baseDest + STYLE_CHANGING_CONFIGURATIONS] = type_set_flags.get(); 502 out_values[baseDest + STYLE_DENSITY] = config.get().density; 503 504 if (out_indices != null && 505 (value.get().dataType != Res_value.TYPE_NULL 506 || value.get().data == Res_value.DATA_NULL_EMPTY)) { 507 indices_idx++; 508 out_indices[indices_idx] = ii; 509 } 510 511 // out_values += STYLE_NUM_ENTRIES; 512 baseDest += STYLE_NUM_ENTRIES; 513 } 514 515 if (out_indices != null) { 516 out_indices[0] = indices_idx; 517 } 518 519 return true; 520 } 521 } 522