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