• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.resources.IResourceRepository;
20 import com.android.ide.eclipse.adt.internal.resources.ResourceItem;
21 import com.android.ide.eclipse.adt.internal.resources.ResourceType;
22 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
23 import com.android.ide.eclipse.adt.internal.resources.configurations.LanguageQualifier;
24 import com.android.ide.eclipse.adt.internal.resources.configurations.RegionQualifier;
25 import com.android.ide.eclipse.adt.internal.resources.configurations.ResourceQualifier;
26 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
27 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
28 import com.android.ide.eclipse.adt.io.IFolderWrapper;
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      */
ProjectResources(IProject project)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      */
ProjectResources()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      */
isSystemRepository()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      */
add(ResourceFolderType type, FolderConfiguration config, IAbstractFolder folder)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      */
removeFolder(ResourceFolderType type, IFolder folder)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      */
getFolders(ResourceFolderType type)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      */
getAvailableResourceTypes()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      */
getResources(ResourceType type)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      */
hasResources(ResourceType type)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      */
getResourceFolder(IFolder folder)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      */
getMatchingFile(String name, ResourceFolderType type, FolderConfiguration config)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      */
getConfiguredResources( FolderConfiguration referenceConfig)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.getFullLibraryProjects();
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                 for (int i = libraries.length - 1 ; i >= 0 ; i--) {
396                     IProject library = libraries[i];
397 
398                     ProjectResources libRes = resMgr.getProjectResources(library);
399                     if (libRes != null) {
400                         // make sure they are loaded
401                         libRes.loadAll();
402 
403                         // we don't want to simply replace the whole map, but instead merge the
404                         // content of any sub-map
405                         Map<String, Map<String, IResourceValue>> libMap =
406                                 libRes.getConfiguredResources(referenceConfig);
407 
408                         for (Entry<String, Map<String, IResourceValue>> entry : libMap.entrySet()) {
409                             // get the map currently in the result map for this resource type
410                             Map<String, IResourceValue> tempMap = map.get(entry.getKey());
411                             if (tempMap == null) {
412                                 // since there's no current map for this type, just add the map
413                                 // directly coming from the library resources
414                                 map.put(entry.getKey(), entry.getValue());
415                             } else {
416                                 // already a map for this type. add the resources from the
417                                 // library.
418                                 tempMap.putAll(entry.getValue());
419                             }
420                         }
421                     }
422                 }
423             }
424         }
425 
426         // now the project resources themselves.
427         // Don't blindly fill the map, instead check if there are sub-map already present
428         // due to library resources.
429 
430         // special case for Id since there's a mix of compiled id (declared inline) and id declared
431         // in the XML files.
432         if (mIdResourceList.size() > 0) {
433             String idType = ResourceType.ID.getName();
434             Map<String, IResourceValue> idMap = map.get(idType);
435 
436             if (idMap == null) {
437                 idMap = new HashMap<String, IResourceValue>();
438                 map.put(idType, idMap);
439             }
440             for (IdResourceItem id : mIdResourceList) {
441                 // FIXME: cache the ResourceValue!
442                 idMap.put(id.getName(), new ResourceValue(idType, id.getName(),
443                         mIsFrameworkRepository));
444             }
445 
446         }
447 
448         Set<ResourceType> keys = mResourceMap.keySet();
449         for (ResourceType key : keys) {
450             // we don't process ID resources since we already did it above.
451             if (key != ResourceType.ID) {
452                 // get the local results
453                 Map<String, IResourceValue> localResMap = getConfiguredResource(key,
454                         referenceConfig);
455 
456                 // check if a map for this type already exists
457                 String resName = key.getName();
458                 Map<String, IResourceValue> resMap = map.get(resName);
459                 if (resMap == null) {
460                     // just use the local results.
461                     map.put(resName, localResMap);
462                 } else {
463                     // add to the library results.
464                     resMap.putAll(localResMap);
465                 }
466             }
467         }
468 
469         return map;
470     }
471 
472     /**
473      * Loads all the resources. Essentially this forces to load the values from the
474      * {@link ResourceFile} objects to make sure they are up to date and loaded
475      * in {@link #mResourceMap}.
476      */
loadAll()477     public void loadAll() {
478         // gets all the resource types available.
479         ResourceType[] types = getAvailableResourceTypes();
480 
481         // loop on them and load them
482         for (ResourceType type: types) {
483             checkAndUpdate(type);
484         }
485     }
486 
487     /**
488      * Resolves a compiled resource id into the resource name and type
489      * @param id
490      * @return an array of 2 strings { name, type } or null if the id could not be resolved
491      */
resolveResourceValue(int id)492     public String[] resolveResourceValue(int id) {
493         if (mResIdValueToNameMap != null) {
494             return mResIdValueToNameMap.get(id);
495         }
496 
497         return null;
498     }
499 
500     /**
501      * Resolves a compiled resource id of type int[] into the resource name.
502      */
resolveResourceValue(int[] id)503     public String resolveResourceValue(int[] id) {
504         if (mStyleableValueToNameMap != null) {
505             mWrapper.set(id);
506             return mStyleableValueToNameMap.get(mWrapper);
507         }
508 
509         return null;
510     }
511 
512     /**
513      * Returns the value of a resource by its type and name.
514      * <p/>If the resource is of type {@link ResourceType#ID} and does not exist in the
515      * internal map, then new id values are dynamically generated (and stored so that queries
516      * with the same names will return the same value).
517      */
getResourceValue(String type, String name)518     public Integer getResourceValue(String type, String name) {
519         if (mResourceValueMap != null) {
520             Map<String, Integer> map = mResourceValueMap.get(type);
521             if (map != null) {
522                 Integer value = map.get(name);
523 
524                 // if no value
525                 if (value == null && ResourceType.ID.getName().equals(type)) {
526                     return getDynamicId(name);
527                 }
528 
529                 return value;
530             } else if (ResourceType.ID.getName().equals(type)) {
531                 return getDynamicId(name);
532             }
533         }
534 
535         return null;
536     }
537 
538     /**
539      * Returns the sorted list of languages used in the resources.
540      */
getLanguages()541     public SortedSet<String> getLanguages() {
542         SortedSet<String> set = new TreeSet<String>();
543 
544         Collection<List<ResourceFolder>> folderList = mFolderMap.values();
545         for (List<ResourceFolder> folderSubList : folderList) {
546             for (ResourceFolder folder : folderSubList) {
547                 FolderConfiguration config = folder.getConfiguration();
548                 LanguageQualifier lang = config.getLanguageQualifier();
549                 if (lang != null) {
550                     set.add(lang.getShortDisplayValue());
551                 }
552             }
553         }
554 
555         return set;
556     }
557 
558     /**
559      * Returns the sorted list of regions used in the resources with the given language.
560      * @param currentLanguage the current language the region must be associated with.
561      */
getRegions(String currentLanguage)562     public SortedSet<String> getRegions(String currentLanguage) {
563         SortedSet<String> set = new TreeSet<String>();
564 
565         Collection<List<ResourceFolder>> folderList = mFolderMap.values();
566         for (List<ResourceFolder> folderSubList : folderList) {
567             for (ResourceFolder folder : folderSubList) {
568                 FolderConfiguration config = folder.getConfiguration();
569 
570                 // get the language
571                 LanguageQualifier lang = config.getLanguageQualifier();
572                 if (lang != null && lang.getShortDisplayValue().equals(currentLanguage)) {
573                     RegionQualifier region = config.getRegionQualifier();
574                     if (region != null) {
575                         set.add(region.getShortDisplayValue());
576                     }
577                 }
578             }
579         }
580 
581         return set;
582     }
583 
584     /**
585      * Resets the list of dynamic Ids. This list is used by
586      * {@link #getResourceValue(String, String)} when the resource query is an ID that doesn't
587      * exist (for example for ID automatically generated in layout files that are not saved.
588      * <p/>This method resets those dynamic ID and must be called whenever the actual list of IDs
589      * change.
590      */
resetDynamicIds()591     public void resetDynamicIds() {
592         synchronized (mDynamicIds) {
593             mDynamicIds.clear();
594             mDynamicSeed = DYNAMIC_ID_SEED_START;
595         }
596     }
597 
598     /**
599      * Returns a map of (resource name, resource value) for the given {@link ResourceType}.
600      * <p/>The values returned are taken from the resource files best matching a given
601      * {@link FolderConfiguration}.
602      * @param type the type of the resources.
603      * @param referenceConfig the configuration to best match.
604      */
getConfiguredResource(ResourceType type, FolderConfiguration referenceConfig)605     private Map<String, IResourceValue> getConfiguredResource(ResourceType type,
606             FolderConfiguration referenceConfig) {
607         // get the resource item for the given type
608         List<ProjectResourceItem> items = mResourceMap.get(type);
609 
610         // create the map
611         HashMap<String, IResourceValue> map = new HashMap<String, IResourceValue>();
612 
613         for (ProjectResourceItem item : items) {
614             // get the source files generating this resource
615             List<ResourceFile> list = item.getSourceFileList();
616 
617             // look for the best match for the given configuration
618             Resource match = findMatchingConfiguredResource(list, referenceConfig);
619 
620             if (match instanceof ResourceFile) {
621                 ResourceFile matchResFile = (ResourceFile)match;
622 
623                 // get the value of this configured resource.
624                 IResourceValue value = matchResFile.getValue(type, item.getName());
625 
626                 if (value != null) {
627                     map.put(item.getName(), value);
628                 }
629             }
630         }
631 
632         return map;
633     }
634 
635     /**
636      * Returns the best matching {@link Resource}.
637      * @param resources the list of {@link Resource} to choose from.
638      * @param referenceConfig the {@link FolderConfiguration} to match.
639      * @see http://d.android.com/guide/topics/resources/resources-i18n.html#best-match
640      */
findMatchingConfiguredResource(List<? extends Resource> resources, FolderConfiguration referenceConfig)641     private Resource findMatchingConfiguredResource(List<? extends Resource> resources,
642             FolderConfiguration referenceConfig) {
643         //
644         // 1: eliminate resources that contradict the reference configuration
645         // 2: pick next qualifier type
646         // 3: check if any resources use this qualifier, if no, back to 2, else move on to 4.
647         // 4: eliminate resources that don't use this qualifier.
648         // 5: if more than one resource left, go back to 2.
649         //
650         // The precedence of the qualifiers is more important than the number of qualifiers that
651         // exactly match the device.
652 
653         // 1: eliminate resources that contradict
654         ArrayList<Resource> matchingResources = new ArrayList<Resource>();
655         for (int i = 0 ; i < resources.size(); i++) {
656             Resource res = resources.get(i);
657 
658             if (res.getConfiguration().isMatchFor(referenceConfig)) {
659                 matchingResources.add(res);
660             }
661         }
662 
663         // if there is only one match, just take it
664         if (matchingResources.size() == 1) {
665             return matchingResources.get(0);
666         } else if (matchingResources.size() == 0) {
667             return null;
668         }
669 
670         // 2. Loop on the qualifiers, and eliminate matches
671         final int count = FolderConfiguration.getQualifierCount();
672         for (int q = 0 ; q < count ; q++) {
673             // look to see if one resource has this qualifier.
674             // At the same time also record the best match value for the qualifier (if applicable).
675 
676             // The reference value, to find the best match.
677             // Note that this qualifier could be null. In which case any qualifier found in the
678             // possible match, will all be considered best match.
679             ResourceQualifier referenceQualifier = referenceConfig.getQualifier(q);
680 
681             boolean found = false;
682             ResourceQualifier bestMatch = null; // this is to store the best match.
683             for (Resource res : matchingResources) {
684                 ResourceQualifier qualifier = res.getConfiguration().getQualifier(q);
685                 if (qualifier != null) {
686                     // set the flag.
687                     found = true;
688 
689                     // Now check for a best match. If the reference qualifier is null ,
690                     // any qualifier is a "best" match (we don't need to record all of them.
691                     // Instead the non compatible ones are removed below)
692                     if (referenceQualifier != null) {
693                         if (qualifier.isBetterMatchThan(bestMatch, referenceQualifier)) {
694                             bestMatch = qualifier;
695                         }
696                     }
697                 }
698             }
699 
700             // 4. If a resources has a qualifier at the current index, remove all the resources that
701             // do not have one, or whose qualifier value does not equal the best match found above
702             // unless there's no reference qualifier, in which case they are all considered
703             // "best" match.
704             if (found) {
705                 for (int i = 0 ; i < matchingResources.size(); ) {
706                     Resource res = matchingResources.get(i);
707                     ResourceQualifier qualifier = res.getConfiguration().getQualifier(q);
708 
709                     if (qualifier == null) {
710                         // this resources has no qualifier of this type: rejected.
711                         matchingResources.remove(res);
712                     } else if (referenceQualifier != null && bestMatch != null &&
713                             bestMatch.equals(qualifier) == false) {
714                         // there's a reference qualifier and there is a better match for it than
715                         // this resource, so we reject it.
716                         matchingResources.remove(res);
717                     } else {
718                         // looks like we keep this resource, move on to the next one.
719                         i++;
720                     }
721                 }
722 
723                 // at this point we may have run out of matching resources before going
724                 // through all the qualifiers.
725                 if (matchingResources.size() < 2) {
726                     break;
727                 }
728             }
729         }
730 
731         // Because we accept resources whose configuration have qualifiers where the reference
732         // configuration doesn't, we can end up with more than one match. In this case, we just
733         // take the first one.
734         if (matchingResources.size() == 0) {
735             return null;
736         }
737         return matchingResources.get(0);
738     }
739 
740     /**
741      * Checks if the list of {@link ResourceItem}s for the specified {@link ResourceType} needs
742      * to be updated.
743      * @param type the Resource Type.
744      */
checkAndUpdate(ResourceType type)745     private void checkAndUpdate(ResourceType type) {
746         // get the list of folder that can output this type
747         ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type);
748 
749         for (ResourceFolderType folderType : folderTypes) {
750             List<ResourceFolder> folders = mFolderMap.get(folderType);
751 
752             if (folders != null) {
753                 for (ResourceFolder folder : folders) {
754                     if (folder.isTouched()) {
755                         // if this folder is touched we need to update all the types that can
756                         // be generated from a file in this folder.
757                         // This will include 'type' obviously.
758                         ResourceType[] resTypes = FolderTypeRelationship.getRelatedResourceTypes(
759                                 folderType);
760                         for (ResourceType resType : resTypes) {
761                             update(resType);
762                         }
763                         return;
764                     }
765                 }
766             }
767         }
768     }
769 
770     /**
771      * Updates the list of {@link ResourceItem} objects associated with a {@link ResourceType}.
772      * This will reset the touch status of all the folders that can generate this resource type.
773      * @param type the Resource Type.
774      */
update(ResourceType type)775     private void update(ResourceType type) {
776         // get the cache list, and lets make a backup
777         List<ProjectResourceItem> items = mResourceMap.get(type);
778         List<ProjectResourceItem> backup = new ArrayList<ProjectResourceItem>();
779 
780         if (items == null) {
781             items = new ArrayList<ProjectResourceItem>();
782             mResourceMap.put(type, items);
783         } else {
784             // backup the list
785             backup.addAll(items);
786 
787             // we reset the list itself.
788             items.clear();
789         }
790 
791         // get the list of folder that can output this type
792         ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type);
793 
794         for (ResourceFolderType folderType : folderTypes) {
795             List<ResourceFolder> folders = mFolderMap.get(folderType);
796 
797             if (folders != null) {
798                 for (ResourceFolder folder : folders) {
799                     items.addAll(folder.getResources(type, this));
800                     folder.resetTouch();
801                 }
802             }
803         }
804 
805         // now items contains the new list. We "merge" it with the backup list.
806         // Basically, we need to keep the old instances of ResourceItem (where applicable),
807         // but replace them by the content of the new items.
808         // This will let the resource explorer keep the expanded state of the nodes whose data
809         // is a ResourceItem object.
810         if (backup.size() > 0) {
811             // this is not going to change as we're only replacing instances.
812             int count = items.size();
813 
814             for (int i = 0 ; i < count;) {
815                 // get the "new" item
816                 ProjectResourceItem item = items.get(i);
817 
818                 // look for a similar item in the old list.
819                 ProjectResourceItem foundOldItem = null;
820                 for (ProjectResourceItem oldItem : backup) {
821                     if (oldItem.getName().equals(item.getName())) {
822                         foundOldItem = oldItem;
823                         break;
824                     }
825                 }
826 
827                 if (foundOldItem != null) {
828                     // erase the data of the old item with the data from the new one.
829                     foundOldItem.replaceWith(item);
830 
831                     // remove the old and new item from their respective lists
832                     items.remove(i);
833                     backup.remove(foundOldItem);
834 
835                     // add the old item to the new list
836                     items.add(foundOldItem);
837                 } else {
838                     // this is a new item, we skip to the next object
839                     i++;
840                 }
841             }
842         }
843 
844         // if this is the ResourceType.ID, we create the actual list, from this list and
845         // the compiled resource list.
846         if (type == ResourceType.ID) {
847             mergeIdResources();
848         } else {
849             // else this is the list that will actually be displayed, so we sort it.
850             Collections.sort(items);
851         }
852     }
853 
getDynamicId(String name)854     private Integer getDynamicId(String name) {
855         synchronized (mDynamicIds) {
856             Integer value = mDynamicIds.get(name);
857             if (value == null) {
858                 value = new Integer(++mDynamicSeed);
859                 mDynamicIds.put(name, value);
860             }
861 
862             return value;
863         }
864     }
865 
866     /**
867      * Looks up an existing {@link ProjectResourceItem} by {@link ResourceType} and name.
868      * @param type the Resource Type.
869      * @param name the Resource name.
870      * @return the existing ResourceItem or null if no match was found.
871      */
findResourceItem(ResourceType type, String name)872     protected ProjectResourceItem findResourceItem(ResourceType type, String name) {
873         List<ProjectResourceItem> list = mResourceMap.get(type);
874 
875         for (ProjectResourceItem item : list) {
876             if (name.equals(item.getName())) {
877                 return item;
878             }
879         }
880 
881         return null;
882     }
883 
884     /**
885      * Sets compiled resource information.
886      * @param resIdValueToNameMap a map of compiled resource id to resource name.
887      *  The map is acquired by the {@link ProjectResources} object.
888      * @param styleableValueMap
889      * @param resourceValueMap a map of (name, id) for resources of type {@link ResourceType#ID}.
890      * The list is acquired by the {@link ProjectResources} object.
891      */
setCompiledResources(Map<Integer, String[]> resIdValueToNameMap, Map<IntArrayWrapper, String> styleableValueMap, Map<String, Map<String, Integer>> resourceValueMap)892     void setCompiledResources(Map<Integer, String[]> resIdValueToNameMap,
893             Map<IntArrayWrapper, String> styleableValueMap,
894             Map<String, Map<String, Integer>> resourceValueMap) {
895         mResourceValueMap = resourceValueMap;
896         mResIdValueToNameMap = resIdValueToNameMap;
897         mStyleableValueToNameMap = styleableValueMap;
898         mergeIdResources();
899     }
900 
901     /**
902      * Merges the list of ID resource coming from R.java and the list of ID resources
903      * coming from XML declaration into the cached list {@link #mIdResourceList}.
904      */
mergeIdResources()905     void mergeIdResources() {
906         // get the list of IDs coming from XML declaration. Those ids are present in
907         // mCompiledIdResources already, so we'll need to use those instead of creating
908         // new IdResourceItem
909         List<ProjectResourceItem> xmlIdResources = mResourceMap.get(ResourceType.ID);
910 
911         synchronized (mIdResourceList) {
912             // copy the currently cached items.
913             ArrayList<IdResourceItem> oldItems = new ArrayList<IdResourceItem>();
914             oldItems.addAll(mIdResourceList);
915 
916             // empty the current list
917             mIdResourceList.clear();
918 
919             // get the list of compile id resources.
920             Map<String, Integer> idMap = null;
921             if (mResourceValueMap != null) {
922                 idMap = mResourceValueMap.get(ResourceType.ID.getName());
923             }
924 
925             if (idMap == null) {
926                 if (xmlIdResources != null) {
927                     for (ProjectResourceItem resourceItem : xmlIdResources) {
928                         // check the actual class just for safety.
929                         if (resourceItem instanceof IdResourceItem) {
930                             mIdResourceList.add((IdResourceItem)resourceItem);
931                         }
932                     }
933                 }
934             } else {
935                 // loop on the full list of id, and look for a match in the old list,
936                 // in the list coming from XML (in case a new XML item was created.)
937 
938                 Set<String> idSet = idMap.keySet();
939 
940                 idLoop: for (String idResource : idSet) {
941                     // first look in the XML list in case an id went from inline to XML declared.
942                     if (xmlIdResources != null) {
943                         for (ProjectResourceItem resourceItem : xmlIdResources) {
944                             if (resourceItem instanceof IdResourceItem &&
945                                     resourceItem.getName().equals(idResource)) {
946                                 mIdResourceList.add((IdResourceItem)resourceItem);
947                                 continue idLoop;
948                             }
949                         }
950                     }
951 
952                     // if we haven't found it, look in the old items.
953                     int count = oldItems.size();
954                     for (int i = 0 ; i < count ; i++) {
955                         IdResourceItem resourceItem = oldItems.get(i);
956                         if (resourceItem.getName().equals(idResource)) {
957                             oldItems.remove(i);
958                             mIdResourceList.add(resourceItem);
959                             continue idLoop;
960                         }
961                     }
962 
963                     // if we haven't found it, it looks like it's a new id that was
964                     // declared inline.
965                     mIdResourceList.add(new IdResourceItem(idResource,
966                             true /* isDeclaredInline */));
967                 }
968             }
969 
970             // now we sort the list
971             Collections.sort(mIdResourceList);
972         }
973     }
974 }
975