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 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 
     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      */
     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 
     82     public String getThemeName() {
     83         return mThemeName;
     84     }
     85 
     86     public boolean isProjectTheme() {
     87         return mIsProjectTheme;
     88     }
     89 
     90     public Map<ResourceType, Map<String, ResourceValue>> getProjectResources() {
     91         return mProjectResources;
     92     }
     93 
     94     public Map<ResourceType, Map<String, ResourceValue>> getFrameworkResources() {
     95         return mFrameworkResources;
     96     }
     97 
     98     // ---- RenderResources Methods
     99 
    100     @Override
    101     public void setFrameworkResourceIdProvider(FrameworkResourceIdProvider provider) {
    102         mFrameworkProvider = provider;
    103     }
    104 
    105     @Override
    106     public void setLogger(LayoutLog logger) {
    107         mLogger = logger;
    108     }
    109 
    110     @Override
    111     public StyleResourceValue getCurrentTheme() {
    112         return mTheme;
    113     }
    114 
    115     @Override
    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
    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
    148     public ResourceValue getFrameworkResource(ResourceType resourceType, String resourceName) {
    149         return getResource(resourceType, resourceName, mFrameworkResources);
    150     }
    151 
    152     @Override
    153     public ResourceValue getProjectResource(ResourceType resourceType, String resourceName) {
    154         return getResource(resourceType, resourceName, mProjectResources);
    155     }
    156 
    157     @Override
    158     @Deprecated
    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
    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
    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
    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
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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