/* * 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.LayoutLibrary; import com.android.ide.common.rendering.api.Capability; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.common.resources.ResourceResolver; import com.android.ide.common.resources.configuration.*; 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.*; import com.google.common.base.Objects; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.module.Module; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import static com.android.SdkConstants.*; import static com.android.tools.idea.configurations.ConfigurationListener.*; /** * A {@linkplain Configuration} is a selection of device, orientation, theme, * etc for use when rendering a layout. */ public class Configuration implements Disposable { /** The associated file */ @Nullable VirtualFile myFile; /** * The {@link com.android.ide.common.resources.configuration.FolderConfiguration} representing the state of the UI controls */ @NotNull protected final FolderConfiguration myFullConfig = new FolderConfiguration(); /** The associated {@link ConfigurationManager} */ @NotNull protected final ConfigurationManager myManager; /** * The {@link com.android.ide.common.resources.configuration.FolderConfiguration} being edited. */ @NotNull protected final FolderConfiguration myEditedConfig; /** * The target of the project of the file being edited. */ @Nullable private IAndroidTarget myTarget; /** * The theme style to render with */ @Nullable private String myTheme; /** * A specific device to render with */ @Nullable private Device mySpecificDevice; /** * The specific device state */ @Nullable private State myState; /** * The computed effective device; if this configuration does not have a hardcoded specific device, * it will be computed based on the current device list; this field caches the value. */ @Nullable private Device myDevice; /** * The device state to use. Used to update {@link #getDeviceState()} such that it returns a state * suitable with whatever {@link #getDevice()} returns, since {@link #getDevice()} updates dynamically, * and the specific {@link State} instances are tied to actual devices (through the * {@link com.android.sdklib.devices.State#getHardware()} accessor). */ @Nullable private String myStateName; /** * The activity associated with the layout. This is just a cached value of * the true value stored on the layout. */ @Nullable private String myActivity; /** * The locale to use for this configuration */ @Nullable private Locale myLocale = null; /** * UI mode */ @NotNull private UiMode myUiMode = UiMode.NORMAL; /** * Night mode */ @NotNull private NightMode myNightMode = NightMode.NOTNIGHT; /** * The display name */ private String myDisplayName; /** For nesting count use by {@link #startBulkEditing()} and {@link #finishBulkEditing()} */ private int myBulkEditingCount; /** Optional set of listeners to notify via {@link #updated(int)} */ @Nullable private List<ConfigurationListener> myListeners; /** Dirty flags since last notify: corresponds to constants in {@link ConfigurationListener} */ protected int myNotifyDirty; /** Dirty flags since last folder config sync: corresponds to constants in {@link ConfigurationListener} */ protected int myFolderConfigDirty = MASK_FOLDERCONFIG; protected int myProjectStateVersion; /** * Creates a new {@linkplain Configuration} */ protected Configuration(@NotNull ConfigurationManager manager, @Nullable VirtualFile file, @NotNull FolderConfiguration editedConfig) { myManager = manager; myFile = file; myEditedConfig = editedConfig; if (isLocaleSpecificLayout()) { myLocale = Locale.create(editedConfig); } if (isOrientationSpecificLayout()) { ScreenOrientationQualifier qualifier = editedConfig.getScreenOrientationQualifier(); assert qualifier != null; // because isOrientationSpecificLayout() ScreenOrientation orientation = qualifier.getValue(); if (orientation != null) { myStateName = orientation.getShortDisplayValue(); } } } /** * Creates a new {@linkplain Configuration} * * @return a new configuration */ @NotNull @VisibleForTesting static Configuration create(@NotNull ConfigurationManager manager, @Nullable VirtualFile file, @NotNull FolderConfiguration editedConfig) { return new Configuration(manager, file, editedConfig); } /** * Creates a configuration suitable for the given file * * @param base the base configuration to base the file configuration off of * @param file the file to look up a configuration for * @return a suitable configuration */ @NotNull public static Configuration create(@NotNull Configuration base, @NotNull VirtualFile file) { // TODO: Figure out whether we need this, or if it should be replaced by // a call to ConfigurationManager#createSimilar() Configuration configuration = base.clone(); LocalResourceRepository resources = AppResourceRepository.getAppResources(base.getModule(), true); ConfigurationMatcher matcher = new ConfigurationMatcher(configuration, resources, file); configuration.getEditedConfig().set(FolderConfiguration.getConfigForFolder(file.getParent().getName())); matcher.adaptConfigSelection(true /*needBestMatch*/); return configuration; } @NotNull public static Configuration create(@NotNull ConfigurationManager manager, @Nullable VirtualFile file, @Nullable ConfigurationFileState fileState, @NotNull FolderConfiguration editedConfig) { Configuration configuration = new Configuration(manager, file, editedConfig); configuration.startBulkEditing(); if (fileState != null) { fileState.loadState(configuration); } configuration.finishBulkEditing(); return configuration; } /** * Creates a new {@linkplain Configuration} that is a copy from a different configuration * * @param original the original to copy from * @return a new configuration copied from the original */ @NotNull public static Configuration copy(@NotNull Configuration original) { FolderConfiguration copiedConfig = new FolderConfiguration(); copiedConfig.set(original.getEditedConfig()); Configuration copy = new Configuration(original.myManager, original.myFile, copiedConfig); copy.myFullConfig.set(original.myFullConfig); copy.myFolderConfigDirty = original.myFolderConfigDirty; copy.myProjectStateVersion = original.myProjectStateVersion; copy.myTarget = original.myTarget; // avoid getTarget() since it fetches project state copy.myLocale = original.myLocale; // avoid getLocale() since it fetches project state copy.myTheme = original.getTheme(); copy.mySpecificDevice = original.mySpecificDevice; copy.myDevice = original.myDevice; // avoid getDevice() since it fetches project state copy.myStateName = original.myStateName; copy.myState = original.myState; copy.myActivity = original.getActivity(); copy.myUiMode = original.getUiMode(); copy.myNightMode = original.getNightMode(); copy.myDisplayName = original.getDisplayName(); return copy; } @Override public Configuration clone() { return copy(this); } /** * Copies attributes from the given source configuration into the given destination configuration, * as long as the attributes are compatible with the folder of the destination file. * * @param source the original to copy from * @return a new configuration copied from the original */ @NotNull public static Configuration copyCompatible(@NotNull Configuration source, @NotNull Configuration destination) { assert !Comparing.equal(source.myFile, destination.myFile); // This method is intended to sync configurations for resource variations FolderConfiguration editedConfig = destination.getEditedConfig(); if (editedConfig.getVersionQualifier() == null) { destination.myTarget = source.myTarget; // avoid getTarget() since it fetches project state } if (editedConfig.getScreenSizeQualifier() == null) { destination.mySpecificDevice = source.mySpecificDevice; // avoid getDevice() since it fetches project state } if (editedConfig.getScreenOrientationQualifier() == null && editedConfig.getSmallestScreenWidthQualifier() == null) { destination.myStateName = source.myStateName; destination.myState = source.myState; } if (editedConfig.getLanguageQualifier() == null) { destination.myLocale = source.myLocale; // avoid getLocale() since it fetches project state } if (editedConfig.getUiModeQualifier() == null) { destination.myUiMode = source.getUiMode(); } if (editedConfig.getNightModeQualifier() == null) { destination.myNightMode = source.getNightMode(); } destination.myActivity = source.getActivity(); destination.myTheme = source.getTheme(); //destination.myDisplayName = source.getDisplayName(); LocalResourceRepository resources = AppResourceRepository.getAppResources(source.myManager.getModule(), true); ConfigurationMatcher matcher = new ConfigurationMatcher(destination, resources, destination.myFile); //if (!matcher.isCurrentFileBestMatchFor(editedConfig)) { matcher.adaptConfigSelection(true /*needBestMatch*/); //} return destination; } public void save() { ConfigurationStateManager stateManager = ConfigurationStateManager.get(myManager.getModule().getProject()); if (myFile != null) { ConfigurationFileState fileState = new ConfigurationFileState(); fileState.saveState(this); stateManager.setConfigurationState(myFile, fileState); } } /** * Returns the associated {@link ConfigurationManager} * * @return the manager */ @NotNull public ConfigurationManager getConfigurationManager() { return myManager; } /** * Returns the file associated with this configuration, if any * * @return the file, or null */ @Nullable public VirtualFile getFile() { return myFile; } /** * Returns the associated activity * * @return the activity */ @Nullable public String getActivity() { if (myActivity == NO_ACTIVITY) { return null; } else if (myActivity == null && myFile != null) { myActivity = ApplicationManager.getApplication().runReadAction(new Computable<String>() { @Nullable @Override public String compute() { PsiFile file = PsiManager.getInstance(myManager.getProject()).findFile(myFile); if (file instanceof XmlFile) { XmlFile xmlFile = (XmlFile)file; XmlTag rootTag = xmlFile.getRootTag(); if (rootTag != null) { XmlAttribute attribute = rootTag.getAttribute(ATTR_CONTEXT, TOOLS_URI); if (attribute != null) { return attribute.getValue(); } } } return null; } }); if (myActivity == null) { myActivity = NO_ACTIVITY; return null; } } return myActivity; } /** Special marker value which indicates that this activity has been checked and has no activity * (whereas a null {@link #myActivity} field means that it has not yet been initialized */ private static final String NO_ACTIVITY = new String(); /** * Returns the chosen device. * * @return the chosen device */ @Nullable public Device getDevice() { if (myDevice == null) { if (mySpecificDevice != null) { myDevice = mySpecificDevice; } else { myDevice = computeBestDevice(); } } return myDevice; } @Nullable public static FolderConfiguration getFolderConfig(@NotNull Module module, @NotNull State state, @NotNull Locale locale, @Nullable IAndroidTarget target) { FolderConfiguration currentConfig = DeviceConfigHelper.getFolderConfig(state); if (currentConfig != null) { if (locale.hasLanguage()) { currentConfig.setLanguageQualifier(locale.language); if (locale.hasRegion()) { currentConfig.setRegionQualifier(locale.region); } if (locale.hasLanguage()) { LayoutLibrary layoutLib = RenderService.getLayoutLibrary(module, target); if (layoutLib != null) { if (layoutLib.isRtl(locale.toLocaleId())) { currentConfig.setLayoutDirectionQualifier(new LayoutDirectionQualifier(LayoutDirection.RTL)); } } } } // Don't match on target since we tend to use recent layout lib versions to render even default (older) layouts // since more recent versions work a lot better fidelity wise // if (target != null) { // currentConfig.setVersionQualifier(new VersionQualifier(target.getVersion().getApiLevel())); // } } return currentConfig; } @Nullable private Device computeBestDevice() { for (Device device : myManager.getRecentDevices()) { String stateName = myStateName; if (stateName == null) { stateName = device.getDefaultState().getName(); } State selectedState = ConfigurationFileState.getState(device, stateName); Module module = myManager.getModule(); FolderConfiguration currentConfig = getFolderConfig(module, selectedState, getLocale(), getTarget()); if (currentConfig != null) { if (myEditedConfig.isMatchFor(currentConfig)) { LocalResourceRepository resources = AppResourceRepository.getAppResources(module, true); if (resources != null && myFile != null) { ResourceFolderType folderType = ResourceHelper.getFolderType(myFile); if (folderType != null) { List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folderType); if (!types.isEmpty()) { ResourceType type = types.get(0); VirtualFile match = resources.getMatchingFile(myFile, type, currentConfig); if (match != null && myFile.equals(match)) { return device; } } } } } } } return myManager.getDefaultDevice(); } /** * Returns the chosen device state * * @return the device state */ @Nullable public State getDeviceState() { if (myState == null) { Device device = getDevice(); myState = ConfigurationFileState.getState(device, myStateName); } return myState; } /** * Returns the chosen locale * * @return the locale */ @NotNull public Locale getLocale() { if (myLocale == null) { return myManager.getLocale(); } return myLocale; } /** * Returns the UI mode * * @return the UI mode */ @NotNull public UiMode getUiMode() { return myUiMode; } /** * Returns the day/night mode * * @return the night mode */ @NotNull public NightMode getNightMode() { return myNightMode; } /** * Returns the current theme style * * @return the theme style */ @Nullable public String getTheme() { if (myTheme == null) { myTheme = myManager.computePreferredTheme(this); } return myTheme; } /** * Returns the rendering target * * @return the target */ @Nullable public IAndroidTarget getTarget() { if (myTarget == null) { IAndroidTarget target = myManager.getTarget(); // If the project-wide render target isn't a match for the version qualifier in this layout // (for example, the render target is at API 11, and layout is in a -v14 folder) then pick // a target which matches. VersionQualifier version = myEditedConfig.getVersionQualifier(); if (target != null && version != null && version.getVersion() > target.getVersion().getFeatureLevel()) { return myManager.getTarget(version.getVersion()); } return target; } return myTarget; } /** * Returns the display name to show for this configuration * * @return the display name, or null if none has been assigned */ @Nullable public String getDisplayName() { return myDisplayName; } /** * Returns true if the current layout is locale-specific * * @return if this configuration represents a locale-specific layout */ public boolean isLocaleSpecificLayout() { return myEditedConfig.getLanguageQualifier() != null; } /** * Returns true if the current layout is target-specific * * @return if this configuration represents a target-specific layout */ public boolean isTargetSpecificLayout() { return myEditedConfig.getVersionQualifier() != null; } /** * Returns true if the current layout is locale-specific * * @return if this configuration represents a locale-specific layout */ public boolean isOrientationSpecificLayout() { return myEditedConfig.getScreenOrientationQualifier() != null; } /** * Returns the full, complete {@link com.android.ide.common.resources.configuration.FolderConfiguration} * * @return the full configuration */ @NotNull public FolderConfiguration getFullConfig() { if ((myFolderConfigDirty & MASK_FOLDERCONFIG) != 0 || myProjectStateVersion != myManager.getStateVersion()) { syncFolderConfig(); } return myFullConfig; } /** * Copies the full, complete {@link com.android.ide.common.resources.configuration.FolderConfiguration} into the given * folder config instance. * * @param dest the {@link com.android.ide.common.resources.configuration.FolderConfiguration} instance to copy into */ public void copyFullConfig(FolderConfiguration dest) { dest.set(myFullConfig); } /** * Returns the edited {@link com.android.ide.common.resources.configuration.FolderConfiguration} (this is not a full * configuration, so you can think of it as the "constraints" used by the * {@link ConfigurationMatcher} to produce a full configuration. * * @return the constraints configuration */ @NotNull public FolderConfiguration getEditedConfig() { return myEditedConfig; } /** * Sets the associated activity * * @param activity the activity */ public void setActivity(@Nullable String activity) { if (!StringUtil.equals(myActivity, activity)) { myActivity = activity; updated(CFG_ACTIVITY); } } /** * Sets the device * * @param device the device * @param preserveState if true, attempt to preserve the state associated with the config */ public void setDevice(Device device, boolean preserveState) { if (mySpecificDevice != device) { Device prevDevice = mySpecificDevice; State prevState = myState; myDevice = mySpecificDevice = device; int updateFlags = CFG_DEVICE; if (device != null) { State state = null; // Attempt to preserve the device state? if (preserveState && prevDevice != null) { if (prevState != null) { FolderConfiguration oldConfig = DeviceConfigHelper.getFolderConfig(prevState); if (oldConfig != null) { String stateName = getClosestMatch(oldConfig, device.getAllStates()); state = device.getState(stateName); } else { state = device.getState(prevState.getName()); } } } else if (preserveState && myStateName != null) { state = device.getState(myStateName); } if (state == null) { state = device.getDefaultState(); } if (myState != state) { setDeviceStateName(state.getName()); myState = state; updateFlags |= CFG_DEVICE_STATE; } } // TODO: Is this redundant with the stuff above? if (mySpecificDevice != null && myState == null) { setDeviceStateName(mySpecificDevice.getDefaultState().getName()); myState = mySpecificDevice.getDefaultState(); updateFlags |= CFG_DEVICE_STATE; } updated(updateFlags); } } /** * Attempts to find a close state among a list * * @param oldConfig the reference config. * @param states the list of states to search through * @return the name of the closest state match, or possibly null if no states are compatible * (this can only happen if the states don't have a single qualifier that is the same). */ @Nullable private static String getClosestMatch(@NotNull FolderConfiguration oldConfig, @NotNull List<State> states) { // create 2 lists as we're going to go through one and put the // candidates in the other. List<State> list1 = new ArrayList<State>(states.size()); List<State> list2 = new ArrayList<State>(states.size()); list1.addAll(states); final int count = FolderConfiguration.getQualifierCount(); for (int i = 0; i < count; i++) { // compute the new candidate list by only taking states that have // the same i-th qualifier as the old state for (State s : list1) { ResourceQualifier oldQualifier = oldConfig.getQualifier(i); FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s); ResourceQualifier newQualifier = folderConfig != null ? folderConfig.getQualifier(i) : null; if (oldQualifier == null) { if (newQualifier == null) { list2.add(s); } } else if (oldQualifier.equals(newQualifier)) { list2.add(s); } } // at any moment if the new candidate list contains only one match, its name // is returned. if (list2.size() == 1) { return list2.get(0).getName(); } // if the list is empty, then all the new states failed. It is considered ok, and // we move to the next qualifier anyway. This way, if a qualifier is different for // all new states it is simply ignored. if (list2.size() != 0) { // move the candidates back into list1. list1.clear(); list1.addAll(list2); list2.clear(); } } // the only way to reach this point is if there's an exact match. // (if there are more than one, then there's a duplicate state and it doesn't matter, // we take the first one). if (list1.size() > 0) { return list1.get(0).getName(); } return null; } /** * Sets the device state * * @param state the device state */ public void setDeviceState(State state) { if (myState != state) { if (state != null) { setDeviceStateName(state.getName()); } else { myStateName = null; } myState = state; updated(CFG_DEVICE_STATE); } } /** * Sets the device state name * * @param stateName the device state name */ public void setDeviceStateName(@Nullable String stateName) { ScreenOrientationQualifier qualifier = myEditedConfig.getScreenOrientationQualifier(); if (qualifier != null) { ScreenOrientation orientation = qualifier.getValue(); if (orientation != null) { stateName = orientation.getShortDisplayValue(); // Also used as state names } } if (!Objects.equal(stateName, myStateName)) { myStateName = stateName; myState = null; updated(CFG_DEVICE_STATE); } } /** * Sets the locale * * @param locale the locale */ public void setLocale(@NotNull Locale locale) { if (!Objects.equal(myLocale, locale)) { myLocale = locale; updated(CFG_LOCALE); } } /** * Sets the rendering target * * @param target rendering target */ public void setTarget(@Nullable IAndroidTarget target) { if (myTarget != target) { myTarget = target; updated(CFG_TARGET); } } /** * Sets the display name to be shown for this configuration. * * @param displayName the new display name */ public void setDisplayName(@Nullable String displayName) { if (!StringUtil.equals(myDisplayName, displayName)) { myDisplayName = displayName; updated(CFG_NAME); } } /** * Sets the night mode * * @param night the night mode */ public void setNightMode(@NotNull NightMode night) { if (myNightMode != night) { myNightMode = night; updated(CFG_NIGHT_MODE); } } /** * Sets the UI mode * * @param uiMode the UI mode */ public void setUiMode(@NotNull UiMode uiMode) { if (myUiMode != uiMode) { myUiMode = uiMode; updated(CFG_UI_MODE); } } /** * Sets the theme style * * @param theme the theme */ public void setTheme(@Nullable String theme) { if (!StringUtil.equals(myTheme, theme)) { myTheme = theme; checkThemePrefix(); updated(CFG_THEME); } } /** * Updates the folder configuration such that it reflects changes in * configuration state such as the device orientation, the UI mode, the * rendering target, etc. */ protected void syncFolderConfig() { Device device = getDevice(); if (device == null) { return; } // get the device config from the device/state combos. State deviceState = getDeviceState(); if (deviceState == null) { deviceState = device.getDefaultState(); } FolderConfiguration config = getFolderConfig(getModule(), deviceState, getLocale(), getTarget()); // replace the config with the one from the device myFullConfig.set(config); // sync the selected locale Locale locale = getLocale(); myFullConfig.setLanguageQualifier(locale.language); myFullConfig.setRegionQualifier(locale.region); if (myEditedConfig.getLayoutDirectionQualifier() != null) { myFullConfig.setLayoutDirectionQualifier(myEditedConfig.getLayoutDirectionQualifier()); } else if (!locale.hasLanguage()) { // Avoid getting the layout library if the locale doesn't have any language. myFullConfig.setLayoutDirectionQualifier(new LayoutDirectionQualifier(LayoutDirection.LTR)); } else { LayoutLibrary layoutLib = RenderService.getLayoutLibrary(getModule(), getTarget()); if (layoutLib != null) { if (layoutLib.isRtl(locale.toLocaleId())) { myFullConfig.setLayoutDirectionQualifier(new LayoutDirectionQualifier(LayoutDirection.RTL)); } else { myFullConfig.setLayoutDirectionQualifier(new LayoutDirectionQualifier(LayoutDirection.LTR)); } } } // Replace the UiMode with the selected one, if one is selected UiMode uiMode = getUiMode(); myFullConfig.setUiModeQualifier(new UiModeQualifier(uiMode)); // Replace the NightMode with the selected one, if one is selected NightMode nightMode = getNightMode(); myFullConfig.setNightModeQualifier(new NightModeQualifier(nightMode)); // replace the API level by the selection of the combo IAndroidTarget target = getTarget(); if (target != null) { int apiLevel = target.getVersion().getFeatureLevel(); myFullConfig.setVersionQualifier(new VersionQualifier(apiLevel)); } myFolderConfigDirty = 0; myProjectStateVersion = myManager.getStateVersion(); } /** Returns the screen size required for this configuration */ @Nullable public ScreenSize getScreenSize() { // Look up the screen size for the current state State deviceState = getDeviceState(); if (deviceState != null) { FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(deviceState); if (folderConfig != null) { ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier(); assert qualifier != null; return qualifier.getValue(); } } ScreenSize screenSize = null; Device device = getDevice(); if (device != null) { List<State> states = device.getAllStates(); for (State state : states) { FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(state); if (folderConfig != null) { ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier(); assert qualifier != null; screenSize = qualifier.getValue(); break; } } } return screenSize; } private void checkThemePrefix() { if (myTheme != null && !myTheme.startsWith(PREFIX_RESOURCE_REF)) { if (myTheme.isEmpty()) { myTheme = myManager.computePreferredTheme(this); return; } // TODO: When we get a local project repository, handle this: //ResourceRepository frameworkRes = mConfigChooser.getClient().getFrameworkResources(); //if (frameworkRes != null && frameworkRes.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + myTheme)) { // myTheme = ANDROID_STYLE_RESOURCE_PREFIX + myTheme; //} //else { myTheme = STYLE_RESOURCE_PREFIX + myTheme; //} } } /** * Returns the currently selected {@link com.android.resources.Density}. This is guaranteed to be non null. * * @return the density */ @NotNull public Density getDensity() { DensityQualifier qualifier = myFullConfig.getDensityQualifier(); if (qualifier != null) { // just a sanity check Density d = qualifier.getValue(); if (d != Density.NODPI) { return d; } } // no config? return medium as the default density. return Density.MEDIUM; } /** * Get the next cyclical state after the given state * * @param from the state to start with * @return the following state following */ @Nullable public State getNextDeviceState(@Nullable State from) { Device device = getDevice(); if (device == null) { return null; } List<State> states = device.getAllStates(); for (int i = 0; i < states.size(); i++) { if (states.get(i) == from) { return states.get((i + 1) % states.size()); } } // Search by name instead if (from != null) { String name = from.getName(); for (int i = 0; i < states.size(); i++) { if (states.get(i).getName().equals(name)) { return states.get((i + 1) % states.size()); } } } return null; } /** * Returns true if this configuration supports the given rendering * capability * * @param capability the capability to check * @return true if the capability is supported */ public boolean supports(Capability capability) { IAndroidTarget target = getTarget(); if (target != null) { return RenderService.supportsCapability(getModule(), target, capability); } return false; } /** * Marks the beginning of a "bulk" editing operation with repeated calls to * various setters. After all the values have been set, the client <b>must</b> * call {@link #finishBulkEditing()}. This allows configurations to avoid * doing {@link FolderConfiguration} syncing for intermediate stages, and all * listener updates are deferred until the bulk operation is complete. */ public void startBulkEditing() { synchronized (this) { myBulkEditingCount++; } } /** * Marks the end of a "bulk" editing operation. At this point listeners will * be notified of the cumulative changes, etc. See {@link #startBulkEditing()} * for details. */ public void finishBulkEditing() { boolean notify = false; synchronized (this) { myBulkEditingCount--; if (myBulkEditingCount == 0) { notify = true; } } if (notify) { updated(0); } } /** Called when one or more attributes of the configuration has changed */ public void updated(int flags) { myNotifyDirty |= flags; myFolderConfigDirty |= flags; if (myManager.getStateVersion() != myProjectStateVersion) { myNotifyDirty |= MASK_PROJECT_STATE; myFolderConfigDirty |= MASK_PROJECT_STATE; myDevice = null; myState = null; } if (myBulkEditingCount == 0) { int changed = myNotifyDirty; if (myListeners != null) { for (ConfigurationListener listener : myListeners) { listener.changed(changed); } } myNotifyDirty = 0; } } /** * Adds a listener to be notified when the configuration changes * * @param listener the listener to add */ public void addListener(@NotNull ConfigurationListener listener) { if (myListeners == null) { myListeners = new ArrayList<ConfigurationListener>(); } myListeners.add(listener); } /** * Removes a listener such that it is no longer notified of changes * * @param listener the listener to remove */ public void removeListener(@NotNull ConfigurationListener listener) { if (myListeners != null) { myListeners.remove(listener); if (myListeners.isEmpty()) { myListeners = null; } } } // ---- Resolving resources ---- @Nullable public ResourceResolver getResourceResolver() { String theme = getTheme(); if (theme != null) { return myManager.getResolverCache().getResourceResolver(getTarget(), theme, getFullConfig()); } return null; } /** * Returns a {@link com.android.tools.idea.rendering.LocalResourceRepository} for the framework resources based on the current * configuration selection. * * @return the framework resources or null if not found. */ @Nullable public ResourceRepository getFrameworkResources() { IAndroidTarget target = getTarget(); if (target != null) { return myManager.getResolverCache().getFrameworkResources(getFullConfig(), target); } return null; } // For debugging only @SuppressWarnings("SpellCheckingInspection") @Override public String toString() { return Objects.toStringHelper(this.getClass()) .add("display", getDisplayName()) .add("theme", getTheme()) .add("activity", getActivity()) .add("device", getDevice()) .add("state", getDeviceState()) .add("locale", getLocale()) .add("target", getTarget()) .add("uimode", getUiMode()) .add("nightmode", getNightMode()) .toString(); } public Module getModule() { return myManager.getModule(); } public boolean isBestMatchFor(VirtualFile file, FolderConfiguration config) { LocalResourceRepository resources = AppResourceRepository.getAppResources(getModule(), true); return new ConfigurationMatcher(this, resources, file).isCurrentFileBestMatchFor(config); } @Override public void dispose() { } public void setEffectiveDevice(@Nullable Device device, @Nullable State state) { int updateFlags = 0; if (myDevice != device) { updateFlags = CFG_DEVICE; myDevice = device; } if (myState != state) { myState = state; myStateName = state != null ? state.getName() : null; updateFlags |= CFG_DEVICE_STATE; } if (updateFlags != 0) { updated(updateFlags); } } }