blob: d99a5ec0794a0c99ff9d011187e8359bd1ee7fee [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.annotations.VisibleForTesting;
import com.android.ide.common.rendering.api.Bridge;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.LocaleQualifier;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkManager;
import com.android.sdklib.devices.Device;
import com.android.sdklib.devices.DeviceManager;
import com.android.sdklib.internal.androidTarget.PlatformTarget;
import com.android.sdklib.internal.avd.AvdInfo;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.model.ManifestInfo;
import com.android.tools.idea.model.ManifestInfo.ActivityAttributes;
import com.android.tools.idea.rendering.*;
import com.android.tools.idea.rendering.Locale;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.SoftValueHashMap;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.android.sdk.AndroidTargetData;
import org.jetbrains.android.sdk.MessageBuildingSdkLog;
import org.jetbrains.android.uipreview.UserDeviceManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
import static com.android.tools.idea.configurations.ConfigurationListener.*;
/**
* A {@linkplain ConfigurationManager} is responsible for managing {@link Configuration}
* objects for a given project.
* <p>
* Whereas a {@link Configuration} is tied to a specific render target or theme,
* the {@linkplain ConfigurationManager} knows the set of available targets, themes,
* locales etc. for the current project.
* <p>
* The {@linkplain ConfigurationManager} is also responsible for storing and retrieving
* the saved configuration state for a given file.
*/
public class ConfigurationManager implements Disposable {
@NotNull private final Module myModule;
private List<Device> myDevices;
private Map<String,Device> myDeviceMap;
private final UserDeviceManager myUserDeviceManager;
private final SoftValueHashMap<VirtualFile, Configuration> myCache = new SoftValueHashMap<VirtualFile, Configuration>();
private List<Locale> myLocales;
private Device myDefaultDevice;
private Locale myLocale;
private IAndroidTarget myTarget;
private int myStateVersion;
private ResourceResolverCache myResolverCache;
private long myLocaleCacheStamp;
private ConfigurationManager(@NotNull Module module) {
myModule = module;
myUserDeviceManager = new UserDeviceManager() {
@Override
protected void userDevicesChanged() {
// Force refresh
myDevices = null;
myDeviceMap = null;
// TODO: How do I trigger changes in the UI?
}
};
Disposer.register(this, myUserDeviceManager);
}
/**
* Gets the {@link Configuration} associated with the given file
* @return the {@link Configuration} for the given file
*/
@NotNull
public Configuration getConfiguration(@NotNull VirtualFile file) {
Configuration configuration = myCache.get(file);
if (configuration == null) {
configuration = create(file);
myCache.put(file, configuration);
}
return configuration;
}
@VisibleForTesting
boolean hasCachedConfiguration(@NotNull VirtualFile file) {
return myCache.get(file) != null;
}
/**
* Creates a new {@link Configuration} associated with this manager
* @return a new {@link Configuration}
*/
@NotNull
private Configuration create(@NotNull VirtualFile file) {
ConfigurationStateManager stateManager = getStateManager();
ConfigurationFileState fileState = stateManager.getConfigurationState(file);
assert file.getParent() != null : file;
FolderConfiguration config = FolderConfiguration.getConfigForFolder(file.getParent().getName());
if (config == null) {
config = new FolderConfiguration();
}
Configuration configuration = Configuration.create(this, file, fileState, config);
LocalResourceRepository resources = AppResourceRepository.getAppResources(myModule, true);
ConfigurationMatcher matcher = new ConfigurationMatcher(configuration, resources, file);
if (fileState != null) {
matcher.adaptConfigSelection(true);
} else {
matcher.findAndSetCompatibleConfig(false);
}
return configuration;
}
/**
* Similar to {@link #getConfiguration(VirtualFile)}, but creates a configuration
* for a file known to be new, and crucially, bases the configuration on the existing configuration
* for a known file. This is intended for when you fork a layout, and you expect the forked layout
* to have a configuration that is (as much as possible) similar to the configuration of the
* forked file. For example, if you create a landscape version of a layout, it will preserve the
* screen size, locale, theme and render target of the existing layout.
*
* @param file the file to create a configuration for
* @param baseFile the other file to base the configuration on
* @return the new configuration
*/
@NotNull
public Configuration createSimilar(@NotNull VirtualFile file, @NotNull VirtualFile baseFile) {
ConfigurationStateManager stateManager = getStateManager();
ConfigurationFileState fileState = stateManager.getConfigurationState(baseFile);
FolderConfiguration config = FolderConfiguration.getConfigForFolder(file.getParent().getName());
if (config == null) {
config = new FolderConfiguration();
}
Configuration configuration = Configuration.create(this, file, fileState, config);
Configuration baseConfig = myCache.get(file);
if (baseConfig != null) {
configuration.setEffectiveDevice(baseConfig.getDevice(), baseConfig.getDeviceState());
}
LocalResourceRepository resources = AppResourceRepository.getAppResources(myModule, true);
ConfigurationMatcher matcher = new ConfigurationMatcher(configuration, resources, file);
matcher.adaptConfigSelection(true /*needBestMatch*/);
myCache.put(file, configuration);
return configuration;
}
/** Returns the associated persistence manager */
public ConfigurationStateManager getStateManager() {
return ConfigurationStateManager.get(myModule.getProject());
}
/**
* Creates a new {@link ConfigurationManager} for the given module
*
* @param module the associated module
* @return a new {@link ConfigurationManager}
*/
@NotNull
public static ConfigurationManager create(@NotNull Module module) {
return new ConfigurationManager(module);
}
/** Returns the list of available devices for the current platform, if any */
@NotNull
public List<Device> getDevices() {
if (myDevices == null || myDevices.isEmpty()) {
List<Device> devices = null;
AndroidPlatform platform = AndroidPlatform.getInstance(myModule);
if (platform != null) {
final AndroidSdkData sdkData = platform.getSdkData();
devices = new ArrayList<Device>();
DeviceManager deviceManager = sdkData.getDeviceManager();
devices.addAll(deviceManager.getDevices(EnumSet.of(DeviceManager.DeviceFilter.DEFAULT, DeviceManager.DeviceFilter.VENDOR)));
devices.addAll(myUserDeviceManager.parseUserDevices(new MessageBuildingSdkLog()));
}
if (devices == null) {
myDevices = Collections.emptyList();
} else {
myDevices = devices;
}
}
return myDevices;
}
@NotNull
private Map<String,Device> getDeviceMap() {
if (myDeviceMap == null) {
List<Device> devices = getDevices();
myDeviceMap = Maps.newHashMapWithExpectedSize(devices.size());
for (Device device : devices) {
myDeviceMap.put(device.getId(), device);
}
}
return myDeviceMap;
}
@Nullable
public Device getDeviceById(@NotNull String id) {
return getDeviceMap().get(id);
}
@Nullable
public Device createDeviceForAvd(@NotNull AvdInfo avd) {
AndroidFacet facet = AndroidFacet.getInstance(myModule);
assert facet != null;
for (Device device : getDevices()) {
if (device.getManufacturer().equals(avd.getDeviceManufacturer())
&& (device.getId().equals(avd.getDeviceName()) || device.getDisplayName().equals(avd.getDeviceName()))) {
String avdName = avd.getName();
Device.Builder builder = new Device.Builder(device);
builder.setName(avdName);
return builder.build();
}
}
return null;
}
/**
* Returns all the {@link IAndroidTarget} instances applicable for the current module.
* Note that this may include non-rendering targets, so for layout rendering contexts,
* check individual members by calling {@link #isLayoutLibTarget(IAndroidTarget)} first.
*/
@NotNull
public IAndroidTarget[] getTargets() {
AndroidPlatform platform = AndroidPlatform.getInstance(myModule);
if (platform != null) {
final AndroidSdkData sdkData = platform.getSdkData();
return sdkData.getTargets();
}
return new IAndroidTarget[0];
}
public static boolean isLayoutLibTarget(@NotNull IAndroidTarget target) {
return target.isPlatform() && target.hasRenderingLibrary();
}
@Nullable
public IAndroidTarget getHighestApiTarget() {
// Note: The target list is already sorted in ascending API order.
IAndroidTarget[] targetList = getTargets();
for (int i = targetList.length - 1; i >= 0; i--) {
IAndroidTarget target = targetList[i];
if (isLayoutLibTarget(target) && isLayoutLibSupported(target)) {
return target;
}
}
return null;
}
/**
* Returns if the LayoutLib API (not to be confused with Platform API) level is supported.
*/
private static boolean isLayoutLibSupported(IAndroidTarget target) {
if (target instanceof PlatformTarget) {
SdkManager.LayoutlibVersion layoutlibVersion = ((PlatformTarget)target).getLayoutlibVersion();
return layoutlibVersion.getApi() <= Bridge.API_CURRENT;
}
return false;
}
/**
* Returns the preferred theme
*/
@NotNull
public String computePreferredTheme(@NotNull Configuration configuration) {
ManifestInfo manifest = ManifestInfo.get(myModule, false);
// TODO: If we are rendering a layout in included context, pick the theme
// from the outer layout instead
String activity = configuration.getActivity();
if (activity != null) {
String activityFqcn = activity;
if (activity.startsWith(".")) {
AndroidModuleInfo moduleInfo = AndroidModuleInfo.get(myModule);
if (moduleInfo != null) {
String pkg = StringUtil.notNullize(ManifestInfo.get(myModule, false).getPackage());
activityFqcn = pkg + activity;
}
}
ActivityAttributes attributes = manifest.getActivityAttributes(activityFqcn);
if (attributes != null) {
String theme = attributes.getTheme();
if (theme != null) {
return theme;
}
}
// Try with the package name from the manifest.
attributes = manifest.getActivityAttributes(activity);
if (attributes != null) {
String theme = attributes.getTheme();
if (theme != null) {
return theme;
}
}
}
// Look up the default/fallback theme to use for this project (which
// depends on the screen size when no particular theme is specified
// in the manifest)
return manifest.getDefaultTheme(configuration.getTarget(), configuration.getScreenSize(), configuration.getDevice());
}
@NotNull
public Module getModule() {
return myModule;
}
@NotNull
public Project getProject() {
return myModule.getProject();
}
@Override
public void dispose() {
Disposer.dispose(myUserDeviceManager);
}
@Nullable
public Device getDefaultDevice() {
if (myDefaultDevice == null) {
// Note that this may not be the device actually used in new layouts; the ConfigMatcher
// has a PhoneComparator which sorts devices for a best match
List<Device> devices = getDevices();
if (!devices.isEmpty()) {
Device device = devices.get(0);
for (Device d : devices) {
String name = d.getId();
if (name.equals("Nexus 4")) {
device = d;
break;
} else if (name.equals("Galaxy Nexus")) {
device = d;
}
}
myDefaultDevice = device;
}
}
return myDefaultDevice;
}
/**
* Return the default render target to use, or null if no strong preference
*/
@Nullable
public IAndroidTarget getDefaultTarget() {
// Use the most recent target
return getHighestApiTarget();
}
@NotNull
public List<Locale> getLocales() {
// Get locales from modules, but not libraries!
LocalResourceRepository projectResources = ProjectResourceRepository.getProjectResources(myModule, true);
assert projectResources != null;
if (projectResources.getModificationCount() != myLocaleCacheStamp) {
myLocales = null;
}
if (myLocales == null) {
List<Locale> locales = new ArrayList<Locale>();
for (LocaleQualifier locale : projectResources.getLocales()) {
locales.add(Locale.create(locale));
}
myLocales = locales;
myLocaleCacheStamp = projectResources.getModificationCount();
}
return myLocales;
}
@Nullable
public IAndroidTarget getProjectTarget() {
AndroidPlatform platform = AndroidPlatform.getInstance(myModule);
return platform != null ? platform.getTarget() : null;
}
@NotNull
public Locale getLocale() {
if (myLocale == null) {
String localeString = getStateManager().getProjectState().getLocale();
if (localeString != null) {
myLocale = ConfigurationProjectState.fromLocaleString(localeString);
} else {
myLocale = Locale.ANY;
}
}
return myLocale;
}
public void setLocale(@NotNull Locale locale) {
if (!locale.equals(myLocale)) {
myLocale = locale;
myStateVersion++;
getStateManager().getProjectState().setLocale(ConfigurationProjectState.toLocaleString(locale));
for (Configuration configuration : myCache.values()) {
configuration.updated(CFG_LOCALE);
}
}
}
/** Returns the most recently used devices, in MRU order */
public List<Device> getRecentDevices() {
List<String> deviceIds = getStateManager().getProjectState().getDeviceIds();
if (deviceIds.isEmpty()) {
return Collections.emptyList();
}
List<Device> devices = Lists.newArrayListWithExpectedSize(deviceIds.size());
ListIterator<String> iterator = deviceIds.listIterator();
while (iterator.hasNext()) {
String id = iterator.next();
Device device = getDeviceById(id);
if (device != null) {
devices.add(device);
} else {
iterator.remove();
}
}
return devices;
}
public void selectDevice(@NotNull Device device) {
// Manually move the given device to the front of the eligibility queue
String id = device.getId();
List<String> deviceIds = getStateManager().getProjectState().getDeviceIds();
deviceIds.remove(id);
deviceIds.add(0, id);
// Only store a limited number of recent devices
while (deviceIds.size() > 10) {
deviceIds.remove(deviceIds.size() - 1);
}
myStateVersion++;
for (Configuration configuration : myCache.values()) {
// TODO: Null out the themes too if using a system theme (e.g. where the theme was not chosen
// by the activity or manifest default, but inferred based on the device and API level).
// For example, if you switch from an Android Wear device (where the default is DeviceDefault) to
// a Nexus 5 (where the default is currently Theme.Holo) we should recompute the theme for the
// configuration too!
boolean updateTheme = false;
String theme = configuration.getTheme();
if (theme != null && theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) {
updateTheme = true;
configuration.startBulkEditing();
configuration.setTheme(null);
}
configuration.updated(CFG_DEVICE);
if (updateTheme) {
configuration.finishBulkEditing();
}
}
}
@Nullable
public IAndroidTarget getTarget() {
if (myTarget == null) {
ConfigurationProjectState projectState = getStateManager().getProjectState();
if (projectState.isPickTarget()) {
myTarget = getDefaultTarget();
} else {
String targetString = projectState.getTarget();
myTarget = ConfigurationProjectState.fromTargetString(this, targetString);
if (myTarget == null) {
myTarget = getDefaultTarget();
}
}
return myTarget;
}
return myTarget;
}
/** Returns the best render target to use for the given minimum API level */
@Nullable
public IAndroidTarget getTarget(int min) {
IAndroidTarget target = getTarget();
if (target != null && target.getVersion().getApiLevel() >= min) {
return target;
}
IAndroidTarget[] targetList = getTargets();
for (int i = targetList.length - 1; i >= 0; i--) {
target = targetList[i];
if (isLayoutLibTarget(target) && target.getVersion().getFeatureLevel() >= min && isLayoutLibSupported(target)) {
return target;
}
}
return null;
}
public void setTarget(@Nullable IAndroidTarget target) {
if (target != myTarget) {
if (myTarget != null) {
// Clear out the bitmap cache of the previous platform, since it's likely we won't
// need it again. If you have *two* projects open with different platforms, this will
// needlessly flush the bitmap cache for the project still using it, but that just
// means the next render will need to fetch them again; from that point on both platform
// bitmap sets are in memory.
AndroidTargetData targetData = AndroidTargetData.getTargetData(myTarget, myModule);
if (targetData != null) {
targetData.clearLayoutBitmapCache(myModule);
}
}
myTarget = target;
if (target != null) {
getStateManager().getProjectState().setTarget(ConfigurationProjectState.toTargetString(target));
myStateVersion++;
for (Configuration configuration : myCache.values()) {
configuration.updated(CFG_TARGET);
}
}
}
}
/**
* Synchronizes changes to the given attributes (indicated by the mask
* referencing the {@code CFG_} configuration attribute bit flags in
* {@link Configuration} to the layout variations of the given updated file.
*
* @param flags the attributes which were updated
* @param updatedFile the file which was updated
* @param base the base configuration to base the chooser off of
* @param includeSelf whether the updated file itself should be updated
* @param async whether the updates should be performed asynchronously
*/
public void syncToVariations(
final int flags,
final @NotNull VirtualFile updatedFile,
final @NotNull Configuration base,
final boolean includeSelf,
boolean async) {
if (async) {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
doSyncToVariations(flags, updatedFile, includeSelf, base);
}
});
} else {
doSyncToVariations(flags, updatedFile, includeSelf, base);
}
}
private void doSyncToVariations(@SuppressWarnings("UnusedParameters") int flags,
VirtualFile updatedFile, boolean includeSelf,
Configuration base) {
// Synchronize the given changes to other configurations as well
List<VirtualFile> files = ResourceHelper.getResourceVariations(updatedFile, includeSelf);
for (VirtualFile file : files) {
Configuration configuration = getConfiguration(file);
Configuration.copyCompatible(base, configuration);
configuration.save();
}
}
public int getStateVersion() {
return myStateVersion;
}
public ResourceResolverCache getResolverCache() {
if (myResolverCache == null) {
myResolverCache = ResourceResolverCache.create(this);
}
return myResolverCache;
}
}