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