/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php * * 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.ide.eclipse.adt.internal.editors.layout.configuration; import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; import static com.android.SdkConstants.PREFIX_RESOURCE_REF; import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.rendering.LayoutLibrary; import com.android.ide.common.rendering.api.Capability; import com.android.ide.common.resources.ResourceFolder; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.common.resources.configuration.DensityQualifier; import com.android.ide.common.resources.configuration.DeviceConfigHelper; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.common.resources.configuration.LayoutDirectionQualifier; import com.android.ide.common.resources.configuration.LocaleQualifier; import com.android.ide.common.resources.configuration.NightModeQualifier; import com.android.ide.common.resources.configuration.ScreenSizeQualifier; import com.android.ide.common.resources.configuration.UiModeQualifier; import com.android.ide.common.resources.configuration.VersionQualifier; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.resources.Density; import com.android.resources.LayoutDirection; import com.android.resources.NightMode; import com.android.resources.ScreenSize; import com.android.resources.UiMode; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.devices.Device; import com.android.sdklib.devices.State; import com.android.utils.Pair; import com.google.common.base.Objects; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.QualifiedName; import java.util.List; /** * A {@linkplain Configuration} is a selection of device, orientation, theme, * etc for use when rendering a layout. */ public class Configuration { /** The {@link FolderConfiguration} in change flags or override flags */ public static final int CFG_FOLDER = 1 << 0; /** The {@link Device} in change flags or override flags */ public static final int CFG_DEVICE = 1 << 1; /** The {@link State} in change flags or override flags */ public static final int CFG_DEVICE_STATE = 1 << 2; /** The theme in change flags or override flags */ public static final int CFG_THEME = 1 << 3; /** The locale in change flags or override flags */ public static final int CFG_LOCALE = 1 << 4; /** The rendering {@link IAndroidTarget} in change flags or override flags */ public static final int CFG_TARGET = 1 << 5; /** The {@link NightMode} in change flags or override flags */ public static final int CFG_NIGHT_MODE = 1 << 6; /** The {@link UiMode} in change flags or override flags */ public static final int CFG_UI_MODE = 1 << 7; /** The {@link UiMode} in change flags or override flags */ public static final int CFG_ACTIVITY = 1 << 8; /** References all attributes */ public static final int MASK_ALL = 0xFFFF; /** Attributes which affect which best-layout-file selection */ public static final int MASK_FILE_ATTRS = CFG_DEVICE|CFG_DEVICE_STATE|CFG_LOCALE|CFG_TARGET|CFG_NIGHT_MODE|CFG_UI_MODE; /** Attributes which affect rendering appearance */ public static final int MASK_RENDERING = MASK_FILE_ATTRS|CFG_THEME; /** * Setting name for project-wide setting controlling rendering target and locale which * is shared for all files */ public final static QualifiedName NAME_RENDER_STATE = new QualifiedName(AdtPlugin.PLUGIN_ID, "render"); //$NON-NLS-1$ private final static String MARKER_FRAMEWORK = "-"; //$NON-NLS-1$ private final static String MARKER_PROJECT = "+"; //$NON-NLS-1$ private final static String SEP = ":"; //$NON-NLS-1$ private final static String SEP_LOCALE = "-"; //$NON-NLS-1$ @NonNull protected ConfigurationChooser mConfigChooser; /** The {@link FolderConfiguration} representing the state of the UI controls */ @NonNull protected final FolderConfiguration mFullConfig = new FolderConfiguration(); /** The {@link FolderConfiguration} being edited. */ @Nullable protected FolderConfiguration mEditedConfig; /** The target of the project of the file being edited. */ @Nullable private IAndroidTarget mTarget; /** The theme style to render with */ @Nullable private String mTheme; /** The device to render with */ @Nullable private Device mDevice; /** The device state */ @Nullable private State mState; /** * The activity associated with the layout. This is just a cached value of * the true value stored on the layout. */ @Nullable private String mActivity; /** The locale to use for this configuration */ @NonNull private Locale mLocale = Locale.ANY; /** UI mode */ @NonNull private UiMode mUiMode = UiMode.NORMAL; /** Night mode */ @NonNull private NightMode mNightMode = NightMode.NOTNIGHT; /** The display name */ private String mDisplayName; /** * Creates a new {@linkplain Configuration} * * @param chooser the associated chooser */ protected Configuration(@NonNull ConfigurationChooser chooser) { mConfigChooser = chooser; } /** * Sets the associated configuration chooser * * @param chooser the chooser */ void setChooser(@NonNull ConfigurationChooser chooser) { // TODO: We should get rid of the binding between configurations // and configuration choosers. This is currently needed because // the choosers contain vital data such as the set of available // rendering targets, the set of available locales etc, which // also doesn't belong inside the configuration but is needed by it. mConfigChooser = chooser; } /** * Gets the associated configuration chooser * * @return the chooser */ @NonNull ConfigurationChooser getChooser() { return mConfigChooser; } /** * Creates a new {@linkplain Configuration} * * @param chooser the associated chooser * @return a new configuration */ @NonNull public static Configuration create(@NonNull ConfigurationChooser chooser) { return new Configuration(chooser); } /** * 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 */ @NonNull public static Configuration create( @NonNull Configuration base, @NonNull IFile file) { Configuration configuration = copy(base); ConfigurationChooser chooser = base.getChooser(); ProjectResources resources = chooser.getResources(); ConfigurationMatcher matcher = new ConfigurationMatcher(chooser, configuration, file, resources, false); ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); configuration.mEditedConfig = new FolderConfiguration(); configuration.mEditedConfig.set(resFolder.getConfiguration()); matcher.adaptConfigSelection(true /*needBestMatch*/); configuration.syncFolderConfig(); 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 */ @NonNull public static Configuration copy(@NonNull Configuration original) { Configuration copy = create(original.mConfigChooser); copy.mFullConfig.set(original.mFullConfig); if (original.mEditedConfig != null) { copy.mEditedConfig = new FolderConfiguration(); copy.mEditedConfig.set(original.mEditedConfig); } copy.mTarget = original.getTarget(); copy.mTheme = original.getTheme(); copy.mDevice = original.getDevice(); copy.mState = original.getDeviceState(); copy.mActivity = original.getActivity(); copy.mLocale = original.getLocale(); copy.mUiMode = original.getUiMode(); copy.mNightMode = original.getNightMode(); copy.mDisplayName = original.getDisplayName(); return copy; } /** * Returns the associated activity * * @return the activity */ @Nullable public String getActivity() { return mActivity; } /** * Returns the chosen device. * * @return the chosen device */ @Nullable public Device getDevice() { return mDevice; } /** * Returns the chosen device state * * @return the device state */ @Nullable public State getDeviceState() { return mState; } /** * Returns the chosen locale * * @return the locale */ @NonNull public Locale getLocale() { return mLocale; } /** * Returns the UI mode * * @return the UI mode */ @NonNull public UiMode getUiMode() { return mUiMode; } /** * Returns the day/night mode * * @return the night mode */ @NonNull public NightMode getNightMode() { return mNightMode; } /** * Returns the current theme style * * @return the theme style */ @Nullable public String getTheme() { return mTheme; } /** * Returns the rendering target * * @return the target */ @Nullable public IAndroidTarget getTarget() { return mTarget; } /** * 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 mDisplayName; } /** * Returns whether the configuration's theme is a project theme. *
* The returned value is meaningless if {@link #getTheme()} returns *null
.
*
* @return true for project a theme, false for a framework theme
*/
public boolean isProjectTheme() {
String theme = getTheme();
if (theme != null) {
assert theme.startsWith(STYLE_RESOURCE_PREFIX)
|| theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX);
return ResourceHelper.isProjectStyle(theme);
}
return false;
}
/**
* Returns true if the current layout is locale-specific
*
* @return if this configuration represents a locale-specific layout
*/
public boolean isLocaleSpecificLayout() {
return mEditedConfig == null || mEditedConfig.getLocaleQualifier() != null;
}
/**
* Returns the full, complete {@link FolderConfiguration}
*
* @return the full configuration
*/
@NonNull
public FolderConfiguration getFullConfig() {
return mFullConfig;
}
/**
* Copies the full, complete {@link FolderConfiguration} into the given
* folder config instance.
*
* @param dest the {@link FolderConfiguration} instance to copy into
*/
public void copyFullConfig(FolderConfiguration dest) {
dest.set(mFullConfig);
}
/**
* Returns the edited {@link 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
*/
@NonNull
public FolderConfiguration getEditedConfig() {
return mEditedConfig;
}
/**
* Sets the edited {@link 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.
*
* @param editedConfig the constraints configuration
*/
public void setEditedConfig(@NonNull FolderConfiguration editedConfig) {
mEditedConfig = editedConfig;
}
/**
* Sets the associated activity
*
* @param activity the activity
*/
public void setActivity(String activity) {
mActivity = activity;
}
/**
* Sets the device
*
* @param device the device
* @param skipSync if true, don't sync folder configuration (typically because
* you are going to set other configuration parameters and you'll call
* {@link #syncFolderConfig()} once at the end)
*/
public void setDevice(Device device, boolean skipSync) {
mDevice = device;
if (!skipSync) {
syncFolderConfig();
}
}
/**
* Sets the device state
*
* @param state the device state
* @param skipSync if true, don't sync folder configuration (typically because
* you are going to set other configuration parameters and you'll call
* {@link #syncFolderConfig()} once at the end)
*/
public void setDeviceState(State state, boolean skipSync) {
mState = state;
if (!skipSync) {
syncFolderConfig();
}
}
/**
* Sets the locale
*
* @param locale the locale
* @param skipSync if true, don't sync folder configuration (typically because
* you are going to set other configuration parameters and you'll call
* {@link #syncFolderConfig()} once at the end)
*/
public void setLocale(@NonNull Locale locale, boolean skipSync) {
mLocale = locale;
if (!skipSync) {
syncFolderConfig();
}
}
/**
* Sets the rendering target
*
* @param target rendering target
* @param skipSync if true, don't sync folder configuration (typically because
* you are going to set other configuration parameters and you'll call
* {@link #syncFolderConfig()} once at the end)
*/
public void setTarget(IAndroidTarget target, boolean skipSync) {
mTarget = target;
if (!skipSync) {
syncFolderConfig();
}
}
/**
* Sets the display name to be shown for this configuration.
*
* @param displayName the new display name
*/
public void setDisplayName(@Nullable String displayName) {
mDisplayName = displayName;
}
/**
* Sets the night mode
*
* @param night the night mode
* @param skipSync if true, don't sync folder configuration (typically because
* you are going to set other configuration parameters and you'll call
* {@link #syncFolderConfig()} once at the end)
*/
public void setNightMode(@NonNull NightMode night, boolean skipSync) {
mNightMode = night;
if (!skipSync) {
syncFolderConfig();
}
}
/**
* Sets the UI mode
*
* @param uiMode the UI mode
* @param skipSync if true, don't sync folder configuration (typically because
* you are going to set other configuration parameters and you'll call
* {@link #syncFolderConfig()} once at the end)
*/
public void setUiMode(@NonNull UiMode uiMode, boolean skipSync) {
mUiMode = uiMode;
if (!skipSync) {
syncFolderConfig();
}
}
/**
* Sets the theme style
*
* @param theme the theme
*/
public void setTheme(String theme) {
mTheme = theme;
checkThemePrefix();
}
/**
* Updates the folder configuration such that it reflects changes in
* configuration state such as the device orientation, the UI mode, the
* rendering target, etc.
*/
public void syncFolderConfig() {
Device device = getDevice();
if (device == null) {
return;
}
// get the device config from the device/state combos.
FolderConfiguration config = DeviceConfigHelper.getFolderConfig(getDeviceState());
// replace the config with the one from the device
mFullConfig.set(config);
// sync the selected locale
Locale locale = getLocale();
mFullConfig.setLocaleQualifier(locale.qualifier);
if (!locale.hasLanguage()) {
// Avoid getting the layout library if the locale doesn't have any language.
mFullConfig.setLayoutDirectionQualifier(
new LayoutDirectionQualifier(LayoutDirection.LTR));
} else {
Sdk currentSdk = Sdk.getCurrent();
if (currentSdk != null) {
AndroidTargetData targetData = currentSdk.getTargetData(getTarget());
if (targetData != null) {
LayoutLibrary layoutLib = targetData.getLayoutLibrary();
if (layoutLib != null) {
if (layoutLib.isRtl(locale.toLocaleId())) {
mFullConfig.setLayoutDirectionQualifier(
new LayoutDirectionQualifier(LayoutDirection.RTL));
} else {
mFullConfig.setLayoutDirectionQualifier(
new LayoutDirectionQualifier(LayoutDirection.LTR));
}
}
}
}
}
// Replace the UiMode with the selected one, if one is selected
UiMode uiMode = getUiMode();
if (uiMode != null) {
mFullConfig.setUiModeQualifier(new UiModeQualifier(uiMode));
}
// Replace the NightMode with the selected one, if one is selected
NightMode nightMode = getNightMode();
if (nightMode != null) {
mFullConfig.setNightModeQualifier(new NightModeQualifier(nightMode));
}
// replace the API level by the selection of the combo
IAndroidTarget target = getTarget();
if (target == null && mConfigChooser != null) {
target = mConfigChooser.getProjectTarget();
}
if (target != null) {
int apiLevel = target.getVersion().getApiLevel();
mFullConfig.setVersionQualifier(new VersionQualifier(apiLevel));
}
}
/**
* Creates a string suitable for persistence, which can be initialized back
* to a configuration via {@link #initialize(String)}
*
* @return a persistent string
*/
@NonNull
public String toPersistentString() {
StringBuilder sb = new StringBuilder(32);
Device device = getDevice();
if (device != null) {
sb.append(device.getName());
sb.append(SEP);
State state = getDeviceState();
if (state != null) {
sb.append(state.getName());
}
sb.append(SEP);
Locale locale = getLocale();
if (isLocaleSpecificLayout() && locale != null && locale.qualifier.hasLanguage()) {
// locale[0]/[1] can be null sometimes when starting Eclipse
sb.append(locale.qualifier.getLanguage());
sb.append(SEP_LOCALE);
if (locale.qualifier.hasRegion()) {
sb.append(locale.qualifier.getRegion());
}
}
sb.append(SEP);
// Need to escape the theme: if we write the full theme style, then
// we can end up with ":"'s in the string (as in @android:style/Theme) which
// can be mistaken for {@link #SEP}. Instead use {@link #MARKER_FRAMEWORK}.
String theme = getTheme();
if (theme != null) {
String themeName = ResourceHelper.styleToTheme(theme);
if (theme.startsWith(STYLE_RESOURCE_PREFIX)) {
sb.append(MARKER_PROJECT);
} else if (theme.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) {
sb.append(MARKER_FRAMEWORK);
}
sb.append(themeName);
}
sb.append(SEP);
UiMode uiMode = getUiMode();
if (uiMode != null) {
sb.append(uiMode.getResourceValue());
}
sb.append(SEP);
NightMode nightMode = getNightMode();
if (nightMode != null) {
sb.append(nightMode.getResourceValue());
}
sb.append(SEP);
// We used to store the render target here in R9. Leave a marker
// to ensure that we don't reuse this slot; add new extra fields after it.
sb.append(SEP);
String activity = getActivity();
if (activity != null) {
sb.append(activity);
}
}
return sb.toString();
}
/** Returns the preferred theme, or null */
@Nullable
String computePreferredTheme() {
IProject project = mConfigChooser.getProject();
ManifestInfo manifest = ManifestInfo.get(project);
// Look up the screen size for the current state
ScreenSize screenSize = null;
Device device = getDevice();
if (device != null) {
List