Home | History | Annotate | Download | only in resources
      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:]&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      */
    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