• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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:]&lt;name&gt;</li>
501      * <li>[android:]style/&lt;name&gt;</li>
502      * <li>@[android:]style/&lt;name&gt;</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