Home | History | Annotate | Download | only in configuration
      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.utils.Pair;
     50 import com.android.utils.SparseIntArray;
     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 
     77     ConfigurationMatcher(ConfigurationChooser chooser) {
     78         this(chooser, chooser.getConfiguration(), chooser.getEditedFile(),
     79                 chooser.getResources(), true);
     80     }
     81 
     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 
    103         private ConfigBundle() {
    104             config = new FolderConfiguration();
    105         }
    106 
    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 
    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
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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 
    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 
    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
    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
    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
    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
    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 
    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
    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