• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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 package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
17 
18 import com.android.annotations.NonNull;
19 import com.android.annotations.Nullable;
20 import com.android.ide.common.resources.ResourceFile;
21 import com.android.ide.common.resources.configuration.DensityQualifier;
22 import com.android.ide.common.resources.configuration.DeviceConfigHelper;
23 import com.android.ide.common.resources.configuration.FolderConfiguration;
24 import com.android.ide.common.resources.configuration.LanguageQualifier;
25 import com.android.ide.common.resources.configuration.NightModeQualifier;
26 import com.android.ide.common.resources.configuration.RegionQualifier;
27 import com.android.ide.common.resources.configuration.ResourceQualifier;
28 import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
29 import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
30 import com.android.ide.common.resources.configuration.UiModeQualifier;
31 import com.android.ide.common.resources.configuration.VersionQualifier;
32 import com.android.ide.eclipse.adt.AdtPlugin;
33 import com.android.ide.eclipse.adt.AdtUtils;
34 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
35 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
36 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
37 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
38 import com.android.ide.eclipse.adt.io.IFileWrapper;
39 import com.android.resources.Density;
40 import com.android.resources.NightMode;
41 import com.android.resources.ResourceType;
42 import com.android.resources.ScreenOrientation;
43 import com.android.resources.ScreenSize;
44 import com.android.resources.UiMode;
45 import com.android.sdklib.IAndroidTarget;
46 import com.android.sdklib.devices.Device;
47 import com.android.sdklib.devices.State;
48 import com.android.sdklib.repository.PkgProps;
49 import com.android.sdklib.util.SparseIntArray;
50 import com.android.utils.Pair;
51 
52 import org.eclipse.core.resources.IFile;
53 import org.eclipse.core.resources.IProject;
54 import org.eclipse.core.runtime.IStatus;
55 import org.eclipse.ui.IEditorPart;
56 
57 import java.util.ArrayList;
58 import java.util.Collections;
59 import java.util.Comparator;
60 import java.util.List;
61 
62 /**
63  * Produces matches for configurations
64  * <p>
65  * See algorithm described here:
66  * http://developer.android.com/guide/topics/resources/providing-resources.html
67  */
68 public class ConfigurationMatcher {
69     private static final boolean PREFER_RECENT_RENDER_TARGETS = true;
70 
71     private final ConfigurationChooser mConfigChooser;
72     private final Configuration mConfiguration;
73     private final IFile mEditedFile;
74     private final ProjectResources mResources;
75     private final boolean mUpdateUi;
76 
ConfigurationMatcher(ConfigurationChooser chooser)77     ConfigurationMatcher(ConfigurationChooser chooser) {
78         this(chooser, chooser.getConfiguration(), chooser.getEditedFile(),
79                 chooser.getResources(), true);
80     }
81 
ConfigurationMatcher( @onNull ConfigurationChooser chooser, @NonNull Configuration configuration, @Nullable IFile editedFile, @Nullable ProjectResources resources, boolean updateUi)82     ConfigurationMatcher(
83             @NonNull ConfigurationChooser chooser,
84             @NonNull Configuration configuration,
85             @Nullable IFile editedFile,
86             @Nullable ProjectResources resources,
87             boolean updateUi) {
88         mConfigChooser = chooser;
89         mConfiguration = configuration;
90         mEditedFile = editedFile;
91         mResources = resources;
92         mUpdateUi = updateUi;
93     }
94 
95     // ---- Finding matching configurations ----
96 
97     private static class ConfigBundle {
98         private final FolderConfiguration config;
99         private int localeIndex;
100         private int dockModeIndex;
101         private int nightModeIndex;
102 
ConfigBundle()103         private ConfigBundle() {
104             config = new FolderConfiguration();
105         }
106 
ConfigBundle(ConfigBundle bundle)107         private ConfigBundle(ConfigBundle bundle) {
108             config = new FolderConfiguration();
109             config.set(bundle.config);
110             localeIndex = bundle.localeIndex;
111             dockModeIndex = bundle.dockModeIndex;
112             nightModeIndex = bundle.nightModeIndex;
113         }
114     }
115 
116     private static class ConfigMatch {
117         final FolderConfiguration testConfig;
118         final Device device;
119         final State state;
120         final ConfigBundle bundle;
121 
ConfigMatch(@onNull FolderConfiguration testConfig, @NonNull Device device, @NonNull State state, @NonNull ConfigBundle bundle)122         public ConfigMatch(@NonNull FolderConfiguration testConfig, @NonNull Device device,
123                 @NonNull State state, @NonNull ConfigBundle bundle) {
124             this.testConfig = testConfig;
125             this.device = device;
126             this.state = state;
127             this.bundle = bundle;
128         }
129 
130         @Override
toString()131         public String toString() {
132             return device.getName() + " - " + state.getName();
133         }
134     }
135 
136     /**
137      * Checks whether the current edited file is the best match for a given config.
138      * <p>
139      * This tests against other versions of the same layout in the project.
140      * <p>
141      * The given config must be compatible with the current edited file.
142      * @param config the config to test.
143      * @return true if the current edited file is the best match in the project for the
144      * given config.
145      */
isCurrentFileBestMatchFor(FolderConfiguration config)146     public boolean isCurrentFileBestMatchFor(FolderConfiguration config) {
147         ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(),
148                 ResourceType.LAYOUT, config);
149 
150         if (match != null) {
151             return match.getFile().equals(mEditedFile);
152         } else {
153             // if we stop here that means the current file is not even a match!
154             AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config.");
155         }
156 
157         return false;
158     }
159 
160     /**
161      * Adapts the current device/config selection so that it's compatible with
162      * the configuration.
163      * <p>
164      * If the current selection is compatible, nothing is changed.
165      * <p>
166      * If it's not compatible, configs from the current devices are tested.
167      * <p>
168      * If none are compatible, it reverts to
169      * {@link #findAndSetCompatibleConfig(boolean)}
170      */
adaptConfigSelection(boolean needBestMatch)171     void adaptConfigSelection(boolean needBestMatch) {
172         // check the device config (ie sans locale)
173         boolean needConfigChange = true; // if still true, we need to find another config.
174         boolean currentConfigIsCompatible = false;
175         State selectedState = mConfiguration.getDeviceState();
176         FolderConfiguration editedConfig = mConfiguration.getEditedConfig();
177         if (selectedState != null) {
178             FolderConfiguration currentConfig = DeviceConfigHelper.getFolderConfig(selectedState);
179             if (currentConfig != null && editedConfig.isMatchFor(currentConfig)) {
180                 currentConfigIsCompatible = true; // current config is compatible
181                 if (!needBestMatch || isCurrentFileBestMatchFor(currentConfig)) {
182                     needConfigChange = false;
183                 }
184             }
185         }
186 
187         if (needConfigChange) {
188             List<Locale> localeList = mConfigChooser.getLocaleList();
189 
190             // if the current state/locale isn't a correct match, then
191             // look for another state/locale in the same device.
192             FolderConfiguration testConfig = new FolderConfiguration();
193 
194             // first look in the current device.
195             State matchState = null;
196             int localeIndex = -1;
197             Device device = mConfiguration.getDevice();
198             if (device != null) {
199                 mainloop: for (State state : device.getAllStates()) {
200                     testConfig.set(DeviceConfigHelper.getFolderConfig(state));
201 
202                     // loop on the locales.
203                     for (int i = 0 ; i < localeList.size() ; i++) {
204                         Locale locale = localeList.get(i);
205 
206                         // update the test config with the locale qualifiers
207                         testConfig.setLanguageQualifier(locale.language);
208                         testConfig.setRegionQualifier(locale.region);
209 
210                         if (editedConfig.isMatchFor(testConfig) &&
211                                 isCurrentFileBestMatchFor(testConfig)) {
212                             matchState = state;
213                             localeIndex = i;
214                             break mainloop;
215                         }
216                     }
217                 }
218             }
219 
220             if (matchState != null) {
221                 mConfiguration.setDeviceState(matchState, true);
222                 Locale locale = localeList.get(localeIndex);
223                 mConfiguration.setLocale(locale, true);
224                 if (mUpdateUi) {
225                     mConfigChooser.selectDeviceState(matchState);
226                     mConfigChooser.selectLocale(locale);
227                 }
228                 mConfiguration.syncFolderConfig();
229             } else {
230                 // no match in current device with any state/locale
231                 // attempt to find another device that can display this
232                 // particular state.
233                 findAndSetCompatibleConfig(currentConfigIsCompatible);
234             }
235         }
236     }
237 
238     /**
239      * Finds a device/config that can display a configuration.
240      * <p>
241      * Once found the device and config combos are set to the config.
242      * <p>
243      * If there is no compatible configuration, a custom one is created.
244      *
245      * @param favorCurrentConfig if true, and no best match is found, don't
246      *            change the current config. This must only be true if the
247      *            current config is compatible.
248      */
findAndSetCompatibleConfig(boolean favorCurrentConfig)249     void findAndSetCompatibleConfig(boolean favorCurrentConfig) {
250         List<Locale> localeList = mConfigChooser.getLocaleList();
251         List<Device> deviceList = mConfigChooser.getDeviceList();
252         FolderConfiguration editedConfig = mConfiguration.getEditedConfig();
253         FolderConfiguration currentConfig = mConfiguration.getFullConfig();
254 
255         // list of compatible device/state/locale
256         List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>();
257 
258         // list of actual best match (ie the file is a best match for the
259         // device/state)
260         List<ConfigMatch> bestMatches = new ArrayList<ConfigMatch>();
261 
262         // get a locale that match the host locale roughly (may not be exact match on the region.)
263         int localeHostMatch = getLocaleMatch();
264 
265         // build a list of combinations of non standard qualifiers to add to each device's
266         // qualifier set when testing for a match.
267         // These qualifiers are: locale, night-mode, car dock.
268         List<ConfigBundle> configBundles = new ArrayList<ConfigBundle>(200);
269 
270         // If the edited file has locales, then we have to select a matching locale from
271         // the list.
272         // However, if it doesn't, we don't randomly take the first locale, we take one
273         // matching the current host locale (making sure it actually exist in the project)
274         int start, max;
275         if (editedConfig.getLanguageQualifier() != null || localeHostMatch == -1) {
276             // add all the locales
277             start = 0;
278             max = localeList.size();
279         } else {
280             // only add the locale host match
281             start = localeHostMatch;
282             max = localeHostMatch + 1; // test is <
283         }
284 
285         for (int i = start ; i < max ; i++) {
286             Locale l = localeList.get(i);
287 
288             ConfigBundle bundle = new ConfigBundle();
289             bundle.config.setLanguageQualifier(l.language);
290             bundle.config.setRegionQualifier(l.region);
291 
292             bundle.localeIndex = i;
293             configBundles.add(bundle);
294         }
295 
296         // add the dock mode to the bundle combinations.
297         addDockModeToBundles(configBundles);
298 
299         // add the night mode to the bundle combinations.
300         addNightModeToBundles(configBundles);
301 
302         addRenderTargetToBundles(configBundles);
303 
304         for (Device device : deviceList) {
305             for (State state : device.getAllStates()) {
306 
307                 // loop on the list of config bundles to create full
308                 // configurations.
309                 FolderConfiguration stateConfig = DeviceConfigHelper.getFolderConfig(state);
310                 for (ConfigBundle bundle : configBundles) {
311                     // create a new config with device config
312                     FolderConfiguration testConfig = new FolderConfiguration();
313                     testConfig.set(stateConfig);
314 
315                     // add on top of it, the extra qualifiers from the bundle
316                     testConfig.add(bundle.config);
317 
318                     if (editedConfig.isMatchFor(testConfig)) {
319                         // this is a basic match. record it in case we don't
320                         // find a match
321                         // where the edited file is a best config.
322                         anyMatches.add(new ConfigMatch(testConfig, device, state, bundle));
323 
324                         if (isCurrentFileBestMatchFor(testConfig)) {
325                             // this is what we want.
326                             bestMatches.add(new ConfigMatch(testConfig, device, state, bundle));
327                         }
328                     }
329                 }
330             }
331         }
332 
333         if (bestMatches.size() == 0) {
334             if (favorCurrentConfig) {
335                 // quick check
336                 if (!editedConfig.isMatchFor(currentConfig)) {
337                     AdtPlugin.log(IStatus.ERROR,
338                         "favorCurrentConfig can only be true if the current config is compatible");
339                 }
340 
341                 // just display the warning
342                 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
343                         String.format(
344                                 "'%1$s' is not a best match for any device/locale combination.",
345                                 editedConfig.toDisplayString()),
346                         String.format(
347                                 "Displaying it with '%1$s'",
348                                 currentConfig.toDisplayString()));
349             } else if (anyMatches.size() > 0) {
350                 // select the best device anyway.
351                 ConfigMatch match = selectConfigMatch(anyMatches);
352                 mConfiguration.setDevice(match.device, true);
353                 mConfiguration.setDeviceState(match.state, true);
354                 mConfiguration.setLocale(localeList.get(match.bundle.localeIndex), true);
355                 mConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true);
356                 mConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex),
357                         true);
358 
359                 if (mUpdateUi) {
360                     mConfigChooser.selectDevice(mConfiguration.getDevice());
361                     mConfigChooser.selectDeviceState(mConfiguration.getDeviceState());
362                     mConfigChooser.selectLocale(mConfiguration.getLocale());
363                 }
364 
365                 mConfiguration.syncFolderConfig();
366 
367                 // TODO: display a better warning!
368                 AdtPlugin.printErrorToConsole(mEditedFile.getProject(),
369                         String.format(
370                                 "'%1$s' is not a best match for any device/locale combination.",
371                                 editedConfig.toDisplayString()),
372                         String.format(
373                                 "Displaying it with '%1$s' which is compatible, but will " +
374                                 "actually be displayed with another more specific version of " +
375                                 "the layout.",
376                                 currentConfig.toDisplayString()));
377 
378             } else {
379                 // TODO: there is no device/config able to display the layout, create one.
380                 // For the base config values, we'll take the first device and state,
381                 // and replace whatever qualifier required by the layout file.
382             }
383         } else {
384             ConfigMatch match = selectConfigMatch(bestMatches);
385             mConfiguration.setDevice(match.device, true);
386             mConfiguration.setDeviceState(match.state, true);
387             mConfiguration.setLocale(localeList.get(match.bundle.localeIndex), true);
388             mConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true);
389             mConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex), true);
390 
391             mConfiguration.syncFolderConfig();
392 
393             if (mUpdateUi) {
394                 mConfigChooser.selectDevice(mConfiguration.getDevice());
395                 mConfigChooser.selectDeviceState(mConfiguration.getDeviceState());
396                 mConfigChooser.selectLocale(mConfiguration.getLocale());
397             }
398         }
399     }
400 
addRenderTargetToBundles(List<ConfigBundle> configBundles)401     private void addRenderTargetToBundles(List<ConfigBundle> configBundles) {
402         Pair<Locale, IAndroidTarget> state = Configuration.loadRenderState(mConfigChooser);
403         if (state != null) {
404             IAndroidTarget target = state.getSecond();
405             if (target != null) {
406                 int apiLevel = target.getVersion().getApiLevel();
407                 for (ConfigBundle bundle : configBundles) {
408                     bundle.config.setVersionQualifier(
409                             new VersionQualifier(apiLevel));
410                 }
411             }
412         }
413     }
414 
addDockModeToBundles(List<ConfigBundle> addConfig)415     private void addDockModeToBundles(List<ConfigBundle> addConfig) {
416         ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
417 
418         // loop on each item and for each, add all variations of the dock modes
419         for (ConfigBundle bundle : addConfig) {
420             int index = 0;
421             for (UiMode mode : UiMode.values()) {
422                 ConfigBundle b = new ConfigBundle(bundle);
423                 b.config.setUiModeQualifier(new UiModeQualifier(mode));
424                 b.dockModeIndex = index++;
425                 list.add(b);
426             }
427         }
428 
429         addConfig.clear();
430         addConfig.addAll(list);
431     }
432 
addNightModeToBundles(List<ConfigBundle> addConfig)433     private void addNightModeToBundles(List<ConfigBundle> addConfig) {
434         ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
435 
436         // loop on each item and for each, add all variations of the night modes
437         for (ConfigBundle bundle : addConfig) {
438             int index = 0;
439             for (NightMode mode : NightMode.values()) {
440                 ConfigBundle b = new ConfigBundle(bundle);
441                 b.config.setNightModeQualifier(new NightModeQualifier(mode));
442                 b.nightModeIndex = index++;
443                 list.add(b);
444             }
445         }
446 
447         addConfig.clear();
448         addConfig.addAll(list);
449     }
450 
getLocaleMatch()451     private int getLocaleMatch() {
452         java.util.Locale defaultLocale = java.util.Locale.getDefault();
453         if (defaultLocale != null) {
454             String currentLanguage = defaultLocale.getLanguage();
455             String currentRegion = defaultLocale.getCountry();
456 
457             List<Locale> localeList = mConfigChooser.getLocaleList();
458             final int count = localeList.size();
459             for (int l = 0; l < count; l++) {
460                 Locale locale = localeList.get(l);
461                 LanguageQualifier langQ = locale.language;
462                 RegionQualifier regionQ = locale.region;
463 
464                 // there's always a ##/Other or ##/Any (which is the same, the region
465                 // contains FAKE_REGION_VALUE). If we don't find a perfect region match
466                 // we take the fake region. Since it's last in the list, this makes the
467                 // test easy.
468                 if (langQ.getValue().equals(currentLanguage) &&
469                         (regionQ.getValue().equals(currentRegion) ||
470                          regionQ.getValue().equals(RegionQualifier.FAKE_REGION_VALUE))) {
471                     return l;
472                 }
473             }
474 
475             // if no locale match the current local locale, it's likely that it is
476             // the default one which is the last one.
477             return count - 1;
478         }
479 
480         return -1;
481     }
482 
selectConfigMatch(List<ConfigMatch> matches)483     private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) {
484         // API 11-13: look for a x-large device
485         Comparator<ConfigMatch> comparator = null;
486         Sdk sdk = Sdk.getCurrent();
487         if (sdk != null) {
488             IAndroidTarget projectTarget = sdk.getTarget(mEditedFile.getProject());
489             if (projectTarget != null) {
490                 int apiLevel = projectTarget.getVersion().getApiLevel();
491                 if (apiLevel >= 11 && apiLevel < 14) {
492                     // TODO: Maybe check the compatible-screen tag in the manifest to figure out
493                     // what kind of device should be used for display.
494                     comparator = new TabletConfigComparator();
495                 }
496             }
497         }
498         if (comparator == null) {
499             // lets look for a high density device
500             comparator = new PhoneConfigComparator();
501         }
502         Collections.sort(matches, comparator);
503 
504         // Look at the currently active editor to see if it's a layout editor, and if so,
505         // look up its configuration and if the configuration is in our match list,
506         // use it. This means we "preserve" the current configuration when you open
507         // new layouts.
508         IEditorPart activeEditor = AdtUtils.getActiveEditor();
509         LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor);
510         if (delegate != null
511                 // (Only do this when the two files are in the same project)
512                 && delegate.getEditor().getProject() == mEditedFile.getProject()) {
513             FolderConfiguration configuration = delegate.getGraphicalEditor().getConfiguration();
514             if (configuration != null) {
515                 for (ConfigMatch match : matches) {
516                     if (configuration.equals(match.testConfig)) {
517                         return match;
518                     }
519                 }
520             }
521         }
522 
523         // the list has been sorted so that the first item is the best config
524         return matches.get(0);
525     }
526 
527     /** Return the default render target to use, or null if no strong preference */
528     @Nullable
findDefaultRenderTarget(ConfigurationChooser chooser)529     static IAndroidTarget findDefaultRenderTarget(ConfigurationChooser chooser) {
530         if (PREFER_RECENT_RENDER_TARGETS) {
531             // Use the most recent target
532             List<IAndroidTarget> targetList = chooser.getTargetList();
533             if (!targetList.isEmpty()) {
534                 return targetList.get(targetList.size() - 1);
535             }
536         }
537 
538         IProject project = chooser.getProject();
539         // Default to layoutlib version 5
540         Sdk current = Sdk.getCurrent();
541         if (current != null) {
542             IAndroidTarget projectTarget = current.getTarget(project);
543             int minProjectApi = Integer.MAX_VALUE;
544             if (projectTarget != null) {
545                 if (!projectTarget.isPlatform() && projectTarget.hasRenderingLibrary()) {
546                     // Renderable non-platform targets are all going to be adequate (they
547                     // will have at least version 5 of layoutlib) so use the project
548                     // target as the render target.
549                     return projectTarget;
550                 }
551 
552                 if (projectTarget.getVersion().isPreview()
553                         && projectTarget.hasRenderingLibrary()) {
554                     // If the project target is a preview version, then just use it
555                     return projectTarget;
556                 }
557 
558                 minProjectApi = projectTarget.getVersion().getApiLevel();
559             }
560 
561             // We want to pick a render target that contains at least version 5 (and
562             // preferably version 6) of the layout library. To do this, we go through the
563             // targets and pick the -smallest- API level that is both simultaneously at
564             // least as big as the project API level, and supports layoutlib level 5+.
565             IAndroidTarget best = null;
566             int bestApiLevel = Integer.MAX_VALUE;
567 
568             for (IAndroidTarget target : current.getTargets()) {
569                 // Non-platform targets are not chosen as the default render target
570                 if (!target.isPlatform()) {
571                     continue;
572                 }
573 
574                 int apiLevel = target.getVersion().getApiLevel();
575 
576                 // Ignore targets that have a lower API level than the minimum project
577                 // API level:
578                 if (apiLevel < minProjectApi) {
579                     continue;
580                 }
581 
582                 // Look up the layout lib API level. This property is new so it will only
583                 // be defined for version 6 or higher, which means non-null is adequate
584                 // to see if this target is eligible:
585                 String property = target.getProperty(PkgProps.LAYOUTLIB_API);
586                 // In addition, Android 3.0 with API level 11 had version 5.0 which is adequate:
587                 if (property != null || apiLevel >= 11) {
588                     if (apiLevel < bestApiLevel) {
589                         bestApiLevel = apiLevel;
590                         best = target;
591                     }
592                 }
593             }
594 
595             return best;
596         }
597 
598         return null;
599     }
600 
601     /**
602      * Attempts to find a close state among a list
603      *
604      * @param oldConfig the reference config.
605      * @param states the list of states to search through
606      * @return the name of the closest state match, or possibly null if no states are compatible
607      * (this can only happen if the states don't have a single qualifier that is the same).
608      */
609     @Nullable
getClosestMatch(@onNull FolderConfiguration oldConfig, @NonNull List<State> states)610     static String getClosestMatch(@NonNull FolderConfiguration oldConfig,
611             @NonNull List<State> states) {
612 
613         // create 2 lists as we're going to go through one and put the
614         // candidates in the other.
615         List<State> list1 = new ArrayList<State>(states.size());
616         List<State> list2 = new ArrayList<State>(states.size());
617 
618         list1.addAll(states);
619 
620         final int count = FolderConfiguration.getQualifierCount();
621         for (int i = 0 ; i < count ; i++) {
622             // compute the new candidate list by only taking states that have
623             // the same i-th qualifier as the old state
624             for (State s : list1) {
625                 ResourceQualifier oldQualifier = oldConfig.getQualifier(i);
626 
627                 FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s);
628                 ResourceQualifier newQualifier =
629                         folderConfig != null ? folderConfig.getQualifier(i) : null;
630 
631                 if (oldQualifier == null) {
632                     if (newQualifier == null) {
633                         list2.add(s);
634                     }
635                 } else if (oldQualifier.equals(newQualifier)) {
636                     list2.add(s);
637                 }
638             }
639 
640             // at any moment if the new candidate list contains only one match, its name
641             // is returned.
642             if (list2.size() == 1) {
643                 return list2.get(0).getName();
644             }
645 
646             // if the list is empty, then all the new states failed. It is considered ok, and
647             // we move to the next qualifier anyway. This way, if a qualifier is different for
648             // all new states it is simply ignored.
649             if (list2.size() != 0) {
650                 // move the candidates back into list1.
651                 list1.clear();
652                 list1.addAll(list2);
653                 list2.clear();
654             }
655         }
656 
657         // the only way to reach this point is if there's an exact match.
658         // (if there are more than one, then there's a duplicate state and it doesn't matter,
659         // we take the first one).
660         if (list1.size() > 0) {
661             return list1.get(0).getName();
662         }
663 
664         return null;
665     }
666 
667     /**
668      * Returns the layout {@link IFile} which best matches the configuration
669      * selected in the given configuration chooser.
670      *
671      * @param chooser the associated configuration chooser holding project state
672      * @return the file which best matches the settings
673      */
674     @Nullable
getBestFileMatch(ConfigurationChooser chooser)675     public static IFile getBestFileMatch(ConfigurationChooser chooser) {
676         // get the resources of the file's project.
677         ResourceManager manager = ResourceManager.getInstance();
678         ProjectResources resources = manager.getProjectResources(chooser.getProject());
679         if (resources == null) {
680             return null;
681         }
682 
683         // From the resources, look for a matching file
684         IFile editedFile = chooser.getEditedFile();
685         if (editedFile == null) {
686             return null;
687         }
688         String name = editedFile.getName();
689         FolderConfiguration config = chooser.getConfiguration().getFullConfig();
690         ResourceFile match = resources.getMatchingFile(name, ResourceType.LAYOUT, config);
691 
692         if (match != null) {
693             // In Eclipse, the match's file is always an instance of IFileWrapper
694             return ((IFileWrapper) match.getFile()).getIFile();
695         }
696 
697         return null;
698     }
699 
700     /**
701      * Note: this comparator imposes orderings that are inconsistent with equals.
702      */
703     private static class TabletConfigComparator implements Comparator<ConfigMatch> {
704         @Override
compare(ConfigMatch o1, ConfigMatch o2)705         public int compare(ConfigMatch o1, ConfigMatch o2) {
706             FolderConfiguration config1 = o1 != null ? o1.testConfig : null;
707             FolderConfiguration config2 = o2 != null ? o2.testConfig : null;
708             if (config1 == null) {
709                 if (config2 == null) {
710                     return 0;
711                 } else {
712                     return -1;
713                 }
714             } else if (config2 == null) {
715                 return 1;
716             }
717 
718             ScreenSizeQualifier size1 = config1.getScreenSizeQualifier();
719             ScreenSizeQualifier size2 = config2.getScreenSizeQualifier();
720             ScreenSize ss1 = size1 != null ? size1.getValue() : ScreenSize.NORMAL;
721             ScreenSize ss2 = size2 != null ? size2.getValue() : ScreenSize.NORMAL;
722 
723             // X-LARGE is better than all others (which are considered identical)
724             // if both X-LARGE, then LANDSCAPE is better than all others (which are identical)
725 
726             if (ss1 == ScreenSize.XLARGE) {
727                 if (ss2 == ScreenSize.XLARGE) {
728                     ScreenOrientationQualifier orientation1 =
729                             config1.getScreenOrientationQualifier();
730                     ScreenOrientation so1 = orientation1.getValue();
731                     if (so1 == null) {
732                         so1 = ScreenOrientation.PORTRAIT;
733                     }
734                     ScreenOrientationQualifier orientation2 =
735                             config2.getScreenOrientationQualifier();
736                     ScreenOrientation so2 = orientation2.getValue();
737                     if (so2 == null) {
738                         so2 = ScreenOrientation.PORTRAIT;
739                     }
740 
741                     if (so1 == ScreenOrientation.LANDSCAPE) {
742                         if (so2 == ScreenOrientation.LANDSCAPE) {
743                             return 0;
744                         } else {
745                             return -1;
746                         }
747                     } else if (so2 == ScreenOrientation.LANDSCAPE) {
748                         return 1;
749                     } else {
750                         return 0;
751                     }
752                 } else {
753                     return -1;
754                 }
755             } else if (ss2 == ScreenSize.XLARGE) {
756                 return 1;
757             } else {
758                 return 0;
759             }
760         }
761     }
762 
763     /**
764      * Note: this comparator imposes orderings that are inconsistent with equals.
765      */
766     private static class PhoneConfigComparator implements Comparator<ConfigMatch> {
767 
768         private SparseIntArray mDensitySort = new SparseIntArray(4);
769 
PhoneConfigComparator()770         public PhoneConfigComparator() {
771             // put the sort order for the density.
772             mDensitySort.put(Density.HIGH.getDpiValue(),   1);
773             mDensitySort.put(Density.MEDIUM.getDpiValue(), 2);
774             mDensitySort.put(Density.XHIGH.getDpiValue(),  3);
775             mDensitySort.put(Density.LOW.getDpiValue(),    4);
776         }
777 
778         @Override
compare(ConfigMatch o1, ConfigMatch o2)779         public int compare(ConfigMatch o1, ConfigMatch o2) {
780             FolderConfiguration config1 = o1 != null ? o1.testConfig : null;
781             FolderConfiguration config2 = o2 != null ? o2.testConfig : null;
782             if (config1 == null) {
783                 if (config2 == null) {
784                     return 0;
785                 } else {
786                     return -1;
787                 }
788             } else if (config2 == null) {
789                 return 1;
790             }
791 
792             int dpi1 = Density.DEFAULT_DENSITY;
793             int dpi2 = Density.DEFAULT_DENSITY;
794 
795             DensityQualifier dpiQualifier1 = config1.getDensityQualifier();
796             if (dpiQualifier1 != null) {
797                 Density value = dpiQualifier1.getValue();
798                 dpi1 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY;
799             }
800             dpi1 = mDensitySort.get(dpi1, 100 /* valueIfKeyNotFound*/);
801 
802             DensityQualifier dpiQualifier2 = config2.getDensityQualifier();
803             if (dpiQualifier2 != null) {
804                 Density value = dpiQualifier2.getValue();
805                 dpi2 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY;
806             }
807             dpi2 = mDensitySort.get(dpi2, 100 /* valueIfKeyNotFound*/);
808 
809             if (dpi1 == dpi2) {
810                 // portrait is better
811                 ScreenOrientation so1 = ScreenOrientation.PORTRAIT;
812                 ScreenOrientationQualifier orientationQualifier1 =
813                         config1.getScreenOrientationQualifier();
814                 if (orientationQualifier1 != null) {
815                     so1 = orientationQualifier1.getValue();
816                     if (so1 == null) {
817                         so1 = ScreenOrientation.PORTRAIT;
818                     }
819                 }
820                 ScreenOrientation so2 = ScreenOrientation.PORTRAIT;
821                 ScreenOrientationQualifier orientationQualifier2 =
822                         config2.getScreenOrientationQualifier();
823                 if (orientationQualifier2 != null) {
824                     so2 = orientationQualifier2.getValue();
825                     if (so2 == null) {
826                         so2 = ScreenOrientation.PORTRAIT;
827                     }
828                 }
829 
830                 if (so1 == ScreenOrientation.PORTRAIT) {
831                     if (so2 == ScreenOrientation.PORTRAIT) {
832                         return 0;
833                     } else {
834                         return -1;
835                     }
836                 } else if (so2 == ScreenOrientation.PORTRAIT) {
837                     return 1;
838                 } else {
839                     return 0;
840                 }
841             }
842 
843             return dpi1 - dpi2;
844         }
845     }
846 }
847