blob: f089a4835859ebcfd1c426b3a2e4dfcf91ad3ca3 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.configurations;
import com.android.ide.common.resources.configuration.*;
import com.android.io.IAbstractFile;
import com.android.resources.*;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.devices.Device;
import com.android.sdklib.devices.State;
import com.android.tools.idea.rendering.AppResourceRepository;
import com.android.tools.idea.rendering.LocalResourceRepository;
import com.android.tools.idea.rendering.Locale;
import com.android.tools.idea.rendering.ResourceHelper;
import com.android.utils.SparseIntArray;
import com.google.common.collect.Maps;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.android.uipreview.VirtualFileWrapper;
import org.jetbrains.android.util.BufferingFileWrapper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
import static com.android.SdkConstants.FD_RES_LAYOUT;
/**
* Produces matches for configurations.
* <p/>
* See algorithm described here:
* http://developer.android.com/guide/topics/resources/providing-resources.html#BestMatch
* <p>
* This class was ported from ADT and could probably use a rewrite.
*/
public class ConfigurationMatcher {
private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.rendering.ConfigurationMatcher");
private final Configuration myConfiguration;
private final ConfigurationManager myManager;
private final VirtualFile myFile;
private final LocalResourceRepository myResources;
public ConfigurationMatcher(@NotNull Configuration configuration,
@Nullable LocalResourceRepository resources,
@Nullable VirtualFile file) {
myConfiguration = configuration;
myFile = file;
myResources = resources;
myManager = myConfiguration.getConfigurationManager();
}
// ---- Finding matching configurations ----
private static class ConfigBundle {
private final FolderConfiguration config;
private int localeIndex;
private int dockModeIndex;
private int nightModeIndex;
private ConfigBundle() {
config = new FolderConfiguration();
}
private ConfigBundle(ConfigBundle bundle) {
config = new FolderConfiguration();
config.set(bundle.config);
localeIndex = bundle.localeIndex;
dockModeIndex = bundle.dockModeIndex;
nightModeIndex = bundle.nightModeIndex;
}
@Override
public String toString() {
return config.getUniqueKey();
}
}
private static class ConfigMatch {
final FolderConfiguration testConfig;
final Device device;
final State state;
final ConfigBundle bundle;
public ConfigMatch(@NotNull FolderConfiguration testConfig,
@NotNull Device device,
@NotNull State state,
@NotNull ConfigBundle bundle) {
this.testConfig = testConfig;
this.device = device;
this.state = state;
this.bundle = bundle;
}
@Override
public String toString() {
return device.getDisplayName() + " - " + state.getName() + " - " + bundle;
}
}
/**
* Checks whether the current edited file is the best match for a given config.
* <p/>
* This tests against other versions of the same layout in the project.
* <p/>
* The given config must be compatible with the current edited file.
*
* @param config the config to test.
* @return true if the current edited file is the best match in the project for the
* given config.
*/
public boolean isCurrentFileBestMatchFor(@NotNull FolderConfiguration config) {
if (myResources != null && myFile != null) {
VirtualFile match = myResources.getMatchingFile(myFile, getResourceType(), config);
if (match != null) {
return myFile.equals(match);
}
else {
// if we stop here that means the current file is not even a match!
LOG.info("Current file is not a match for the given config.");
}
}
return false;
}
private ResourceType getResourceType() {
// We're usually using the ConfigurationMatcher for layouts, but support other types too
ResourceType type = ResourceType.LAYOUT;
VirtualFile parent = myFile.getParent();
if (parent != null) {
String parentName = parent.getName();
if (!parentName.startsWith(FD_RES_LAYOUT)) {
ResourceFolderType folderType = ResourceHelper.getFolderType(myFile);
if (folderType != null) {
List<ResourceType> related = FolderTypeRelationship.getRelatedResourceTypes(folderType);
if (!related.isEmpty()) {
type = related.get(0); // the primary type is always first
}
}
}
}
return type;
}
@Nullable
public static VirtualFile getVirtualFile(@NotNull IAbstractFile file) {
if (file instanceof VirtualFileWrapper) {
return ((VirtualFileWrapper)file).getFile();
} else if (file instanceof BufferingFileWrapper) {
BufferingFileWrapper wrapper = (BufferingFileWrapper)file;
File ioFile = wrapper.getFile();
return LocalFileSystem.getInstance().findFileByIoFile(ioFile);
} else {
LOG.warn("Unexpected type of match file: " + file.getClass().getName());
}
return null;
}
/**
* Returns a list of {@link VirtualFile} which best match the configuration.
*/
@NotNull
public List<VirtualFile> getBestFileMatches() {
if (myResources != null && myFile != null) {
FolderConfiguration config = myConfiguration.getFullConfig();
VersionQualifier prevQualifier = config.getVersionQualifier();
try {
config.setVersionQualifier(null);
return myResources.getMatchingFiles(myFile, getResourceType(), config);
}
finally {
config.setVersionQualifier(prevQualifier);
}
}
return Collections.emptyList();
}
/** Like {@link ConfigurationManager#getLocales()}, but ensures that the currently selected locale is first in the list */
@NotNull
public List<Locale> getPrioritizedLocales() {
List<Locale> projectLocales = myManager.getLocales();
List<Locale> locales = new ArrayList<Locale>(projectLocales.size() + 1); // Locale.ANY is not in getLocales() list
Locale current = myManager.getLocale();
locales.add(current);
for (Locale locale : projectLocales) {
if (!locale.equals(current)) {
locales.add(locale);
}
}
return locales;
}
/**
* Adapts the current device/config selection so that it's compatible with
* the configuration.
* <p/>
* If the current selection is compatible, nothing is changed.
* <p/>
* If it's not compatible, configs from the current devices are tested.
* <p/>
* If none are compatible, it reverts to
* {@link #findAndSetCompatibleConfig(boolean)}
*/
void adaptConfigSelection(boolean needBestMatch) {
// check the device config (ie sans locale)
boolean needConfigChange = true; // if still true, we need to find another config.
boolean currentConfigIsCompatible = false;
State selectedState = myConfiguration.getDeviceState();
FolderConfiguration editedConfig = myConfiguration.getEditedConfig();
Module module = myConfiguration.getModule();
if (selectedState != null) {
FolderConfiguration currentConfig = Configuration.getFolderConfig(module, selectedState, myConfiguration.getLocale(),
myConfiguration.getTarget());
if (currentConfig != null && editedConfig.isMatchFor(currentConfig)) {
currentConfigIsCompatible = true; // current config is compatible
if (!needBestMatch || isCurrentFileBestMatchFor(currentConfig)) {
needConfigChange = false;
}
}
}
if (needConfigChange) {
List<Locale> localeList = getPrioritizedLocales();
// if the current state/locale isn't a correct match, then
// look for another state/locale in the same device.
FolderConfiguration testConfig = new FolderConfiguration();
// first look in the current device.
State matchState = null;
int localeIndex = -1;
Device device = myConfiguration.getDevice();
IAndroidTarget target = myConfiguration.getTarget();
if (device != null && target != null) {
VersionQualifier versionQualifier = new VersionQualifier(target.getVersion().getFeatureLevel());
mainloop:
for (State state : device.getAllStates()) {
testConfig.set(Configuration.getFolderConfig(module, state, myConfiguration.getLocale(), target));
testConfig.setVersionQualifier(versionQualifier);
// loop on the locales.
for (int i = 0; i < localeList.size(); i++) {
Locale locale = localeList.get(i);
// update the test config with the locale qualifiers
testConfig.setLocaleQualifier(locale.qualifier);
if (editedConfig.isMatchFor(testConfig) && isCurrentFileBestMatchFor(testConfig)) {
matchState = state;
localeIndex = i;
break mainloop;
}
}
}
}
if (matchState != null) {
myConfiguration.startBulkEditing();
myConfiguration.setDeviceState(matchState);
myConfiguration.setEffectiveDevice(device, matchState);
myConfiguration.finishBulkEditing();
}
else {
// no match in current device with any state/locale
// attempt to find another device that can display this
// particular state.
findAndSetCompatibleConfig(currentConfigIsCompatible);
}
}
}
/**
* Finds a device/config that can display a configuration.
* <p/>
* Once found the device and config combos are set to the config.
* <p/>
* If there is no compatible configuration, a custom one is created.
*
* @param favorCurrentConfig if true, and no best match is found, don't
* change the current config. This must only be true if the
* current config is compatible.
*/
void findAndSetCompatibleConfig(boolean favorCurrentConfig) {
List<Locale> localeList = getPrioritizedLocales();
List<Device> deviceList = myManager.getDevices();
FolderConfiguration editedConfig = myConfiguration.getEditedConfig();
FolderConfiguration currentConfig = myConfiguration.getFullConfig();
// list of compatible device/state/locale
List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>();
// list of actual best match (ie the file is a best match for the
// device/state)
List<ConfigMatch> bestMatches = new ArrayList<ConfigMatch>();
// get a locale that matches the host locale roughly (may not be exact match on the region.)
int localeHostMatch = getLocaleMatch();
// build a list of combinations of non standard qualifiers to add to each device's
// qualifier set when testing for a match.
// These qualifiers are: locale, night-mode, car dock.
List<ConfigBundle> configBundles = new ArrayList<ConfigBundle>(200);
// If the edited file has locales, then we have to select a matching locale from
// the list.
// However, if it doesn't, we don't randomly take the first locale, we take one
// matching the current host locale (making sure it actually exist in the project)
int start, max;
if (editedConfig.getLocaleQualifier() != null || localeHostMatch == -1) {
// add all the locales
start = 0;
max = localeList.size();
}
else {
// only add the locale host match
start = localeHostMatch;
max = localeHostMatch + 1; // test is <
}
for (int i = start; i < max; i++) {
Locale l = localeList.get(i);
ConfigBundle bundle = new ConfigBundle();
bundle.config.setLocaleQualifier(l.qualifier);
bundle.localeIndex = i;
configBundles.add(bundle);
}
// add the dock mode to the bundle combinations.
addDockModeToBundles(configBundles);
// add the night mode to the bundle combinations.
addNightModeToBundles(configBundles);
addRenderTargetToBundles(configBundles);
Locale currentLocale = myConfiguration.getLocale();
IAndroidTarget currentTarget = myConfiguration.getTarget();
Module module = myConfiguration.getModule();
for (Device device : deviceList) {
for (State state : device.getAllStates()) {
// loop on the list of config bundles to create full
// configurations.
FolderConfiguration stateConfig = Configuration.getFolderConfig(module, state, currentLocale, currentTarget);
for (ConfigBundle bundle : configBundles) {
// create a new config with device config
FolderConfiguration testConfig = new FolderConfiguration();
testConfig.set(stateConfig);
// add on top of it, the extra qualifiers from the bundle
testConfig.add(bundle.config);
if (editedConfig.isMatchFor(testConfig)) {
// this is a basic match. record it in case we don't
// find a match
// where the edited file is a best config.
anyMatches.add(new ConfigMatch(testConfig, device, state, bundle));
if (isCurrentFileBestMatchFor(testConfig)) {
// this is what we want.
bestMatches.add(new ConfigMatch(testConfig, device, state, bundle));
}
}
}
}
}
if (bestMatches.size() == 0) {
if (favorCurrentConfig) {
// quick check
if (!editedConfig.isMatchFor(currentConfig)) {
LOG.warn("favorCurrentConfig can only be true if the current config is compatible");
}
// just display the warning
LOG.warn(String.format("'%1$s' is not a best match for any device/locale combination for %2$s.\n" +
"Displaying it with '%3$s'.",
editedConfig.toDisplayString(), myConfiguration.getFile(), currentConfig.toDisplayString()));
}
else if (anyMatches.size() > 0) {
// select the best device anyway.
ConfigMatch match = selectConfigMatch(anyMatches);
myConfiguration.startBulkEditing();
myConfiguration.setEffectiveDevice(match.device, match.state);
myConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex));
myConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex));
myConfiguration.finishBulkEditing();
// TODO: display a better warning!
LOG.warn(String.format("'%1$s' is not a best match for any device/locale combination for %2$s.\n" +
"Displaying it with\n" +
" %3$s\n" +
"which is compatible, but will actually be displayed with " +
"another more specific version of the layout.", editedConfig.toDisplayString(),
myConfiguration.getFile(), currentConfig.toDisplayString()));
}
else {
// TODO: there is no device/config able to display the layout, create one.
// For the base config values, we'll take the first device and state,
// and replace whatever qualifier required by the layout file.
}
}
else {
ConfigMatch match = selectConfigMatch(bestMatches);
myConfiguration.startBulkEditing();
myConfiguration.setEffectiveDevice(match.device, match.state);
myConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex));
myConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex));
myConfiguration.finishBulkEditing();
}
}
private void addRenderTargetToBundles(List<ConfigBundle> configBundles) {
IAndroidTarget target = myManager.getTarget();
if (target != null) {
int apiLevel = target.getVersion().getFeatureLevel();
for (ConfigBundle bundle : configBundles) {
bundle.config.setVersionQualifier(new VersionQualifier(apiLevel));
}
}
}
private static void addDockModeToBundles(List<ConfigBundle> addConfig) {
ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
// loop on each item and for each, add all variations of the dock modes
for (ConfigBundle bundle : addConfig) {
int index = 0;
for (UiMode mode : UiMode.values()) {
ConfigBundle b = new ConfigBundle(bundle);
b.config.setUiModeQualifier(new UiModeQualifier(mode));
b.dockModeIndex = index++;
list.add(b);
}
}
addConfig.clear();
addConfig.addAll(list);
}
private static void addNightModeToBundles(List<ConfigBundle> addConfig) {
ArrayList<ConfigBundle> list = new ArrayList<ConfigBundle>();
// loop on each item and for each, add all variations of the night modes
for (ConfigBundle bundle : addConfig) {
int index = 0;
for (NightMode mode : NightMode.values()) {
ConfigBundle b = new ConfigBundle(bundle);
b.config.setNightModeQualifier(new NightModeQualifier(mode));
b.nightModeIndex = index++;
list.add(b);
}
}
addConfig.clear();
addConfig.addAll(list);
}
private int getLocaleMatch() {
java.util.Locale defaultLocale = java.util.Locale.getDefault();
if (defaultLocale != null) {
String currentLanguage = defaultLocale.getLanguage();
String currentRegion = defaultLocale.getCountry();
List<Locale> localeList = myManager.getLocales();
final int count = localeList.size();
for (int l = 0; l < count; l++) {
Locale locale = localeList.get(l);
LocaleQualifier qualifier = locale.qualifier;
// there's always a ##/Other or ##/Any (which is the same, the region
// contains FAKE_REGION_VALUE). If we don't find a perfect region match
// we take the fake region. Since it's last in the list, this makes the
// test easy.
if (qualifier.getLanguage().equals(currentLanguage) &&
(qualifier.getRegion() == null || qualifier.getRegion().equals(currentRegion))) {
return l;
}
}
// If no exact region match, try to just match on the language
for (int l = 0; l < count; l++) {
Locale locale = localeList.get(l);
LocaleQualifier qualifier = locale.qualifier;
// there's always a ##/Other or ##/Any (which is the same, the region
// contains FAKE_REGION_VALUE). If we don't find a perfect region match
// we take the fake region. Since it's last in the list, this makes the
// test easy.
if (qualifier.getLanguage().equals(currentLanguage)) {
return l;
}
}
}
// Nothing found: use the first one (which should be the current locale); see
// getPrioritizedLocales()
return 0;
}
@NotNull
private ConfigMatch selectConfigMatch(@NotNull List<ConfigMatch> matches) {
List<String> deviceIds = myManager.getStateManager().getProjectState().getDeviceIds();
Map<String, Integer> idRank = Maps.newHashMapWithExpectedSize(deviceIds.size());
int rank = 0;
for (String id : deviceIds) {
idRank.put(id, rank++);
}
// API 11-13: look for a x-large device
Comparator<ConfigMatch> comparator = null;
IAndroidTarget projectTarget = myManager.getProjectTarget();
if (projectTarget != null) {
int apiLevel = projectTarget.getVersion().getFeatureLevel();
if (apiLevel >= 11 && apiLevel < 14) {
// TODO: Maybe check the compatible-screen tag in the manifest to figure out
// what kind of device should be used for display.
comparator = new TabletConfigComparator(idRank);
}
}
if (comparator == null) {
// lets look for a high density device
comparator = new PhoneConfigComparator(idRank);
}
Collections.sort(matches, comparator);
// Look at the currently active editor to see if it's a layout editor, and if so,
// look up its configuration and if the configuration is in our match list,
// use it. This means we "preserve" the current configuration when you open
// new layouts.
// TODO: This is running too late for the layout preview; the new editor has
// already taken over so getSelectedTextEditor() returns self. Perhaps we
// need to fish in the open editors instead.
//Editor activeEditor = ApplicationManager.getApplication().runReadAction(new Computable<Editor>() {
// @Override
// public Editor compute() {
// FileEditorManager editorManager = FileEditorManager.getInstance(myManager.getProject());
// return editorManager.getSelectedTextEditor();
// }
//});
// TODO: How do I redispatch without risking lock?
//Editor activeEditor = AndroidUtils.getSelectedEditor(myManager.getProject());
if (ApplicationManager.getApplication().isDispatchThread()) {
FileEditorManager editorManager = FileEditorManager.getInstance(myManager.getProject());
Editor activeEditor = editorManager.getSelectedTextEditor();
if (activeEditor != null) {
FileDocumentManager documentManager = FileDocumentManager.getInstance();
VirtualFile file = documentManager.getFile(activeEditor.getDocument());
if (file != null && !file.equals(myFile) && file.getFileType() == StdFileTypes.XML
&& ResourceHelper.getFolderType(myFile) == ResourceHelper.getFolderType(file)) {
Configuration configuration = myManager.getConfiguration(file);
FolderConfiguration fullConfig = configuration.getFullConfig();
for (ConfigMatch match : matches) {
if (fullConfig.equals(match.testConfig)) {
return match;
}
}
}
}
}
// the list has been sorted so that the first item is the best config
return matches.get(0);
}
/**
* Returns a different file which is a better match for the given device, orientation, target version, etc
* than the current one. You can supply {@code null} for all parameters; in that case, the current value
* in the configuration is used.
*/
@Nullable
public static VirtualFile getBetterMatch(@NotNull Configuration configuration, @Nullable Device device, @Nullable String stateName,
@Nullable Locale locale, @Nullable IAndroidTarget target) {
VirtualFile file = configuration.getFile();
Module module = configuration.getModule();
if (file != null && module != null) {
if (device == null) {
device = configuration.getDevice();
}
if (stateName == null) {
State deviceState = configuration.getDeviceState();
stateName = deviceState != null ? deviceState.getName() : null;
}
State selectedState = ConfigurationFileState.getState(device, stateName);
if (selectedState == null) {
return null; // Invalid state name passed in for the current device
}
if (locale == null) {
locale = configuration.getLocale();
}
if (target == null) {
target = configuration.getTarget();
}
FolderConfiguration currentConfig = Configuration.getFolderConfig(module, selectedState, locale, target);
if (currentConfig != null) {
LocalResourceRepository resources = AppResourceRepository.getAppResources(module, true);
if (resources != null) {
ResourceFolderType folderType = ResourceHelper.getFolderType(file);
if (folderType != null) {
List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folderType);
if (!types.isEmpty()) {
ResourceType type = types.get(0);
List<VirtualFile> matches = resources.getMatchingFiles(file, type, currentConfig);
if (!matches.contains(file) && !matches.isEmpty()) {
return matches.get(0);
}
}
}
}
}
}
return null;
}
/**
* Note: this comparator imposes orderings that are inconsistent with equals.
*/
private static class TabletConfigComparator implements Comparator<ConfigMatch> {
private final Map<String, Integer> mIdRank;
private static final String PREFERRED_ID = "Nexus 10";
private TabletConfigComparator(Map<String, Integer> idRank) {
mIdRank = idRank;
}
@Override
public int compare(ConfigMatch o1, ConfigMatch o2) {
FolderConfiguration config1 = o1 != null ? o1.testConfig : null;
FolderConfiguration config2 = o2 != null ? o2.testConfig : null;
if (config1 == null) {
if (config2 == null) {
return 0;
}
else {
return -1;
}
}
else if (config2 == null) {
return 1;
}
String n1 = o1.device.getId();
String n2 = o2.device.getId();
Integer rank1 = mIdRank.get(o1.device.getId());
Integer rank2 = mIdRank.get(o2.device.getId());
if (rank1 != null) {
if (rank2 != null) {
int delta = rank1 - rank2;
if (delta != 0) {
return delta;
}
} else {
return -1;
}
} else if (rank2 != null) {
return 1;
}
// Default to a modern device
if (n1.equals(PREFERRED_ID)) {
return n2.equals(PREFERRED_ID) ? 0 : -1;
} else if (n2.equals(PREFERRED_ID)) {
return 1;
}
ScreenSizeQualifier size1 = config1.getScreenSizeQualifier();
ScreenSizeQualifier size2 = config2.getScreenSizeQualifier();
ScreenSize ss1 = size1 != null ? size1.getValue() : ScreenSize.NORMAL;
ScreenSize ss2 = size2 != null ? size2.getValue() : ScreenSize.NORMAL;
// X-LARGE is better than all others (which are considered identical)
// if both X-LARGE, then LANDSCAPE is better than all others (which are identical)
if (ss1 == ScreenSize.XLARGE) {
if (ss2 == ScreenSize.XLARGE) {
ScreenOrientationQualifier orientation1 = config1.getScreenOrientationQualifier();
ScreenOrientation so1 = orientation1.getValue();
if (so1 == null) {
so1 = ScreenOrientation.PORTRAIT;
}
ScreenOrientationQualifier orientation2 = config2.getScreenOrientationQualifier();
ScreenOrientation so2 = orientation2.getValue();
if (so2 == null) {
so2 = ScreenOrientation.PORTRAIT;
}
if (so1 == ScreenOrientation.LANDSCAPE) {
if (so2 == ScreenOrientation.LANDSCAPE) {
return 0;
}
else {
return -1;
}
}
else if (so2 == ScreenOrientation.LANDSCAPE) {
return 1;
}
else {
return 0;
}
}
else {
return -1;
}
}
else if (ss2 == ScreenSize.XLARGE) {
return 1;
}
else {
return 0;
}
}
}
/**
* Note: this comparator imposes orderings that are inconsistent with equals.
*/
private static class PhoneConfigComparator implements Comparator<ConfigMatch> {
// Default phone. Not picking the Nexus 5 yet since it has much higher resolution
// (which will on a typical desktop rendering of the layout be scaled back down again anyway)
// so it's just extra work.
private static final String PREFERRED_ID = "Nexus 4";
private final SparseIntArray mDensitySort = new SparseIntArray(4);
private final Map<String, Integer> mIdRank;
public PhoneConfigComparator(Map<String, Integer> idRank) {
int i = 0;
mDensitySort.put(Density.HIGH.getDpiValue(), ++i);
mDensitySort.put(Density.MEDIUM.getDpiValue(), ++i);
mDensitySort.put(Density.XHIGH.getDpiValue(), ++i);
mDensitySort.put(Density.DPI_400.getDpiValue(), ++i);
mDensitySort.put(Density.XXHIGH.getDpiValue(), ++i);
mDensitySort.put(Density.DPI_560.getDpiValue(), ++i);
mDensitySort.put(Density.XXXHIGH.getDpiValue(), ++i);
mDensitySort.put(Density.DPI_360.getDpiValue(), ++i);
mDensitySort.put(Density.DPI_280.getDpiValue(), ++i);
mDensitySort.put(Density.TV.getDpiValue(), ++i);
mDensitySort.put(Density.LOW.getDpiValue(), ++i);
mIdRank = idRank;
}
@Override
public int compare(ConfigMatch o1, ConfigMatch o2) {
FolderConfiguration config1 = o1 != null ? o1.testConfig : null;
FolderConfiguration config2 = o2 != null ? o2.testConfig : null;
if (config1 == null) {
if (config2 == null) {
return 0;
}
else {
return -1;
}
}
else if (config2 == null) {
return 1;
}
String n1 = o1.device.getId();
String n2 = o2.device.getId();
Integer rank1 = mIdRank.get(o1.device.getId());
Integer rank2 = mIdRank.get(o2.device.getId());
if (rank1 != null) {
if (rank2 != null) {
int delta = rank1 - rank2;
if (delta != 0) {
return delta;
}
} else {
return -1;
}
} else if (rank2 != null) {
return 1;
}
// Default to a modern device
if (n1.equals(PREFERRED_ID)) {
return n2.equals(PREFERRED_ID) ? 0 : -1;
} else if (n2.equals(PREFERRED_ID)) {
return 1;
}
int dpi1 = Density.DEFAULT_DENSITY;
int dpi2 = Density.DEFAULT_DENSITY;
DensityQualifier dpiQualifier1 = config1.getDensityQualifier();
if (dpiQualifier1 != null) {
Density value = dpiQualifier1.getValue();
dpi1 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY;
}
dpi1 = mDensitySort.get(dpi1, 100 /* valueIfKeyNotFound*/);
DensityQualifier dpiQualifier2 = config2.getDensityQualifier();
if (dpiQualifier2 != null) {
Density value = dpiQualifier2.getValue();
dpi2 = value != null ? value.getDpiValue() : Density.DEFAULT_DENSITY;
}
dpi2 = mDensitySort.get(dpi2, 100 /* valueIfKeyNotFound*/);
if (dpi1 == dpi2) {
// portrait is better
ScreenOrientation so1 = ScreenOrientation.PORTRAIT;
ScreenOrientationQualifier orientationQualifier1 = config1.getScreenOrientationQualifier();
if (orientationQualifier1 != null) {
so1 = orientationQualifier1.getValue();
if (so1 == null) {
so1 = ScreenOrientation.PORTRAIT;
}
}
ScreenOrientation so2 = ScreenOrientation.PORTRAIT;
ScreenOrientationQualifier orientationQualifier2 = config2.getScreenOrientationQualifier();
if (orientationQualifier2 != null) {
so2 = orientationQualifier2.getValue();
if (so2 == null) {
so2 = ScreenOrientation.PORTRAIT;
}
}
if (so1 == ScreenOrientation.PORTRAIT) {
if (so2 == ScreenOrientation.PORTRAIT) {
return 0;
}
else {
return -1;
}
}
else if (so2 == ScreenOrientation.PORTRAIT) {
return 1;
}
else {
return 0;
}
}
return dpi1 - dpi2;
}
}
}