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.LocaleQualifier; 25 import com.android.ide.common.resources.configuration.NightModeQualifier; 26 import com.android.ide.common.resources.configuration.ResourceQualifier; 27 import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; 28 import com.android.ide.common.resources.configuration.ScreenSizeQualifier; 29 import com.android.ide.common.resources.configuration.UiModeQualifier; 30 import com.android.ide.common.resources.configuration.VersionQualifier; 31 import com.android.ide.eclipse.adt.AdtPlugin; 32 import com.android.ide.eclipse.adt.AdtUtils; 33 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 34 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 35 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 36 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 37 import com.android.ide.eclipse.adt.io.IFileWrapper; 38 import com.android.resources.Density; 39 import com.android.resources.NightMode; 40 import com.android.resources.ResourceType; 41 import com.android.resources.ScreenOrientation; 42 import com.android.resources.ScreenSize; 43 import com.android.resources.UiMode; 44 import com.android.sdklib.IAndroidTarget; 45 import com.android.sdklib.devices.Device; 46 import com.android.sdklib.devices.State; 47 import com.android.sdklib.repository.PkgProps; 48 import com.android.utils.Pair; 49 import com.android.utils.SparseIntArray; 50 51 import org.eclipse.core.resources.IFile; 52 import org.eclipse.core.resources.IProject; 53 import org.eclipse.core.runtime.IStatus; 54 import org.eclipse.ui.IEditorPart; 55 56 import java.util.ArrayList; 57 import java.util.Collection; 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.setLocaleQualifier(locale.qualifier); 208 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 Collection<Device> devices = mConfigChooser.getDevices(); 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.getLocaleQualifier() != 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.setLocaleQualifier(l.qualifier); 290 291 bundle.localeIndex = i; 292 configBundles.add(bundle); 293 } 294 295 // add the dock mode to the bundle combinations. 296 addDockModeToBundles(configBundles); 297 298 // add the night mode to the bundle combinations. 299 addNightModeToBundles(configBundles); 300 301 addRenderTargetToBundles(configBundles); 302 303 for (Device device : devices) { 304 for (State state : device.getAllStates()) { 305 306 // loop on the list of config bundles to create full 307 // configurations. 308 FolderConfiguration stateConfig = DeviceConfigHelper.getFolderConfig(state); 309 for (ConfigBundle bundle : configBundles) { 310 // create a new config with device config 311 FolderConfiguration testConfig = new FolderConfiguration(); 312 testConfig.set(stateConfig); 313 314 // add on top of it, the extra qualifiers from the bundle 315 testConfig.add(bundle.config); 316 317 if (editedConfig.isMatchFor(testConfig)) { 318 // this is a basic match. record it in case we don't 319 // find a match 320 // where the edited file is a best config. 321 anyMatches.add(new ConfigMatch(testConfig, device, state, bundle)); 322 323 if (isCurrentFileBestMatchFor(testConfig)) { 324 // this is what we want. 325 bestMatches.add(new ConfigMatch(testConfig, device, state, bundle)); 326 } 327 } 328 } 329 } 330 } 331 332 if (bestMatches.size() == 0) { 333 if (favorCurrentConfig) { 334 // quick check 335 if (!editedConfig.isMatchFor(currentConfig)) { 336 AdtPlugin.log(IStatus.ERROR, 337 "favorCurrentConfig can only be true if the current config is compatible"); 338 } 339 340 // just display the warning 341 AdtPlugin.printErrorToConsole(mEditedFile.getProject(), 342 String.format( 343 "'%1$s' is not a best match for any device/locale combination.", 344 editedConfig.toDisplayString()), 345 String.format( 346 "Displaying it with '%1$s'", 347 currentConfig.toDisplayString())); 348 } else if (anyMatches.size() > 0) { 349 // select the best device anyway. 350 ConfigMatch match = selectConfigMatch(anyMatches); 351 mConfiguration.setDevice(match.device, true); 352 mConfiguration.setDeviceState(match.state, true); 353 mConfiguration.setLocale(localeList.get(match.bundle.localeIndex), true); 354 mConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true); 355 mConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex), 356 true); 357 358 if (mUpdateUi) { 359 mConfigChooser.selectDevice(mConfiguration.getDevice()); 360 mConfigChooser.selectDeviceState(mConfiguration.getDeviceState()); 361 mConfigChooser.selectLocale(mConfiguration.getLocale()); 362 } 363 364 mConfiguration.syncFolderConfig(); 365 366 // TODO: display a better warning! 367 AdtPlugin.printErrorToConsole(mEditedFile.getProject(), 368 String.format( 369 "'%1$s' is not a best match for any device/locale combination.", 370 editedConfig.toDisplayString()), 371 String.format( 372 "Displaying it with '%1$s' which is compatible, but will " + 373 "actually be displayed with another more specific version of " + 374 "the layout.", 375 currentConfig.toDisplayString())); 376 377 } else { 378 // TODO: there is no device/config able to display the layout, create one. 379 // For the base config values, we'll take the first device and state, 380 // and replace whatever qualifier required by the layout file. 381 } 382 } else { 383 ConfigMatch match = selectConfigMatch(bestMatches); 384 mConfiguration.setDevice(match.device, true); 385 mConfiguration.setDeviceState(match.state, true); 386 mConfiguration.setLocale(localeList.get(match.bundle.localeIndex), true); 387 mConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true); 388 mConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex), true); 389 390 mConfiguration.syncFolderConfig(); 391 392 if (mUpdateUi) { 393 mConfigChooser.selectDevice(mConfiguration.getDevice()); 394 mConfigChooser.selectDeviceState(mConfiguration.getDeviceState()); 395 mConfigChooser.selectLocale(mConfiguration.getLocale()); 396 } 397 } 398 } 399 addRenderTargetToBundles(List<ConfigBundle> configBundles)400 private void addRenderTargetToBundles(List<ConfigBundle> configBundles) { 401 Pair<Locale, IAndroidTarget> state = Configuration.loadRenderState(mConfigChooser); 402 if (state != null) { 403 IAndroidTarget target = state.getSecond(); 404 if (target != null) { 405 int apiLevel = target.getVersion().getApiLevel(); 406 for (ConfigBundle bundle : configBundles) { 407 bundle.config.setVersionQualifier( 408 new VersionQualifier(apiLevel)); 409 } 410 } 411 } 412 } 413 addDockModeToBundles(List<ConfigBundle> addConfig)414 private void addDockModeToBundles(List<ConfigBundle> addConfig) { 415 ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>(); 416 417 // loop on each item and for each, add all variations of the dock modes 418 for (ConfigBundle bundle : addConfig) { 419 int index = 0; 420 for (UiMode mode : UiMode.values()) { 421 ConfigBundle b = new ConfigBundle(bundle); 422 b.config.setUiModeQualifier(new UiModeQualifier(mode)); 423 b.dockModeIndex = index++; 424 list.add(b); 425 } 426 } 427 428 addConfig.clear(); 429 addConfig.addAll(list); 430 } 431 addNightModeToBundles(List<ConfigBundle> addConfig)432 private void addNightModeToBundles(List<ConfigBundle> addConfig) { 433 ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>(); 434 435 // loop on each item and for each, add all variations of the night modes 436 for (ConfigBundle bundle : addConfig) { 437 int index = 0; 438 for (NightMode mode : NightMode.values()) { 439 ConfigBundle b = new ConfigBundle(bundle); 440 b.config.setNightModeQualifier(new NightModeQualifier(mode)); 441 b.nightModeIndex = index++; 442 list.add(b); 443 } 444 } 445 446 addConfig.clear(); 447 addConfig.addAll(list); 448 } 449 getLocaleMatch()450 private int getLocaleMatch() { 451 java.util.Locale defaultLocale = java.util.Locale.getDefault(); 452 if (defaultLocale != null) { 453 String currentLanguage = defaultLocale.getLanguage(); 454 String currentRegion = defaultLocale.getCountry(); 455 456 List<Locale> localeList = mConfigChooser.getLocaleList(); 457 final int count = localeList.size(); 458 for (int l = 0; l < count; l++) { 459 Locale locale = localeList.get(l); 460 LocaleQualifier qualifier = locale.qualifier; 461 462 // there's always a ##/Other or ##/Any (which is the same, the region 463 // contains FAKE_REGION_VALUE). If we don't find a perfect region match 464 // we take the fake region. Since it's last in the list, this makes the 465 // test easy. 466 if (qualifier.getLanguage().equals(currentLanguage) && 467 (qualifier.getRegion() == null || qualifier.getRegion().equals(currentRegion))) { 468 return l; 469 } 470 } 471 472 // if no locale match the current local locale, it's likely that it is 473 // the default one which is the last one. 474 return count - 1; 475 } 476 477 return -1; 478 } 479 selectConfigMatch(List<ConfigMatch> matches)480 private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) { 481 // API 11-13: look for a x-large device 482 Comparator<ConfigMatch> comparator = null; 483 Sdk sdk = Sdk.getCurrent(); 484 if (sdk != null) { 485 IAndroidTarget projectTarget = sdk.getTarget(mEditedFile.getProject()); 486 if (projectTarget != null) { 487 int apiLevel = projectTarget.getVersion().getApiLevel(); 488 if (apiLevel >= 11 && apiLevel < 14) { 489 // TODO: Maybe check the compatible-screen tag in the manifest to figure out 490 // what kind of device should be used for display. 491 comparator = new TabletConfigComparator(); 492 } 493 } 494 } 495 if (comparator == null) { 496 // lets look for a high density device 497 comparator = new PhoneConfigComparator(); 498 } 499 Collections.sort(matches, comparator); 500 501 // Look at the currently active editor to see if it's a layout editor, and if so, 502 // look up its configuration and if the configuration is in our match list, 503 // use it. This means we "preserve" the current configuration when you open 504 // new layouts. 505 IEditorPart activeEditor = AdtUtils.getActiveEditor(); 506 LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor); 507 if (delegate != null 508 // (Only do this when the two files are in the same project) 509 && delegate.getEditor().getProject() == mEditedFile.getProject()) { 510 FolderConfiguration configuration = delegate.getGraphicalEditor().getConfiguration(); 511 if (configuration != null) { 512 for (ConfigMatch match : matches) { 513 if (configuration.equals(match.testConfig)) { 514 return match; 515 } 516 } 517 } 518 } 519 520 // the list has been sorted so that the first item is the best config 521 return matches.get(0); 522 } 523 524 /** Return the default render target to use, or null if no strong preference */ 525 @Nullable findDefaultRenderTarget(ConfigurationChooser chooser)526 static IAndroidTarget findDefaultRenderTarget(ConfigurationChooser chooser) { 527 if (PREFER_RECENT_RENDER_TARGETS) { 528 // Use the most recent target 529 List<IAndroidTarget> targetList = chooser.getTargetList(); 530 if (!targetList.isEmpty()) { 531 return targetList.get(targetList.size() - 1); 532 } 533 } 534 535 IProject project = chooser.getProject(); 536 // Default to layoutlib version 5 537 Sdk current = Sdk.getCurrent(); 538 if (current != null) { 539 IAndroidTarget projectTarget = current.getTarget(project); 540 int minProjectApi = Integer.MAX_VALUE; 541 if (projectTarget != null) { 542 if (!projectTarget.isPlatform() && projectTarget.hasRenderingLibrary()) { 543 // Renderable non-platform targets are all going to be adequate (they 544 // will have at least version 5 of layoutlib) so use the project 545 // target as the render target. 546 return projectTarget; 547 } 548 549 if (projectTarget.getVersion().isPreview() 550 && projectTarget.hasRenderingLibrary()) { 551 // If the project target is a preview version, then just use it 552 return projectTarget; 553 } 554 555 minProjectApi = projectTarget.getVersion().getApiLevel(); 556 } 557 558 // We want to pick a render target that contains at least version 5 (and 559 // preferably version 6) of the layout library. To do this, we go through the 560 // targets and pick the -smallest- API level that is both simultaneously at 561 // least as big as the project API level, and supports layoutlib level 5+. 562 IAndroidTarget best = null; 563 int bestApiLevel = Integer.MAX_VALUE; 564 565 for (IAndroidTarget target : current.getTargets()) { 566 // Non-platform targets are not chosen as the default render target 567 if (!target.isPlatform()) { 568 continue; 569 } 570 571 int apiLevel = target.getVersion().getApiLevel(); 572 573 // Ignore targets that have a lower API level than the minimum project 574 // API level: 575 if (apiLevel < minProjectApi) { 576 continue; 577 } 578 579 // Look up the layout lib API level. This property is new so it will only 580 // be defined for version 6 or higher, which means non-null is adequate 581 // to see if this target is eligible: 582 String property = target.getProperty(PkgProps.LAYOUTLIB_API); 583 // In addition, Android 3.0 with API level 11 had version 5.0 which is adequate: 584 if (property != null || apiLevel >= 11) { 585 if (apiLevel < bestApiLevel) { 586 bestApiLevel = apiLevel; 587 best = target; 588 } 589 } 590 } 591 592 return best; 593 } 594 595 return null; 596 } 597 598 /** 599 * Attempts to find a close state among a list 600 * 601 * @param oldConfig the reference config. 602 * @param states the list of states to search through 603 * @return the name of the closest state match, or possibly null if no states are compatible 604 * (this can only happen if the states don't have a single qualifier that is the same). 605 */ 606 @Nullable getClosestMatch(@onNull FolderConfiguration oldConfig, @NonNull List<State> states)607 static String getClosestMatch(@NonNull FolderConfiguration oldConfig, 608 @NonNull List<State> states) { 609 610 // create 2 lists as we're going to go through one and put the 611 // candidates in the other. 612 List<State> list1 = new ArrayList<State>(states.size()); 613 List<State> list2 = new ArrayList<State>(states.size()); 614 615 list1.addAll(states); 616 617 final int count = FolderConfiguration.getQualifierCount(); 618 for (int i = 0 ; i < count ; i++) { 619 // compute the new candidate list by only taking states that have 620 // the same i-th qualifier as the old state 621 for (State s : list1) { 622 ResourceQualifier oldQualifier = oldConfig.getQualifier(i); 623 624 FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s); 625 ResourceQualifier newQualifier = 626 folderConfig != null ? folderConfig.getQualifier(i) : null; 627 628 if (oldQualifier == null) { 629 if (newQualifier == null) { 630 list2.add(s); 631 } 632 } else if (oldQualifier.equals(newQualifier)) { 633 list2.add(s); 634 } 635 } 636 637 // at any moment if the new candidate list contains only one match, its name 638 // is returned. 639 if (list2.size() == 1) { 640 return list2.get(0).getName(); 641 } 642 643 // if the list is empty, then all the new states failed. It is considered ok, and 644 // we move to the next qualifier anyway. This way, if a qualifier is different for 645 // all new states it is simply ignored. 646 if (list2.size() != 0) { 647 // move the candidates back into list1. 648 list1.clear(); 649 list1.addAll(list2); 650 list2.clear(); 651 } 652 } 653 654 // the only way to reach this point is if there's an exact match. 655 // (if there are more than one, then there's a duplicate state and it doesn't matter, 656 // we take the first one). 657 if (list1.size() > 0) { 658 return list1.get(0).getName(); 659 } 660 661 return null; 662 } 663 664 /** 665 * Returns the layout {@link IFile} which best matches the configuration 666 * selected in the given configuration chooser. 667 * 668 * @param chooser the associated configuration chooser holding project state 669 * @return the file which best matches the settings 670 */ 671 @Nullable getBestFileMatch(ConfigurationChooser chooser)672 public static IFile getBestFileMatch(ConfigurationChooser chooser) { 673 // get the resources of the file's project. 674 ResourceManager manager = ResourceManager.getInstance(); 675 ProjectResources resources = manager.getProjectResources(chooser.getProject()); 676 if (resources == null) { 677 return null; 678 } 679 680 // From the resources, look for a matching file 681 IFile editedFile = chooser.getEditedFile(); 682 if (editedFile == null) { 683 return null; 684 } 685 String name = editedFile.getName(); 686 FolderConfiguration config = chooser.getConfiguration().getFullConfig(); 687 ResourceFile match = resources.getMatchingFile(name, ResourceType.LAYOUT, config); 688 689 if (match != null) { 690 // In Eclipse, the match's file is always an instance of IFileWrapper 691 return ((IFileWrapper) match.getFile()).getIFile(); 692 } 693 694 return null; 695 } 696 697 /** 698 * Note: this comparator imposes orderings that are inconsistent with equals. 699 */ 700 private static class TabletConfigComparator implements Comparator<ConfigMatch> { 701 @Override compare(ConfigMatch o1, ConfigMatch o2)702 public int compare(ConfigMatch o1, ConfigMatch o2) { 703 FolderConfiguration config1 = o1 != null ? o1.testConfig : null; 704 FolderConfiguration config2 = o2 != null ? o2.testConfig : null; 705 if (config1 == null) { 706 if (config2 == null) { 707 return 0; 708 } else { 709 return -1; 710 } 711 } else if (config2 == null) { 712 return 1; 713 } 714 715 ScreenSizeQualifier size1 = config1.getScreenSizeQualifier(); 716 ScreenSizeQualifier size2 = config2.getScreenSizeQualifier(); 717 ScreenSize ss1 = size1 != null ? size1.getValue() : ScreenSize.NORMAL; 718 ScreenSize ss2 = size2 != null ? size2.getValue() : ScreenSize.NORMAL; 719 720 // X-LARGE is better than all others (which are considered identical) 721 // if both X-LARGE, then LANDSCAPE is better than all others (which are identical) 722 723 if (ss1 == ScreenSize.XLARGE) { 724 if (ss2 == ScreenSize.XLARGE) { 725 ScreenOrientationQualifier orientation1 = 726 config1.getScreenOrientationQualifier(); 727 ScreenOrientation so1 = orientation1.getValue(); 728 if (so1 == null) { 729 so1 = ScreenOrientation.PORTRAIT; 730 } 731 ScreenOrientationQualifier orientation2 = 732 config2.getScreenOrientationQualifier(); 733 ScreenOrientation so2 = orientation2.getValue(); 734 if (so2 == null) { 735 so2 = ScreenOrientation.PORTRAIT; 736 } 737 738 if (so1 == ScreenOrientation.LANDSCAPE) { 739 if (so2 == ScreenOrientation.LANDSCAPE) { 740 return 0; 741 } else { 742 return -1; 743 } 744 } else if (so2 == ScreenOrientation.LANDSCAPE) { 745 return 1; 746 } else { 747 return 0; 748 } 749 } else { 750 return -1; 751 } 752 } else if (ss2 == ScreenSize.XLARGE) { 753 return 1; 754 } else { 755 return 0; 756 } 757 } 758 } 759 760 /** 761 * Note: this comparator imposes orderings that are inconsistent with equals. 762 */ 763 private static class PhoneConfigComparator implements Comparator<ConfigMatch> { 764 765 private final SparseIntArray mDensitySort = new SparseIntArray(4); 766 PhoneConfigComparator()767 public PhoneConfigComparator() { 768 // put the sort order for the density. 769 mDensitySort.put(Density.HIGH.getDpiValue(), 1); 770 mDensitySort.put(Density.MEDIUM.getDpiValue(), 2); 771 mDensitySort.put(Density.XHIGH.getDpiValue(), 3); 772 mDensitySort.put(Density.LOW.getDpiValue(), 4); 773 } 774 775 @Override compare(ConfigMatch o1, ConfigMatch o2)776 public int compare(ConfigMatch o1, ConfigMatch o2) { 777 FolderConfiguration config1 = o1 != null ? o1.testConfig : null; 778 FolderConfiguration config2 = o2 != null ? o2.testConfig : null; 779 if (config1 == null) { 780 if (config2 == null) { 781 return 0; 782 } else { 783 return -1; 784 } 785 } else if (config2 == null) { 786 return 1; 787 } 788 789 int dpi1 = Density.DEFAULT_DENSITY; 790 int dpi2 = Density.DEFAULT_DENSITY; 791 792 DensityQualifier dpiQualifier1 = config1.getDensityQualifier(); 793 if (dpiQualifier1 != null) { 794 Density value = dpiQualifier1.getValue(); 795 dpi1 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY; 796 } 797 dpi1 = mDensitySort.get(dpi1, 100 /* valueIfKeyNotFound*/); 798 799 DensityQualifier dpiQualifier2 = config2.getDensityQualifier(); 800 if (dpiQualifier2 != null) { 801 Density value = dpiQualifier2.getValue(); 802 dpi2 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY; 803 } 804 dpi2 = mDensitySort.get(dpi2, 100 /* valueIfKeyNotFound*/); 805 806 if (dpi1 == dpi2) { 807 // portrait is better 808 ScreenOrientation so1 = ScreenOrientation.PORTRAIT; 809 ScreenOrientationQualifier orientationQualifier1 = 810 config1.getScreenOrientationQualifier(); 811 if (orientationQualifier1 != null) { 812 so1 = orientationQualifier1.getValue(); 813 if (so1 == null) { 814 so1 = ScreenOrientation.PORTRAIT; 815 } 816 } 817 ScreenOrientation so2 = ScreenOrientation.PORTRAIT; 818 ScreenOrientationQualifier orientationQualifier2 = 819 config2.getScreenOrientationQualifier(); 820 if (orientationQualifier2 != null) { 821 so2 = orientationQualifier2.getValue(); 822 if (so2 == null) { 823 so2 = ScreenOrientation.PORTRAIT; 824 } 825 } 826 827 if (so1 == ScreenOrientation.PORTRAIT) { 828 if (so2 == ScreenOrientation.PORTRAIT) { 829 return 0; 830 } else { 831 return -1; 832 } 833 } else if (so2 == ScreenOrientation.PORTRAIT) { 834 return 1; 835 } else { 836 return 0; 837 } 838 } 839 840 return dpi1 - dpi2; 841 } 842 } 843 } 844