Home | History | Annotate | Download | only in manager
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
      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.eclipse.adt.internal.resources.manager;
     18 
     19 import com.android.ide.eclipse.adt.internal.project.ProjectState;
     20 import com.android.ide.eclipse.adt.internal.resources.IResourceRepository;
     21 import com.android.ide.eclipse.adt.internal.resources.ResourceItem;
     22 import com.android.ide.eclipse.adt.internal.resources.ResourceType;
     23 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
     24 import com.android.ide.eclipse.adt.internal.resources.configurations.LanguageQualifier;
     25 import com.android.ide.eclipse.adt.internal.resources.configurations.RegionQualifier;
     26 import com.android.ide.eclipse.adt.internal.resources.configurations.ResourceQualifier;
     27 import com.android.ide.eclipse.adt.internal.resources.manager.files.IFolderWrapper;
     28 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     29 import com.android.layoutlib.api.IResourceValue;
     30 import com.android.layoutlib.utils.ResourceValue;
     31 import com.android.sdklib.io.IAbstractFolder;
     32 
     33 import org.eclipse.core.resources.IFolder;
     34 import org.eclipse.core.resources.IProject;
     35 
     36 import java.util.ArrayList;
     37 import java.util.Collection;
     38 import java.util.Collections;
     39 import java.util.HashMap;
     40 import java.util.List;
     41 import java.util.Map;
     42 import java.util.Set;
     43 import java.util.SortedSet;
     44 import java.util.TreeSet;
     45 import java.util.Map.Entry;
     46 
     47 /**
     48  * Represents the resources of a project. This is a file view of the resources, with handling
     49  * for the alternate resource types. For a compiled view use CompiledResources.
     50  */
     51 public class ProjectResources implements IResourceRepository {
     52     private final static int DYNAMIC_ID_SEED_START = 0; // this should not conflict with any
     53                                                         // project IDs that start at a much higher
     54                                                         // value
     55 
     56     private final HashMap<ResourceFolderType, List<ResourceFolder>> mFolderMap =
     57         new HashMap<ResourceFolderType, List<ResourceFolder>>();
     58 
     59     private final HashMap<ResourceType, List<ProjectResourceItem>> mResourceMap =
     60         new HashMap<ResourceType, List<ProjectResourceItem>>();
     61 
     62     /** Map of (name, id) for resources of type {@link ResourceType#ID} coming from R.java */
     63     private Map<String, Map<String, Integer>> mResourceValueMap;
     64     /** Map of (id, [name, resType]) for all resources coming from R.java */
     65     private Map<Integer, String[]> mResIdValueToNameMap;
     66     /** Map of (int[], name) for styleable resources coming from R.java */
     67     private Map<IntArrayWrapper, String> mStyleableValueToNameMap;
     68 
     69     private final Map<String, Integer> mDynamicIds = new HashMap<String, Integer>();
     70     private int mDynamicSeed = DYNAMIC_ID_SEED_START;
     71 
     72     /** Cached list of {@link IdResourceItem}. This is mix of IdResourceItem created by
     73      * {@link MultiResourceFile} for ids coming from XML files under res/values and
     74      * {@link IdResourceItem} created manually, from the list coming from R.java */
     75     private final ArrayList<IdResourceItem> mIdResourceList = new ArrayList<IdResourceItem>();
     76 
     77     private final boolean mIsFrameworkRepository;
     78     private final IProject mProject;
     79 
     80     private final IntArrayWrapper mWrapper = new IntArrayWrapper(null);
     81 
     82 
     83     /**
     84      * Makes a ProjectResources for a given <var>project</var>.
     85      * @param project the project.
     86      */
     87     public ProjectResources(IProject project) {
     88         mIsFrameworkRepository = false;
     89         mProject = project;
     90     }
     91 
     92     /**
     93      * Makes a ProjectResource for a framework repository.
     94      *
     95      * @see #isSystemRepository()
     96      */
     97     public ProjectResources() {
     98         mIsFrameworkRepository = true;
     99         mProject = null;
    100     }
    101 
    102     /**
    103      * Returns whether this ProjectResources is for a project or for a framework.
    104      */
    105     public boolean isSystemRepository() {
    106         return mIsFrameworkRepository;
    107     }
    108 
    109     /**
    110      * Adds a Folder Configuration to the project.
    111      * @param type The resource type.
    112      * @param config The resource configuration.
    113      * @param folder The workspace folder object.
    114      * @return the {@link ResourceFolder} object associated to this folder.
    115      */
    116     protected ResourceFolder add(ResourceFolderType type, FolderConfiguration config,
    117             IAbstractFolder folder) {
    118         // get the list for the resource type
    119         List<ResourceFolder> list = mFolderMap.get(type);
    120 
    121         if (list == null) {
    122             list = new ArrayList<ResourceFolder>();
    123 
    124             ResourceFolder cf = new ResourceFolder(type, config, folder, mIsFrameworkRepository);
    125             list.add(cf);
    126 
    127             mFolderMap.put(type, list);
    128 
    129             return cf;
    130         }
    131 
    132         // look for an already existing folder configuration.
    133         for (ResourceFolder cFolder : list) {
    134             if (cFolder.mConfiguration.equals(config)) {
    135                 // config already exist. Nothing to be done really, besides making sure
    136                 // the IFolder object is up to date.
    137                 cFolder.mFolder = folder;
    138                 return cFolder;
    139             }
    140         }
    141 
    142         // If we arrive here, this means we didn't find a matching configuration.
    143         // So we add one.
    144         ResourceFolder cf = new ResourceFolder(type, config, folder, mIsFrameworkRepository);
    145         list.add(cf);
    146 
    147         return cf;
    148     }
    149 
    150     /**
    151      * Removes a {@link ResourceFolder} associated with the specified {@link IAbstractFolder}.
    152      * @param type The type of the folder
    153      * @param folder the IFolder object.
    154      * @return the {@link ResourceFolder} that was removed, or null if no matches were found.
    155      */
    156     protected ResourceFolder removeFolder(ResourceFolderType type, IFolder folder) {
    157         // get the list of folders for the resource type.
    158         List<ResourceFolder> list = mFolderMap.get(type);
    159 
    160         if (list != null) {
    161             int count = list.size();
    162             for (int i = 0 ; i < count ; i++) {
    163                 ResourceFolder resFolder = list.get(i);
    164                 // this is only used for Eclipse stuff so we know it's an IFolderWrapper
    165                 IFolderWrapper wrapper = (IFolderWrapper) resFolder.getFolder();
    166                 if (wrapper.getIFolder().equals(folder)) {
    167                     // we found the matching ResourceFolder. we need to remove it.
    168                     list.remove(i);
    169 
    170                     // we now need to invalidate this resource type.
    171                     // The easiest way is to touch one of the other folders of the same type.
    172                     if (list.size() > 0) {
    173                         list.get(0).touch();
    174                     } else {
    175                         // if the list is now empty, and we have a single ResouceType out of this
    176                         // ResourceFolderType, then we are done.
    177                         // However, if another ResourceFolderType can generate similar ResourceType
    178                         // than this, we need to update those ResourceTypes as well.
    179                         // For instance, if the last "drawable-*" folder is deleted, we need to
    180                         // refresh the ResourceItem associated with ResourceType.DRAWABLE.
    181                         // Those can be found in ResourceFolderType.DRAWABLE but also in
    182                         // ResourceFolderType.VALUES.
    183                         // If we don't find a single folder to touch, then it's fine, as the top
    184                         // level items (the list of generated resource types) is not cached
    185                         // (for now)
    186 
    187                         // get the lists of ResourceTypes generated by this ResourceFolderType
    188                         ResourceType[] resTypes = FolderTypeRelationship.getRelatedResourceTypes(
    189                                 type);
    190 
    191                         // for each of those, make sure to find one folder to touch so that the
    192                         // list of ResourceItem associated with the type is rebuilt.
    193                         for (ResourceType resType : resTypes) {
    194                             // get the list of folder that can generate this type
    195                             ResourceFolderType[] folderTypes =
    196                                 FolderTypeRelationship.getRelatedFolders(resType);
    197 
    198                             // we only need to touch one folder in any of those (since it's one
    199                             // folder per type, not per folder type).
    200                             for (ResourceFolderType folderType : folderTypes) {
    201                                 List<ResourceFolder> resFolders = mFolderMap.get(folderType);
    202 
    203                                 if (resFolders != null && resFolders.size() > 0) {
    204                                     resFolders.get(0).touch();
    205                                     break;
    206                                 }
    207                             }
    208                         }
    209                     }
    210 
    211                     // we're done updating/touching, we can stop
    212                     return resFolder;
    213                 }
    214             }
    215         }
    216 
    217         return null;
    218     }
    219 
    220 
    221     /**
    222      * Returns a list of {@link ResourceFolder} for a specific {@link ResourceFolderType}.
    223      * @param type The {@link ResourceFolderType}
    224      */
    225     public List<ResourceFolder> getFolders(ResourceFolderType type) {
    226         return mFolderMap.get(type);
    227     }
    228 
    229     /* (non-Javadoc)
    230      * @see com.android.ide.eclipse.editors.resources.IResourceRepository#getAvailableResourceTypes()
    231      */
    232     public ResourceType[] getAvailableResourceTypes() {
    233         ArrayList<ResourceType> list = new ArrayList<ResourceType>();
    234 
    235         // For each key, we check if there's a single ResourceType match.
    236         // If not, we look for the actual content to give us the resource type.
    237 
    238         for (ResourceFolderType folderType : mFolderMap.keySet()) {
    239             ResourceType types[] = FolderTypeRelationship.getRelatedResourceTypes(folderType);
    240             if (types.length == 1) {
    241                 // before we add it we check if it's not already present, since a ResourceType
    242                 // could be created from multiple folders, even for the folders that only create
    243                 // one type of resource (drawable for instance, can be created from drawable/ and
    244                 // values/)
    245                 if (list.indexOf(types[0]) == -1) {
    246                     list.add(types[0]);
    247                 }
    248             } else {
    249                 // there isn't a single resource type out of this folder, so we look for all
    250                 // content.
    251                 List<ResourceFolder> folders = mFolderMap.get(folderType);
    252                 if (folders != null) {
    253                     for (ResourceFolder folder : folders) {
    254                         Collection<ResourceType> folderContent = folder.getResourceTypes();
    255 
    256                         // then we add them, but only if they aren't already in the list.
    257                         for (ResourceType folderResType : folderContent) {
    258                             if (list.indexOf(folderResType) == -1) {
    259                                 list.add(folderResType);
    260                             }
    261                         }
    262                     }
    263                 }
    264             }
    265         }
    266 
    267         // in case ResourceType.ID haven't been added yet because there's no id defined
    268         // in XML, we check on the list of compiled id resources.
    269         if (list.indexOf(ResourceType.ID) == -1 && mResourceValueMap != null) {
    270             Map<String, Integer> map = mResourceValueMap.get(ResourceType.ID.getName());
    271             if (map != null && map.size() > 0) {
    272                 list.add(ResourceType.ID);
    273             }
    274         }
    275 
    276         // at this point the list is full of ResourceType defined in the files.
    277         // We need to sort it.
    278         Collections.sort(list);
    279 
    280         return list.toArray(new ResourceType[list.size()]);
    281     }
    282 
    283     /* (non-Javadoc)
    284      * @see com.android.ide.eclipse.editors.resources.IResourceRepository#getResources(com.android.ide.eclipse.common.resources.ResourceType)
    285      */
    286     public ProjectResourceItem[] getResources(ResourceType type) {
    287         checkAndUpdate(type);
    288 
    289         if (type == ResourceType.ID) {
    290             synchronized (mIdResourceList) {
    291                 return mIdResourceList.toArray(new ProjectResourceItem[mIdResourceList.size()]);
    292             }
    293         }
    294 
    295         List<ProjectResourceItem> items = mResourceMap.get(type);
    296 
    297         return items.toArray(new ProjectResourceItem[items.size()]);
    298     }
    299 
    300     /* (non-Javadoc)
    301      * @see com.android.ide.eclipse.editors.resources.IResourceRepository#hasResources(com.android.ide.eclipse.common.resources.ResourceType)
    302      */
    303     public boolean hasResources(ResourceType type) {
    304         checkAndUpdate(type);
    305 
    306         if (type == ResourceType.ID) {
    307             synchronized (mIdResourceList) {
    308                 return mIdResourceList.size() > 0;
    309             }
    310         }
    311 
    312         List<ProjectResourceItem> items = mResourceMap.get(type);
    313         return (items != null && items.size() > 0);
    314     }
    315 
    316     /**
    317      * Returns the {@link ResourceFolder} associated with a {@link IFolder}.
    318      * @param folder The {@link IFolder} object.
    319      * @return the {@link ResourceFolder} or null if it was not found.
    320      */
    321     public ResourceFolder getResourceFolder(IFolder folder) {
    322         for (List<ResourceFolder> list : mFolderMap.values()) {
    323             for (ResourceFolder resFolder : list) {
    324                 // this is only used for Eclipse stuff so we know it's an IFolderWrapper
    325                 IFolderWrapper wrapper = (IFolderWrapper) resFolder.getFolder();
    326                 if (wrapper.getIFolder().equals(folder)) {
    327                     return resFolder;
    328                 }
    329             }
    330         }
    331 
    332         return null;
    333     }
    334 
    335     /**
    336      * Returns the {@link ResourceFile} matching the given name, {@link ResourceFolderType} and
    337      * configuration.
    338      * <p/>This only works with files generating one resource named after the file (for instance,
    339      * layouts, bitmap based drawable, xml, anims).
    340      * @return the matching file or <code>null</code> if no match was found.
    341      */
    342     public ResourceFile getMatchingFile(String name, ResourceFolderType type,
    343             FolderConfiguration config) {
    344         // get the folders for the given type
    345         List<ResourceFolder> folders = mFolderMap.get(type);
    346 
    347         // look for folders containing a file with the given name.
    348         ArrayList<ResourceFolder> matchingFolders = new ArrayList<ResourceFolder>();
    349 
    350         // remove the folders that do not have a file with the given name, or if their config
    351         // is incompatible.
    352         for (int i = 0 ; i < folders.size(); i++) {
    353             ResourceFolder folder = folders.get(i);
    354 
    355             if (folder.hasFile(name) == true) {
    356                 matchingFolders.add(folder);
    357             }
    358         }
    359 
    360         // from those, get the folder with a config matching the given reference configuration.
    361         Resource match = findMatchingConfiguredResource(matchingFolders, config);
    362 
    363         // do we have a matching folder?
    364         if (match instanceof ResourceFolder) {
    365             // get the ResourceFile from the filename
    366             return ((ResourceFolder)match).getFile(name);
    367         }
    368 
    369         return null;
    370     }
    371 
    372     /**
    373      * Returns the resources values matching a given {@link FolderConfiguration}.
    374      * @param referenceConfig the configuration that each value must match.
    375      */
    376     public Map<String, Map<String, IResourceValue>> getConfiguredResources(
    377             FolderConfiguration referenceConfig) {
    378 
    379         Map<String, Map<String, IResourceValue>> map =
    380             new HashMap<String, Map<String, IResourceValue>>();
    381 
    382         // if the project contains libraries, we need to add the libraries resources here
    383         // so that they are accessible to the layout rendering.
    384         if (mProject != null) {
    385             ProjectState state = Sdk.getProjectState(mProject);
    386             if (state != null) {
    387                 IProject[] libraries = state.getLibraryProjects();
    388 
    389                 ResourceManager resMgr = ResourceManager.getInstance();
    390 
    391                 // because aapt put all the library in their order in this array, the first
    392                 // one will have priority over the 2nd one. So it's better to loop in the inverse
    393                 // order and fill the map with resources that will be overwritten by higher
    394                 // priority resources
    395                 final int libCount = libraries != null ? libraries.length : 0;
    396                 for (int i = libCount - 1 ; i >= 0 ; i--) {
    397                     IProject library = libraries[i];
    398 
    399                     ProjectResources libRes = resMgr.getProjectResources(library);
    400                     if (libRes != null) {
    401                         // make sure they are loaded
    402                         libRes.loadAll();
    403 
    404                         // we don't want to simply replace the whole map, but instead merge the
    405                         // content of any sub-map
    406                         Map<String, Map<String, IResourceValue>> libMap =
    407                                 libRes.getConfiguredResources(referenceConfig);
    408 
    409                         for (Entry<String, Map<String, IResourceValue>> entry : libMap.entrySet()) {
    410                             // get the map currently in the result map for this resource type
    411                             Map<String, IResourceValue> tempMap = map.get(entry.getKey());
    412                             if (tempMap == null) {
    413                                 // since there's no current map for this type, just add the map
    414                                 // directly coming from the library resources
    415                                 map.put(entry.getKey(), entry.getValue());
    416                             } else {
    417                                 // already a map for this type. add the resources from the
    418                                 // library.
    419                                 tempMap.putAll(entry.getValue());
    420                             }
    421                         }
    422                     }
    423                 }
    424             }
    425         }
    426 
    427         // now the project resources themselves.
    428         // Don't blindly fill the map, instead check if there are sub-map already present
    429         // due to library resources.
    430 
    431         // special case for Id since there's a mix of compiled id (declared inline) and id declared
    432         // in the XML files.
    433         if (mIdResourceList.size() > 0) {
    434             String idType = ResourceType.ID.getName();
    435             Map<String, IResourceValue> idMap = map.get(idType);
    436 
    437             if (idMap == null) {
    438                 idMap = new HashMap<String, IResourceValue>();
    439                 map.put(idType, idMap);
    440             }
    441             for (IdResourceItem id : mIdResourceList) {
    442                 // FIXME: cache the ResourceValue!
    443                 idMap.put(id.getName(), new ResourceValue(idType, id.getName(),
    444                         mIsFrameworkRepository));
    445             }
    446 
    447         }
    448 
    449         Set<ResourceType> keys = mResourceMap.keySet();
    450         for (ResourceType key : keys) {
    451             // we don't process ID resources since we already did it above.
    452             if (key != ResourceType.ID) {
    453                 // get the local results
    454                 Map<String, IResourceValue> localResMap = getConfiguredResource(key,
    455                         referenceConfig);
    456 
    457                 // check if a map for this type already exists
    458                 String resName = key.getName();
    459                 Map<String, IResourceValue> resMap = map.get(resName);
    460                 if (resMap == null) {
    461                     // just use the local results.
    462                     map.put(resName, localResMap);
    463                 } else {
    464                     // add to the library results.
    465                     resMap.putAll(localResMap);
    466                 }
    467             }
    468         }
    469 
    470         return map;
    471     }
    472 
    473     /**
    474      * Loads all the resources. Essentially this forces to load the values from the
    475      * {@link ResourceFile} objects to make sure they are up to date and loaded
    476      * in {@link #mResourceMap}.
    477      */
    478     public void loadAll() {
    479         // gets all the resource types available.
    480         ResourceType[] types = getAvailableResourceTypes();
    481 
    482         // loop on them and load them
    483         for (ResourceType type: types) {
    484             checkAndUpdate(type);
    485         }
    486     }
    487 
    488     /**
    489      * Resolves a compiled resource id into the resource name and type
    490      * @param id
    491      * @return an array of 2 strings { name, type } or null if the id could not be resolved
    492      */
    493     public String[] resolveResourceValue(int id) {
    494         if (mResIdValueToNameMap != null) {
    495             return mResIdValueToNameMap.get(id);
    496         }
    497 
    498         return null;
    499     }
    500 
    501     /**
    502      * Resolves a compiled resource id of type int[] into the resource name.
    503      */
    504     public String resolveResourceValue(int[] id) {
    505         if (mStyleableValueToNameMap != null) {
    506             mWrapper.set(id);
    507             return mStyleableValueToNameMap.get(mWrapper);
    508         }
    509 
    510         return null;
    511     }
    512 
    513     /**
    514      * Returns the value of a resource by its type and name.
    515      * <p/>If the resource is of type {@link ResourceType#ID} and does not exist in the
    516      * internal map, then new id values are dynamically generated (and stored so that queries
    517      * with the same names will return the same value).
    518      */
    519     public Integer getResourceValue(String type, String name) {
    520         if (mResourceValueMap != null) {
    521             Map<String, Integer> map = mResourceValueMap.get(type);
    522             if (map != null) {
    523                 Integer value = map.get(name);
    524 
    525                 // if no value
    526                 if (value == null && ResourceType.ID.getName().equals(type)) {
    527                     return getDynamicId(name);
    528                 }
    529 
    530                 return value;
    531             } else if (ResourceType.ID.getName().equals(type)) {
    532                 return getDynamicId(name);
    533             }
    534         }
    535 
    536         return null;
    537     }
    538 
    539     /**
    540      * Returns the sorted list of languages used in the resources.
    541      */
    542     public SortedSet<String> getLanguages() {
    543         SortedSet<String> set = new TreeSet<String>();
    544 
    545         Collection<List<ResourceFolder>> folderList = mFolderMap.values();
    546         for (List<ResourceFolder> folderSubList : folderList) {
    547             for (ResourceFolder folder : folderSubList) {
    548                 FolderConfiguration config = folder.getConfiguration();
    549                 LanguageQualifier lang = config.getLanguageQualifier();
    550                 if (lang != null) {
    551                     set.add(lang.getStringValue());
    552                 }
    553             }
    554         }
    555 
    556         return set;
    557     }
    558 
    559     /**
    560      * Returns the sorted list of regions used in the resources with the given language.
    561      * @param currentLanguage the current language the region must be associated with.
    562      */
    563     public SortedSet<String> getRegions(String currentLanguage) {
    564         SortedSet<String> set = new TreeSet<String>();
    565 
    566         Collection<List<ResourceFolder>> folderList = mFolderMap.values();
    567         for (List<ResourceFolder> folderSubList : folderList) {
    568             for (ResourceFolder folder : folderSubList) {
    569                 FolderConfiguration config = folder.getConfiguration();
    570 
    571                 // get the language
    572                 LanguageQualifier lang = config.getLanguageQualifier();
    573                 if (lang != null && lang.getStringValue().equals(currentLanguage)) {
    574                     RegionQualifier region = config.getRegionQualifier();
    575                     if (region != null) {
    576                         set.add(region.getStringValue());
    577                     }
    578                 }
    579             }
    580         }
    581 
    582         return set;
    583     }
    584 
    585     /**
    586      * Resets the list of dynamic Ids. This list is used by
    587      * {@link #getResourceValue(String, String)} when the resource query is an ID that doesn't
    588      * exist (for example for ID automatically generated in layout files that are not saved.
    589      * <p/>This method resets those dynamic ID and must be called whenever the actual list of IDs
    590      * change.
    591      */
    592     public void resetDynamicIds() {
    593         synchronized (mDynamicIds) {
    594             mDynamicIds.clear();
    595             mDynamicSeed = DYNAMIC_ID_SEED_START;
    596         }
    597     }
    598 
    599     /**
    600      * Returns a map of (resource name, resource value) for the given {@link ResourceType}.
    601      * <p/>The values returned are taken from the resource files best matching a given
    602      * {@link FolderConfiguration}.
    603      * @param type the type of the resources.
    604      * @param referenceConfig the configuration to best match.
    605      */
    606     private Map<String, IResourceValue> getConfiguredResource(ResourceType type,
    607             FolderConfiguration referenceConfig) {
    608         // get the resource item for the given type
    609         List<ProjectResourceItem> items = mResourceMap.get(type);
    610 
    611         // create the map
    612         HashMap<String, IResourceValue> map = new HashMap<String, IResourceValue>();
    613 
    614         for (ProjectResourceItem item : items) {
    615             // get the source files generating this resource
    616             List<ResourceFile> list = item.getSourceFileList();
    617 
    618             // look for the best match for the given configuration
    619             Resource match = findMatchingConfiguredResource(list, referenceConfig);
    620 
    621             if (match instanceof ResourceFile) {
    622                 ResourceFile matchResFile = (ResourceFile)match;
    623 
    624                 // get the value of this configured resource.
    625                 IResourceValue value = matchResFile.getValue(type, item.getName());
    626 
    627                 if (value != null) {
    628                     map.put(item.getName(), value);
    629                 }
    630             }
    631         }
    632 
    633         return map;
    634     }
    635 
    636     /**
    637      * Returns the best matching {@link Resource}.
    638      * @param resources the list of {@link Resource} to choose from.
    639      * @param referenceConfig the {@link FolderConfiguration} to match.
    640      * @see http://d.android.com/guide/topics/resources/resources-i18n.html#best-match
    641      */
    642     private Resource findMatchingConfiguredResource(List<? extends Resource> resources,
    643             FolderConfiguration referenceConfig) {
    644         //
    645         // 1: eliminate resources that contradict the reference configuration
    646         // 2: pick next qualifier type
    647         // 3: check if any resources use this qualifier, if no, back to 2, else move on to 4.
    648         // 4: eliminate resources that don't use this qualifier.
    649         // 5: if more than one resource left, go back to 2.
    650         //
    651         // The precedence of the qualifiers is more important than the number of qualifiers that
    652         // exactly match the device.
    653 
    654         // 1: eliminate resources that contradict
    655         ArrayList<Resource> matchingResources = new ArrayList<Resource>();
    656         for (int i = 0 ; i < resources.size(); i++) {
    657             Resource res = resources.get(i);
    658 
    659             if (res.getConfiguration().isMatchFor(referenceConfig)) {
    660                 matchingResources.add(res);
    661             }
    662         }
    663 
    664         // if there is only one match, just take it
    665         if (matchingResources.size() == 1) {
    666             return matchingResources.get(0);
    667         } else if (matchingResources.size() == 0) {
    668             return null;
    669         }
    670 
    671         // 2. Loop on the qualifiers, and eliminate matches
    672         final int count = FolderConfiguration.getQualifierCount();
    673         for (int q = 0 ; q < count ; q++) {
    674             // look to see if one resource has this qualifier.
    675             // At the same time also record the best match value for the qualifier (if applicable).
    676 
    677             // The reference value, to find the best match.
    678             // Note that this qualifier could be null. In which case any qualifier found in the
    679             // possible match, will all be considered best match.
    680             ResourceQualifier referenceQualifier = referenceConfig.getQualifier(q);
    681 
    682             boolean found = false;
    683             ResourceQualifier bestMatch = null; // this is to store the best match.
    684             for (Resource res : matchingResources) {
    685                 ResourceQualifier qualifier = res.getConfiguration().getQualifier(q);
    686                 if (qualifier != null) {
    687                     // set the flag.
    688                     found = true;
    689 
    690                     // Now check for a best match. If the reference qualifier is null ,
    691                     // any qualifier is a "best" match (we don't need to record all of them.
    692                     // Instead the non compatible ones are removed below)
    693                     if (referenceQualifier != null) {
    694                         if (qualifier.isBetterMatchThan(bestMatch, referenceQualifier)) {
    695                             bestMatch = qualifier;
    696                         }
    697                     }
    698                 }
    699             }
    700 
    701             // 4. If a resources has a qualifier at the current index, remove all the resources that
    702             // do not have one, or whose qualifier value does not equal the best match found above
    703             // unless there's no reference qualifier, in which case they are all considered
    704             // "best" match.
    705             if (found) {
    706                 for (int i = 0 ; i < matchingResources.size(); ) {
    707                     Resource res = matchingResources.get(i);
    708                     ResourceQualifier qualifier = res.getConfiguration().getQualifier(q);
    709 
    710                     if (qualifier == null) {
    711                         // this resources has no qualifier of this type: rejected.
    712                         matchingResources.remove(res);
    713                     } else if (referenceQualifier != null && bestMatch != null &&
    714                             bestMatch.equals(qualifier) == false) {
    715                         // there's a reference qualifier and there is a better match for it than
    716                         // this resource, so we reject it.
    717                         matchingResources.remove(res);
    718                     } else {
    719                         // looks like we keep this resource, move on to the next one.
    720                         i++;
    721                     }
    722                 }
    723 
    724                 // at this point we may have run out of matching resources before going
    725                 // through all the qualifiers.
    726                 if (matchingResources.size() < 2) {
    727                     break;
    728                 }
    729             }
    730         }
    731 
    732         // Because we accept resources whose configuration have qualifiers where the reference
    733         // configuration doesn't, we can end up with more than one match. In this case, we just
    734         // take the first one.
    735         if (matchingResources.size() == 0) {
    736             return null;
    737         }
    738         return matchingResources.get(0);
    739     }
    740 
    741     /**
    742      * Checks if the list of {@link ResourceItem}s for the specified {@link ResourceType} needs
    743      * to be updated.
    744      * @param type the Resource Type.
    745      */
    746     private void checkAndUpdate(ResourceType type) {
    747         // get the list of folder that can output this type
    748         ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type);
    749 
    750         for (ResourceFolderType folderType : folderTypes) {
    751             List<ResourceFolder> folders = mFolderMap.get(folderType);
    752 
    753             if (folders != null) {
    754                 for (ResourceFolder folder : folders) {
    755                     if (folder.isTouched()) {
    756                         // if this folder is touched we need to update all the types that can
    757                         // be generated from a file in this folder.
    758                         // This will include 'type' obviously.
    759                         ResourceType[] resTypes = FolderTypeRelationship.getRelatedResourceTypes(
    760                                 folderType);
    761                         for (ResourceType resType : resTypes) {
    762                             update(resType);
    763                         }
    764                         return;
    765                     }
    766                 }
    767             }
    768         }
    769     }
    770 
    771     /**
    772      * Updates the list of {@link ResourceItem} objects associated with a {@link ResourceType}.
    773      * This will reset the touch status of all the folders that can generate this resource type.
    774      * @param type the Resource Type.
    775      */
    776     private void update(ResourceType type) {
    777         // get the cache list, and lets make a backup
    778         List<ProjectResourceItem> items = mResourceMap.get(type);
    779         List<ProjectResourceItem> backup = new ArrayList<ProjectResourceItem>();
    780 
    781         if (items == null) {
    782             items = new ArrayList<ProjectResourceItem>();
    783             mResourceMap.put(type, items);
    784         } else {
    785             // backup the list
    786             backup.addAll(items);
    787 
    788             // we reset the list itself.
    789             items.clear();
    790         }
    791 
    792         // get the list of folder that can output this type
    793         ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type);
    794 
    795         for (ResourceFolderType folderType : folderTypes) {
    796             List<ResourceFolder> folders = mFolderMap.get(folderType);
    797 
    798             if (folders != null) {
    799                 for (ResourceFolder folder : folders) {
    800                     items.addAll(folder.getResources(type, this));
    801                     folder.resetTouch();
    802                 }
    803             }
    804         }
    805 
    806         // now items contains the new list. We "merge" it with the backup list.
    807         // Basically, we need to keep the old instances of ResourceItem (where applicable),
    808         // but replace them by the content of the new items.
    809         // This will let the resource explorer keep the expanded state of the nodes whose data
    810         // is a ResourceItem object.
    811         if (backup.size() > 0) {
    812             // this is not going to change as we're only replacing instances.
    813             int count = items.size();
    814 
    815             for (int i = 0 ; i < count;) {
    816                 // get the "new" item
    817                 ProjectResourceItem item = items.get(i);
    818 
    819                 // look for a similar item in the old list.
    820                 ProjectResourceItem foundOldItem = null;
    821                 for (ProjectResourceItem oldItem : backup) {
    822                     if (oldItem.getName().equals(item.getName())) {
    823                         foundOldItem = oldItem;
    824                         break;
    825                     }
    826                 }
    827 
    828                 if (foundOldItem != null) {
    829                     // erase the data of the old item with the data from the new one.
    830                     foundOldItem.replaceWith(item);
    831 
    832                     // remove the old and new item from their respective lists
    833                     items.remove(i);
    834                     backup.remove(foundOldItem);
    835 
    836                     // add the old item to the new list
    837                     items.add(foundOldItem);
    838                 } else {
    839                     // this is a new item, we skip to the next object
    840                     i++;
    841                 }
    842             }
    843         }
    844 
    845         // if this is the ResourceType.ID, we create the actual list, from this list and
    846         // the compiled resource list.
    847         if (type == ResourceType.ID) {
    848             mergeIdResources();
    849         } else {
    850             // else this is the list that will actually be displayed, so we sort it.
    851             Collections.sort(items);
    852         }
    853     }
    854 
    855     private Integer getDynamicId(String name) {
    856         synchronized (mDynamicIds) {
    857             Integer value = mDynamicIds.get(name);
    858             if (value == null) {
    859                 value = new Integer(++mDynamicSeed);
    860                 mDynamicIds.put(name, value);
    861             }
    862 
    863             return value;
    864         }
    865     }
    866 
    867     /**
    868      * Looks up an existing {@link ProjectResourceItem} by {@link ResourceType} and name.
    869      * @param type the Resource Type.
    870      * @param name the Resource name.
    871      * @return the existing ResourceItem or null if no match was found.
    872      */
    873     protected ProjectResourceItem findResourceItem(ResourceType type, String name) {
    874         List<ProjectResourceItem> list = mResourceMap.get(type);
    875 
    876         for (ProjectResourceItem item : list) {
    877             if (name.equals(item.getName())) {
    878                 return item;
    879             }
    880         }
    881 
    882         return null;
    883     }
    884 
    885     /**
    886      * Sets compiled resource information.
    887      * @param resIdValueToNameMap a map of compiled resource id to resource name.
    888      *  The map is acquired by the {@link ProjectResources} object.
    889      * @param styleableValueMap
    890      * @param resourceValueMap a map of (name, id) for resources of type {@link ResourceType#ID}.
    891      * The list is acquired by the {@link ProjectResources} object.
    892      */
    893     void setCompiledResources(Map<Integer, String[]> resIdValueToNameMap,
    894             Map<IntArrayWrapper, String> styleableValueMap,
    895             Map<String, Map<String, Integer>> resourceValueMap) {
    896         mResourceValueMap = resourceValueMap;
    897         mResIdValueToNameMap = resIdValueToNameMap;
    898         mStyleableValueToNameMap = styleableValueMap;
    899         mergeIdResources();
    900     }
    901 
    902     /**
    903      * Merges the list of ID resource coming from R.java and the list of ID resources
    904      * coming from XML declaration into the cached list {@link #mIdResourceList}.
    905      */
    906     void mergeIdResources() {
    907         // get the list of IDs coming from XML declaration. Those ids are present in
    908         // mCompiledIdResources already, so we'll need to use those instead of creating
    909         // new IdResourceItem
    910         List<ProjectResourceItem> xmlIdResources = mResourceMap.get(ResourceType.ID);
    911 
    912         synchronized (mIdResourceList) {
    913             // copy the currently cached items.
    914             ArrayList<IdResourceItem> oldItems = new ArrayList<IdResourceItem>();
    915             oldItems.addAll(mIdResourceList);
    916 
    917             // empty the current list
    918             mIdResourceList.clear();
    919 
    920             // get the list of compile id resources.
    921             Map<String, Integer> idMap = null;
    922             if (mResourceValueMap != null) {
    923                 idMap = mResourceValueMap.get(ResourceType.ID.getName());
    924             }
    925 
    926             if (idMap == null) {
    927                 if (xmlIdResources != null) {
    928                     for (ProjectResourceItem resourceItem : xmlIdResources) {
    929                         // check the actual class just for safety.
    930                         if (resourceItem instanceof IdResourceItem) {
    931                             mIdResourceList.add((IdResourceItem)resourceItem);
    932                         }
    933                     }
    934                 }
    935             } else {
    936                 // loop on the full list of id, and look for a match in the old list,
    937                 // in the list coming from XML (in case a new XML item was created.)
    938 
    939                 Set<String> idSet = idMap.keySet();
    940 
    941                 idLoop: for (String idResource : idSet) {
    942                     // first look in the XML list in case an id went from inline to XML declared.
    943                     if (xmlIdResources != null) {
    944                         for (ProjectResourceItem resourceItem : xmlIdResources) {
    945                             if (resourceItem instanceof IdResourceItem &&
    946                                     resourceItem.getName().equals(idResource)) {
    947                                 mIdResourceList.add((IdResourceItem)resourceItem);
    948                                 continue idLoop;
    949                             }
    950                         }
    951                     }
    952 
    953                     // if we haven't found it, look in the old items.
    954                     int count = oldItems.size();
    955                     for (int i = 0 ; i < count ; i++) {
    956                         IdResourceItem resourceItem = oldItems.get(i);
    957                         if (resourceItem.getName().equals(idResource)) {
    958                             oldItems.remove(i);
    959                             mIdResourceList.add(resourceItem);
    960                             continue idLoop;
    961                         }
    962                     }
    963 
    964                     // if we haven't found it, it looks like it's a new id that was
    965                     // declared inline.
    966                     mIdResourceList.add(new IdResourceItem(idResource,
    967                             true /* isDeclaredInline */));
    968                 }
    969             }
    970 
    971             // now we sort the list
    972             Collections.sort(mIdResourceList);
    973         }
    974     }
    975 }
    976