1 /* 2 * Copyright (C) 2011 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 17 package com.android.ide.common.resources; 18 19 import com.android.ide.common.rendering.api.LayoutLog; 20 import com.android.ide.common.rendering.api.RenderResources; 21 import com.android.ide.common.rendering.api.ResourceValue; 22 import com.android.ide.common.rendering.api.StyleResourceValue; 23 import com.android.resources.ResourceType; 24 25 import java.util.Collection; 26 import java.util.HashMap; 27 import java.util.Map; 28 29 public class ResourceResolver extends RenderResources { 30 31 /** The constant {@code style/} */ 32 public final static String REFERENCE_STYLE = ResourceType.STYLE.getName() + "/"; 33 /** The constant {@code @android:} */ 34 public final static String PREFIX_ANDROID_RESOURCE_REF = "@android:"; 35 /** The constant {@code @} */ 36 public final static String PREFIX_RESOURCE_REF = "@"; 37 /** The constant {@code ?android:} */ 38 public final static String PREFIX_ANDROID_THEME_REF = "?android:"; 39 /** The constant {@code ?} */ 40 public final static String PREFIX_THEME_REF = "?"; 41 /** The constant {@code android:} */ 42 public final static String PREFIX_ANDROID = "android:"; 43 /** The constant {@code @style/} */ 44 public static final String PREFIX_STYLE = PREFIX_RESOURCE_REF + REFERENCE_STYLE; 45 /** The constant {@code @android:style/} */ 46 public static final String PREFIX_ANDROID_STYLE = PREFIX_ANDROID_RESOURCE_REF 47 + REFERENCE_STYLE; 48 49 private final Map<ResourceType, Map<String, ResourceValue>> mProjectResources; 50 private final Map<ResourceType, Map<String, ResourceValue>> mFrameworkResources; 51 52 private final Map<StyleResourceValue, StyleResourceValue> mStyleInheritanceMap = 53 new HashMap<StyleResourceValue, StyleResourceValue>(); 54 55 private StyleResourceValue mTheme; 56 57 private FrameworkResourceIdProvider mFrameworkProvider; 58 private LayoutLog mLogger; 59 private String mThemeName; 60 private boolean mIsProjectTheme; 61 ResourceResolver( Map<ResourceType, Map<String, ResourceValue>> projectResources, Map<ResourceType, Map<String, ResourceValue>> frameworkResources)62 private ResourceResolver( 63 Map<ResourceType, Map<String, ResourceValue>> projectResources, 64 Map<ResourceType, Map<String, ResourceValue>> frameworkResources) { 65 mProjectResources = projectResources; 66 mFrameworkResources = frameworkResources; 67 } 68 69 /** 70 * Creates a new {@link ResourceResolver} object. 71 * 72 * @param projectResources the project resources. 73 * @param frameworkResources the framework resources. 74 * @param themeName the name of the current theme. 75 * @param isProjectTheme Is this a project theme? 76 * @return a new {@link ResourceResolver} 77 */ create( Map<ResourceType, Map<String, ResourceValue>> projectResources, Map<ResourceType, Map<String, ResourceValue>> frameworkResources, String themeName, boolean isProjectTheme)78 public static ResourceResolver create( 79 Map<ResourceType, Map<String, ResourceValue>> projectResources, 80 Map<ResourceType, Map<String, ResourceValue>> frameworkResources, 81 String themeName, boolean isProjectTheme) { 82 83 ResourceResolver resolver = new ResourceResolver( 84 projectResources, frameworkResources); 85 86 resolver.computeStyleMaps(themeName, isProjectTheme); 87 88 return resolver; 89 } 90 91 // ---- Methods to help dealing with older LayoutLibs. 92 getThemeName()93 public String getThemeName() { 94 return mThemeName; 95 } 96 isProjectTheme()97 public boolean isProjectTheme() { 98 return mIsProjectTheme; 99 } 100 getProjectResources()101 public Map<ResourceType, Map<String, ResourceValue>> getProjectResources() { 102 return mProjectResources; 103 } 104 getFrameworkResources()105 public Map<ResourceType, Map<String, ResourceValue>> getFrameworkResources() { 106 return mFrameworkResources; 107 } 108 109 // ---- RenderResources Methods 110 111 @Override setFrameworkResourceIdProvider(FrameworkResourceIdProvider provider)112 public void setFrameworkResourceIdProvider(FrameworkResourceIdProvider provider) { 113 mFrameworkProvider = provider; 114 } 115 116 @Override setLogger(LayoutLog logger)117 public void setLogger(LayoutLog logger) { 118 mLogger = logger; 119 } 120 121 @Override getCurrentTheme()122 public StyleResourceValue getCurrentTheme() { 123 return mTheme; 124 } 125 126 @Override getTheme(String name, boolean frameworkTheme)127 public StyleResourceValue getTheme(String name, boolean frameworkTheme) { 128 ResourceValue theme = null; 129 130 if (frameworkTheme) { 131 Map<String, ResourceValue> frameworkStyleMap = mFrameworkResources.get( 132 ResourceType.STYLE); 133 theme = frameworkStyleMap.get(name); 134 } else { 135 Map<String, ResourceValue> projectStyleMap = mProjectResources.get(ResourceType.STYLE); 136 theme = projectStyleMap.get(name); 137 } 138 139 if (theme instanceof StyleResourceValue) { 140 return (StyleResourceValue) theme; 141 } 142 143 return null; 144 } 145 146 @Override themeIsParentOf(StyleResourceValue parentTheme, StyleResourceValue childTheme)147 public boolean themeIsParentOf(StyleResourceValue parentTheme, StyleResourceValue childTheme) { 148 do { 149 childTheme = mStyleInheritanceMap.get(childTheme); 150 if (childTheme == null) { 151 return false; 152 } else if (childTheme == parentTheme) { 153 return true; 154 } 155 } while (true); 156 } 157 158 @Override getFrameworkResource(ResourceType resourceType, String resourceName)159 public ResourceValue getFrameworkResource(ResourceType resourceType, String resourceName) { 160 return getResource(resourceType, resourceName, mFrameworkResources); 161 } 162 163 @Override getProjectResource(ResourceType resourceType, String resourceName)164 public ResourceValue getProjectResource(ResourceType resourceType, String resourceName) { 165 return getResource(resourceType, resourceName, mProjectResources); 166 } 167 168 @Override 169 @Deprecated findItemInStyle(StyleResourceValue style, String itemName)170 public ResourceValue findItemInStyle(StyleResourceValue style, String itemName) { 171 ResourceValue item = style.findValue(itemName, style.isFramework()); 172 173 // if we didn't find it, we look in the parent style (if applicable) 174 if (item == null && mStyleInheritanceMap != null) { 175 StyleResourceValue parentStyle = mStyleInheritanceMap.get(style); 176 if (parentStyle != null) { 177 return findItemInStyle(parentStyle, itemName); 178 } 179 } 180 181 return item; 182 } 183 184 @Override findItemInStyle(StyleResourceValue style, String itemName, boolean isFrameworkAttr)185 public ResourceValue findItemInStyle(StyleResourceValue style, String itemName, 186 boolean isFrameworkAttr) { 187 ResourceValue item = style.findValue(itemName, isFrameworkAttr); 188 189 // if we didn't find it, we look in the parent style (if applicable) 190 if (item == null && mStyleInheritanceMap != null) { 191 StyleResourceValue parentStyle = mStyleInheritanceMap.get(style); 192 if (parentStyle != null) { 193 return findItemInStyle(parentStyle, itemName, isFrameworkAttr); 194 } 195 } 196 197 return item; 198 } 199 200 @Override findResValue(String reference, boolean forceFrameworkOnly)201 public ResourceValue findResValue(String reference, boolean forceFrameworkOnly) { 202 if (reference == null) { 203 return null; 204 } 205 if (reference.startsWith(PREFIX_THEME_REF)) { 206 // no theme? no need to go further! 207 if (mTheme == null) { 208 return null; 209 } 210 211 boolean frameworkOnly = false; 212 213 // eliminate the prefix from the string 214 if (reference.startsWith(PREFIX_ANDROID_THEME_REF)) { 215 frameworkOnly = true; 216 reference = reference.substring(PREFIX_ANDROID_THEME_REF.length()); 217 } else { 218 reference = reference.substring(PREFIX_THEME_REF.length()); 219 } 220 221 // at this point, value can contain type/name (drawable/foo for instance). 222 // split it to make sure. 223 String[] segments = reference.split("\\/"); 224 225 // we look for the referenced item name. 226 String referenceName = null; 227 228 if (segments.length == 2) { 229 // there was a resType in the reference. If it's attr, we ignore it 230 // else, we assert for now. 231 if (ResourceType.ATTR.getName().equals(segments[0])) { 232 referenceName = segments[1]; 233 } else { 234 // At this time, no support for ?type/name where type is not "attr" 235 return null; 236 } 237 } else { 238 // it's just an item name. 239 referenceName = segments[0]; 240 } 241 242 // now we look for android: in the referenceName in order to support format 243 // such as: ?attr/android:name 244 if (referenceName.startsWith(PREFIX_ANDROID)) { 245 frameworkOnly = true; 246 referenceName = referenceName.substring(PREFIX_ANDROID.length()); 247 } 248 249 // Now look for the item in the theme, starting with the current one. 250 ResourceValue item = findItemInStyle(mTheme, referenceName, 251 forceFrameworkOnly || frameworkOnly); 252 253 if (item == null && mLogger != null) { 254 mLogger.warning(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR, 255 String.format("Couldn't find theme resource %1$s for the current theme", 256 reference), 257 new ResourceValue(ResourceType.ATTR, referenceName, frameworkOnly)); 258 } 259 260 return item; 261 } else if (reference.startsWith(PREFIX_RESOURCE_REF)) { 262 boolean frameworkOnly = false; 263 264 // check for the specific null reference value. 265 if (REFERENCE_NULL.equals(reference)) { 266 return null; 267 } 268 269 // Eliminate the prefix from the string. 270 if (reference.startsWith(PREFIX_ANDROID_RESOURCE_REF)) { 271 frameworkOnly = true; 272 reference = reference.substring( 273 PREFIX_ANDROID_RESOURCE_REF.length()); 274 } else { 275 reference = reference.substring(PREFIX_RESOURCE_REF.length()); 276 } 277 278 // at this point, value contains type/[android:]name (drawable/foo for instance) 279 String[] segments = reference.split("\\/"); 280 281 // now we look for android: in the resource name in order to support format 282 // such as: @drawable/android:name 283 if (segments[1].startsWith(PREFIX_ANDROID)) { 284 frameworkOnly = true; 285 segments[1] = segments[1].substring(PREFIX_ANDROID.length()); 286 } 287 288 ResourceType type = ResourceType.getEnum(segments[0]); 289 290 // unknown type? 291 if (type == null) { 292 return null; 293 } 294 295 return findResValue(type, segments[1], 296 forceFrameworkOnly ? true :frameworkOnly); 297 } 298 299 // Looks like the value didn't reference anything. Return null. 300 return null; 301 } 302 303 @Override resolveValue(ResourceType type, String name, String value, boolean isFrameworkValue)304 public ResourceValue resolveValue(ResourceType type, String name, String value, 305 boolean isFrameworkValue) { 306 if (value == null) { 307 return null; 308 } 309 310 // get the ResourceValue referenced by this value 311 ResourceValue resValue = findResValue(value, isFrameworkValue); 312 313 // if resValue is null, but value is not null, this means it was not a reference. 314 // we return the name/value wrapper in a ResourceValue. the isFramework flag doesn't 315 // matter. 316 if (resValue == null) { 317 return new ResourceValue(type, name, value, isFrameworkValue); 318 } 319 320 // we resolved a first reference, but we need to make sure this isn't a reference also. 321 return resolveResValue(resValue); 322 } 323 324 @Override resolveResValue(ResourceValue resValue)325 public ResourceValue resolveResValue(ResourceValue resValue) { 326 if (resValue == null) { 327 return null; 328 } 329 330 // if the resource value is null, we simply return it. 331 String value = resValue.getValue(); 332 if (value == null) { 333 return resValue; 334 } 335 336 // else attempt to find another ResourceValue referenced by this one. 337 ResourceValue resolvedResValue = findResValue(value, resValue.isFramework()); 338 339 // if the value did not reference anything, then we simply return the input value 340 if (resolvedResValue == null) { 341 return resValue; 342 } 343 344 // detect potential loop due to mishandled namespace in attributes 345 if (resValue == resolvedResValue) { 346 if (mLogger != null) { 347 mLogger.error(LayoutLog.TAG_BROKEN, 348 String.format("Potential stackoverflow trying to resolve '%s'. Render may not be accurate.", value), 349 null); 350 } 351 return resValue; 352 } 353 354 // otherwise, we attempt to resolve this new value as well 355 return resolveResValue(resolvedResValue); 356 } 357 358 // ---- Private helper methods. 359 360 /** 361 * Searches for, and returns a {@link ResourceValue} by its name, and type. 362 * @param resType the type of the resource 363 * @param resName the name of the resource 364 * @param frameworkOnly if <code>true</code>, the method does not search in the 365 * project resources 366 */ findResValue(ResourceType resType, String resName, boolean frameworkOnly)367 private ResourceValue findResValue(ResourceType resType, String resName, 368 boolean frameworkOnly) { 369 // map of ResouceValue for the given type 370 Map<String, ResourceValue> typeMap; 371 372 // if allowed, search in the project resources first. 373 if (frameworkOnly == false) { 374 typeMap = mProjectResources.get(resType); 375 ResourceValue item = typeMap.get(resName); 376 if (item != null) { 377 return item; 378 } 379 } 380 381 // now search in the framework resources. 382 typeMap = mFrameworkResources.get(resType); 383 ResourceValue item = typeMap.get(resName); 384 if (item != null) { 385 return item; 386 } 387 388 // if it was not found and the type is an id, it is possible that the ID was 389 // generated dynamically when compiling the framework resources. 390 // Look for it in the R map. 391 if (mFrameworkProvider != null && resType == ResourceType.ID) { 392 if (mFrameworkProvider.getId(resType, resName) != null) { 393 return new ResourceValue(resType, resName, true); 394 } 395 } 396 397 // didn't find the resource anywhere. 398 if (mLogger != null) { 399 mLogger.warning(LayoutLog.TAG_RESOURCES_RESOLVE, 400 "Couldn't resolve resource @" + 401 (frameworkOnly ? "android:" : "") + resType + "/" + resName, 402 new ResourceValue(resType, resName, frameworkOnly)); 403 } 404 return null; 405 } 406 getResource(ResourceType resourceType, String resourceName, Map<ResourceType, Map<String, ResourceValue>> resourceRepository)407 private ResourceValue getResource(ResourceType resourceType, String resourceName, 408 Map<ResourceType, Map<String, ResourceValue>> resourceRepository) { 409 Map<String, ResourceValue> typeMap = resourceRepository.get(resourceType); 410 if (typeMap != null) { 411 ResourceValue item = typeMap.get(resourceName); 412 if (item != null) { 413 item = resolveResValue(item); 414 return item; 415 } 416 } 417 418 // didn't find the resource anywhere. 419 return null; 420 421 } 422 423 /** 424 * Compute style information from the given list of style for the project and framework. 425 * @param themeName the name of the current theme. 426 * @param isProjectTheme Is this a project theme? 427 */ computeStyleMaps(String themeName, boolean isProjectTheme)428 private void computeStyleMaps(String themeName, boolean isProjectTheme) { 429 mThemeName = themeName; 430 mIsProjectTheme = isProjectTheme; 431 Map<String, ResourceValue> projectStyleMap = mProjectResources.get(ResourceType.STYLE); 432 Map<String, ResourceValue> frameworkStyleMap = mFrameworkResources.get(ResourceType.STYLE); 433 434 // first, get the theme 435 ResourceValue theme = null; 436 437 // project theme names have been prepended with a * 438 if (isProjectTheme) { 439 theme = projectStyleMap.get(themeName); 440 } else { 441 theme = frameworkStyleMap.get(themeName); 442 } 443 444 if (theme instanceof StyleResourceValue) { 445 // compute the inheritance map for both the project and framework styles 446 computeStyleInheritance(projectStyleMap.values(), projectStyleMap, 447 frameworkStyleMap); 448 449 // Compute the style inheritance for the framework styles/themes. 450 // Since, for those, the style parent values do not contain 'android:' 451 // we want to force looking in the framework style only to avoid using 452 // similarly named styles from the project. 453 // To do this, we pass null in lieu of the project style map. 454 computeStyleInheritance(frameworkStyleMap.values(), null /*inProjectStyleMap */, 455 frameworkStyleMap); 456 457 mTheme = (StyleResourceValue) theme; 458 } 459 } 460 461 462 463 /** 464 * Compute the parent style for all the styles in a given list. 465 * @param styles the styles for which we compute the parent. 466 * @param inProjectStyleMap the map of project styles. 467 * @param inFrameworkStyleMap the map of framework styles. 468 * @param outInheritanceMap the map of style inheritance. This is filled by the method. 469 */ computeStyleInheritance(Collection<ResourceValue> styles, Map<String, ResourceValue> inProjectStyleMap, Map<String, ResourceValue> inFrameworkStyleMap)470 private void computeStyleInheritance(Collection<ResourceValue> styles, 471 Map<String, ResourceValue> inProjectStyleMap, 472 Map<String, ResourceValue> inFrameworkStyleMap) { 473 for (ResourceValue value : styles) { 474 if (value instanceof StyleResourceValue) { 475 StyleResourceValue style = (StyleResourceValue)value; 476 StyleResourceValue parentStyle = null; 477 478 // first look for a specified parent. 479 String parentName = style.getParentStyle(); 480 481 // no specified parent? try to infer it from the name of the style. 482 if (parentName == null) { 483 parentName = getParentName(value.getName()); 484 } 485 486 if (parentName != null) { 487 parentStyle = getStyle(parentName, inProjectStyleMap, inFrameworkStyleMap); 488 489 if (parentStyle != null) { 490 mStyleInheritanceMap.put(style, parentStyle); 491 } 492 } 493 } 494 } 495 } 496 497 498 /** 499 * Computes the name of the parent style, or <code>null</code> if the style is a root style. 500 */ getParentName(String styleName)501 private String getParentName(String styleName) { 502 int index = styleName.lastIndexOf('.'); 503 if (index != -1) { 504 return styleName.substring(0, index); 505 } 506 507 return null; 508 } 509 510 /** 511 * Searches for and returns the {@link StyleResourceValue} from a given name. 512 * <p/>The format of the name can be: 513 * <ul> 514 * <li>[android:]<name></li> 515 * <li>[android:]style/<name></li> 516 * <li>@[android:]style/<name></li> 517 * </ul> 518 * @param parentName the name of the style. 519 * @param inProjectStyleMap the project style map. Can be <code>null</code> 520 * @param inFrameworkStyleMap the framework style map. 521 * @return The matching {@link StyleResourceValue} object or <code>null</code> if not found. 522 */ getStyle(String parentName, Map<String, ResourceValue> inProjectStyleMap, Map<String, ResourceValue> inFrameworkStyleMap)523 private StyleResourceValue getStyle(String parentName, 524 Map<String, ResourceValue> inProjectStyleMap, 525 Map<String, ResourceValue> inFrameworkStyleMap) { 526 boolean frameworkOnly = false; 527 528 String name = parentName; 529 530 // remove the useless @ if it's there 531 if (name.startsWith(PREFIX_RESOURCE_REF)) { 532 name = name.substring(PREFIX_RESOURCE_REF.length()); 533 } 534 535 // check for framework identifier. 536 if (name.startsWith(PREFIX_ANDROID)) { 537 frameworkOnly = true; 538 name = name.substring(PREFIX_ANDROID.length()); 539 } 540 541 // at this point we could have the format <type>/<name>. we want only the name as long as 542 // the type is style. 543 if (name.startsWith(REFERENCE_STYLE)) { 544 name = name.substring(REFERENCE_STYLE.length()); 545 } else if (name.indexOf('/') != -1) { 546 return null; 547 } 548 549 ResourceValue parent = null; 550 551 // if allowed, search in the project resources. 552 if (frameworkOnly == false && inProjectStyleMap != null) { 553 parent = inProjectStyleMap.get(name); 554 } 555 556 // if not found, then look in the framework resources. 557 if (parent == null) { 558 parent = inFrameworkStyleMap.get(name); 559 } 560 561 // make sure the result is the proper class type and return it. 562 if (parent instanceof StyleResourceValue) { 563 return (StyleResourceValue)parent; 564 } 565 566 if (mLogger != null) { 567 mLogger.error(LayoutLog.TAG_RESOURCES_RESOLVE, 568 String.format("Unable to resolve parent style name: %s", parentName), 569 null /*data*/); 570 } 571 572 return null; 573 } 574 } 575