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