• 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 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:]&lt;name&gt;</li>
515      * <li>[android:]style/&lt;name&gt;</li>
516      * <li>@[android:]style/&lt;name&gt;</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