• 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
findItemInStyle(StyleResourceValue style, String itemName)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
findResValue(String reference, boolean forceFrameworkOnly)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
resolveValue(ResourceType type, String name, String value, boolean isFrameworkValue)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
resolveResValue(ResourceValue resValue)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      */
findResValue(ResourceType resType, String resName, boolean frameworkOnly)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 
getResource(ResourceType resourceType, String resourceName, Map<ResourceType, Map<String, ResourceValue>> resourceRepository)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      */
computeStyleMaps(String themeName, boolean isProjectTheme)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      */
computeStyleInheritance(Collection<ResourceValue> styles, Map<String, ResourceValue> inProjectStyleMap, Map<String, ResourceValue> inFrameworkStyleMap)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      */
getParentName(String styleName)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:]&lt;name&gt;</li>
493      * <li>[android:]style/&lt;name&gt;</li>
494      * <li>@[android:]style/&lt;name&gt;</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      */
getStyle(String parentName, Map<String, ResourceValue> inProjectStyleMap, Map<String, ResourceValue> inFrameworkStyleMap)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