blob: e3839dac62c344acd96a6339e3b48f77c67ab33c [file] [log] [blame]
/*
* Copyright (C) 2015 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.editors.theme.qualifiers;
import com.android.ide.common.resources.configuration.DensityQualifier;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.KeyboardStateQualifier;
import com.android.ide.common.resources.configuration.LayoutDirectionQualifier;
import com.android.ide.common.resources.configuration.LocaleQualifier;
import com.android.ide.common.resources.configuration.NavigationMethodQualifier;
import com.android.ide.common.resources.configuration.NavigationStateQualifier;
import com.android.ide.common.resources.configuration.NightModeQualifier;
import com.android.ide.common.resources.configuration.ResourceQualifier;
import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
import com.android.ide.common.resources.configuration.ScreenRatioQualifier;
import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
import com.android.ide.common.resources.configuration.TextInputMethodQualifier;
import com.android.ide.common.resources.configuration.TouchScreenQualifier;
import com.android.ide.common.resources.configuration.UiModeQualifier;
import com.android.ide.common.resources.configuration.VersionQualifier;
import com.android.resources.Density;
import com.android.resources.Keyboard;
import com.android.resources.KeyboardState;
import com.android.resources.LayoutDirection;
import com.android.resources.Navigation;
import com.android.resources.NavigationState;
import com.android.resources.NightMode;
import com.android.resources.ScreenOrientation;
import com.android.resources.ScreenRatio;
import com.android.resources.ScreenSize;
import com.android.resources.TouchScreen;
import com.android.resources.UiMode;
import com.android.tools.idea.configurations.ConfigurationManager;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
public class QualifierUtils {
private static final Logger LOG = Logger.getInstance(QualifierUtils.class);
/**
* This map contains all the enum qualifiers that can be automatically processed to get an incompatible configuration. For these qualifiers
* it's just enough to take one of the values that it's not the original value from the configuration we are using.
* DensityQualifier is excluded from this class because it is evaluated in a different way and simply getting a different density is
* not enough to make a configuration "incompatible".
*/
static final ImmutableMap<Class<? extends ResourceQualifier>, Class<? extends Enum>> ENUM_QUALIFIERS_MAPPING =
ImmutableMap.<Class<? extends ResourceQualifier>, Class<? extends Enum>>builder()
.put(NightModeQualifier.class, NightMode.class)
.put(KeyboardStateQualifier.class, KeyboardState.class)
.put(ScreenSizeQualifier.class, ScreenSize.class)
.put(NavigationStateQualifier.class, NavigationState.class)
.put(ScreenOrientationQualifier.class, ScreenOrientation.class)
.put(LayoutDirectionQualifier.class, LayoutDirection.class)
.put(TouchScreenQualifier.class, TouchScreen.class)
.put(NavigationMethodQualifier.class, Navigation.class)
.put(UiModeQualifier.class, UiMode.class)
.put(TextInputMethodQualifier.class, Keyboard.class)
.put(ScreenRatioQualifier.class, ScreenRatio.class)
.build();
/**
* Returns one enum value that it's not present in the passed set or null if all the values are contained in the set.
*/
@Nullable
static <T extends Enum<T>> T findIncompatibleEnumValue(EnumSet<T> currentValues) {
EnumSet<T> complement = EnumSet.complementOf(currentValues);
return Iterables.getFirst(complement, null);
}
static <E extends Enum<E>, Q extends ResourceQualifier> ResourceQualifier getIncompatibleEnum(Class<E> enumType, Class<Q> resourceQualifierType, Collection<ResourceQualifier> resourceQualifiers) {
List<E> currentValues = Lists.newArrayList();
// TODO: Remove the use of reflection here by improving the Qualifiers hierarchy
Method getValueMethod;
try {
getValueMethod = resourceQualifierType.getMethod("getValue");
}
catch (NoSuchMethodException e) {
LOG.error("getValue method not found on the qualifier type (not an enum qualifier)", e);
return null;
}
for (ResourceQualifier qualifier : resourceQualifiers) {
// Type check to make sure the passed class type matches the passed resources
if (qualifier.getClass() != resourceQualifierType) {
LOG.error(String.format("The passed list of qualifiers of type '$1%s' doesn't match the passed type '$2%s",
qualifier.getClass().getSimpleName(),
resourceQualifierType.getSimpleName()));
return null;
}
//noinspection unchecked
Q enumQualifier = (Q)qualifier;
try {
//noinspection unchecked
currentValues.add((E)getValueMethod.invoke(enumQualifier));
}
catch (IllegalAccessException e) {
LOG.error("getValue method is not public", e);
}
catch (InvocationTargetException e) {
LOG.error("InvocationTargetException", e);
}
}
E incompatibleEnumValue = findIncompatibleEnumValue(EnumSet.copyOf(currentValues));
if (incompatibleEnumValue == null) {
// None found
return null;
}
try {
Constructor<Q> constructor = resourceQualifierType.getConstructor(enumType);
return constructor.newInstance(incompatibleEnumValue);
}
catch (NoSuchMethodException e) {
LOG.error("The qualifier type does not have a constructor with the passed enum type", e);
}
catch (InvocationTargetException e) {
LOG.error("Error calling qualifier constructor", e);
}
catch (InstantiationException e) {
LOG.error("Error calling qualifier constructor", e);
}
catch (IllegalAccessException e) {
LOG.error("Error calling qualifier constructor", e);
}
return null;
}
/**
* Returns a version qualifier that is lower than any of the version qualifiers found in the passed list.
* @param maxInstalledApiLevel the maximum available API level. This method can not return a version that is higher than this.
* @param qualifiers the list of version qualifiers.
*/
static ResourceQualifier getIncompatibleVersionQualifier(int maxInstalledApiLevel, @NotNull Collection<ResourceQualifier> qualifiers) {
// We start at maxInstalledApiLevel + 1 because this method always return one version lower than the API level found.
int minApiLevel = maxInstalledApiLevel + 1;
for (ResourceQualifier qualifier : qualifiers) {
VersionQualifier versionQualifier = (VersionQualifier)qualifier;
minApiLevel = Math.min(versionQualifier.getVersion(), minApiLevel);
}
return new VersionQualifier(minApiLevel - 1);
}
static ResourceQualifier getIncompatibleDensityQualifier(@NotNull Collection<ResourceQualifier> qualifiers) {
Density minDensity = null;
for (ResourceQualifier qualifier : qualifiers) {
DensityQualifier densityQualifier = (DensityQualifier)qualifier;
Density value = densityQualifier.getValue();
if (minDensity == null || value.getDpiValue() < minDensity.getDpiValue()) {
minDensity = value;
}
}
// There is nothing lower than NODPI
if (minDensity == null || minDensity == Density.NODPI) {
return null;
}
// Now select the next lower density to the minimum we've found
Density lowerDensity = Density.NODPI;
for (Density value : Density.values()) {
if (value.getDpiValue() > lowerDensity.getDpiValue() && value.getDpiValue() < minDensity.getDpiValue()) {
lowerDensity = value;
}
}
return new DensityQualifier(lowerDensity);
}
/**
* Returns a ResourceQualifier that doesn't match any of the passed qualifiers. If there are no incompatible qualifiers, then this method
* returns null.
*/
public static ResourceQualifier getIncompatibleQualifier(@NotNull ConfigurationManager configurationManager,
@NotNull Class<? extends ResourceQualifier> type,
@NotNull Collection<ResourceQualifier> qualifiers) {
if (type == VersionQualifier.class) {
if (configurationManager.getHighestApiTarget() == null) {
return null;
}
int maxApi = configurationManager.getHighestApiTarget().getVersion().getApiLevel();
return getIncompatibleVersionQualifier(maxApi, qualifiers);
} else if (type == LocaleQualifier.class) {
// The FAKE_VALUE doesn't match any real locales
return new LocaleQualifier(LocaleQualifier.FAKE_VALUE);
} else if (type == DensityQualifier.class) {
return getIncompatibleDensityQualifier(qualifiers);
}
if (ENUM_QUALIFIERS_MAPPING.containsKey(type)) {
Class<? extends Enum> enumType = ENUM_QUALIFIERS_MAPPING.get(type);
return getIncompatibleEnum(enumType, type, qualifiers);
}
return null;
}
/**
* Finds a configuration that matches with compatible and doesn't match with any of the incompatible ones.
*/
public static FolderConfiguration restrictConfiguration(@NotNull ConfigurationManager configurationManager,
@NotNull FolderConfiguration compatible,
@NotNull Collection<FolderConfiguration> incompatible) {
FolderConfiguration finalConfiguration = FolderConfiguration.copyOf(compatible);
if (incompatible.isEmpty()) {
return finalConfiguration;
}
// Sort qualifiers based on their type
Multimap<Class<? extends ResourceQualifier>, ResourceQualifier> qualifiers = HashMultimap.create();
for (FolderConfiguration incompatibleConfiguration : incompatible) {
if (incompatibleConfiguration == null) {
continue;
}
for (ResourceQualifier qualifier : incompatibleConfiguration.getQualifiers()) {
if (qualifier != null) {
qualifiers.put(qualifier.getClass(), qualifier);
}
}
}
HashSet<String> existingQualifiers = Sets.newHashSetWithExpectedSize(finalConfiguration.getQualifiers().length);
for (ResourceQualifier existingQualifier : finalConfiguration.getQualifiers()) {
existingQualifiers.add(existingQualifier.getName());
}
for (Class<? extends ResourceQualifier> qualifier : qualifiers.keySet()) {
ResourceQualifier incompatibleQualifier = getIncompatibleQualifier(configurationManager, qualifier, qualifiers.get(qualifier));
if (incompatibleQualifier == null) {
return null;
}
if (!existingQualifiers.contains(incompatibleQualifier.getName())) {
finalConfiguration.addQualifier(incompatibleQualifier);
}
}
return finalConfiguration;
}
}