1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout.configuration; 18 19 import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; 20 import static com.android.SdkConstants.ATTR_NAME; 21 import static com.android.SdkConstants.ATTR_THEME; 22 import static com.android.SdkConstants.PREFIX_RESOURCE_REF; 23 import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; 24 25 import com.android.annotations.NonNull; 26 import com.android.annotations.Nullable; 27 import com.android.ide.common.resources.ResourceRepository; 28 import com.android.ide.common.resources.configuration.DeviceConfigHelper; 29 import com.android.ide.common.resources.configuration.FolderConfiguration; 30 import com.android.ide.common.resources.configuration.LocaleQualifier; 31 import com.android.ide.common.resources.configuration.ScreenSizeQualifier; 32 import com.android.ide.eclipse.adt.AdtPlugin; 33 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; 34 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes; 35 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 36 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 37 import com.android.resources.NightMode; 38 import com.android.resources.ResourceFolderType; 39 import com.android.resources.ScreenSize; 40 import com.android.resources.UiMode; 41 import com.android.sdklib.IAndroidTarget; 42 import com.android.sdklib.devices.Device; 43 import com.android.sdklib.devices.State; 44 import com.google.common.base.Splitter; 45 46 import org.eclipse.core.resources.IFile; 47 import org.eclipse.core.resources.IProject; 48 import org.eclipse.core.runtime.QualifiedName; 49 import org.w3c.dom.Document; 50 import org.w3c.dom.Element; 51 52 import java.util.Collection; 53 import java.util.List; 54 55 /** A description of a configuration, used for persistence */ 56 public class ConfigurationDescription { 57 private static final String TAG_PREVIEWS = "previews"; //$NON-NLS-1$ 58 private static final String TAG_PREVIEW = "preview"; //$NON-NLS-1$ 59 private static final String ATTR_TARGET = "target"; //$NON-NLS-1$ 60 private static final String ATTR_CONFIG = "config"; //$NON-NLS-1$ 61 private static final String ATTR_LOCALE = "locale"; //$NON-NLS-1$ 62 private static final String ATTR_ACTIVITY = "activity"; //$NON-NLS-1$ 63 private static final String ATTR_DEVICE = "device"; //$NON-NLS-1$ 64 private static final String ATTR_STATE = "devicestate"; //$NON-NLS-1$ 65 private static final String ATTR_UIMODE = "ui"; //$NON-NLS-1$ 66 private static final String ATTR_NIGHTMODE = "night"; //$NON-NLS-1$ 67 private final static String SEP_LOCALE = "-"; //$NON-NLS-1$ 68 69 /** 70 * Settings name for file-specific configuration preferences, such as which theme or 71 * device to render the current layout with 72 */ 73 public final static QualifiedName NAME_CONFIG_STATE = 74 new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$ 75 76 /** The project corresponding to this configuration's description */ 77 public final IProject project; 78 79 /** The display name */ 80 public String displayName; 81 82 /** The theme */ 83 public String theme; 84 85 /** The target */ 86 public IAndroidTarget target; 87 88 /** The display name */ 89 public FolderConfiguration folder; 90 91 /** The locale */ 92 public Locale locale = Locale.ANY; 93 94 /** The device */ 95 public Device device; 96 97 /** The device state */ 98 public State state; 99 100 /** The activity */ 101 public String activity; 102 103 /** UI mode */ 104 @NonNull 105 public UiMode uiMode = UiMode.NORMAL; 106 107 /** Night mode */ 108 @NonNull 109 public NightMode nightMode = NightMode.NOTNIGHT; 110 ConfigurationDescription(@ullable IProject project)111 private ConfigurationDescription(@Nullable IProject project) { 112 this.project = project; 113 } 114 115 /** 116 * Returns the persistent configuration description from the given file 117 * 118 * @param file the file to look up a description from 119 * @return the description or null if never written 120 */ 121 @Nullable getDescription(@onNull IFile file)122 public static String getDescription(@NonNull IFile file) { 123 return AdtPlugin.getFileProperty(file, NAME_CONFIG_STATE); 124 } 125 126 /** 127 * Sets the persistent configuration description data for the given file 128 * 129 * @param file the file to associate the description with 130 * @param description the description 131 */ setDescription(@onNull IFile file, @NonNull String description)132 public static void setDescription(@NonNull IFile file, @NonNull String description) { 133 AdtPlugin.setFileProperty(file, NAME_CONFIG_STATE, description); 134 } 135 136 /** 137 * Creates a description from a given configuration 138 * 139 * @param project the project for this configuration's description 140 * @param configuration the configuration to describe 141 * @return a new configuration 142 */ fromConfiguration( @ullable IProject project, @NonNull Configuration configuration)143 public static ConfigurationDescription fromConfiguration( 144 @Nullable IProject project, 145 @NonNull Configuration configuration) { 146 ConfigurationDescription description = new ConfigurationDescription(project); 147 description.displayName = configuration.getDisplayName(); 148 description.theme = configuration.getTheme(); 149 description.target = configuration.getTarget(); 150 description.folder = new FolderConfiguration(); 151 description.folder.set(configuration.getFullConfig()); 152 description.locale = configuration.getLocale(); 153 description.device = configuration.getDevice(); 154 description.state = configuration.getDeviceState(); 155 description.activity = configuration.getActivity(); 156 return description; 157 } 158 159 /** 160 * Initializes a string previously created with 161 * {@link #toXml(Document)} 162 * 163 * @param project the project for this configuration's description 164 * @param element the element to read back from 165 * @param deviceList list of available devices 166 * @return true if the configuration was initialized 167 */ 168 @Nullable fromXml( @ullable IProject project, @NonNull Element element, @NonNull Collection<Device> deviceList)169 public static ConfigurationDescription fromXml( 170 @Nullable IProject project, 171 @NonNull Element element, 172 @NonNull Collection<Device> deviceList) { 173 ConfigurationDescription description = new ConfigurationDescription(project); 174 175 if (!TAG_PREVIEW.equals(element.getTagName())) { 176 return null; 177 } 178 179 String displayName = element.getAttribute(ATTR_NAME); 180 if (!displayName.isEmpty()) { 181 description.displayName = displayName; 182 } 183 184 String config = element.getAttribute(ATTR_CONFIG); 185 Iterable<String> segments = Splitter.on('-').split(config); 186 description.folder = FolderConfiguration.getConfig(segments); 187 188 String theme = element.getAttribute(ATTR_THEME); 189 if (!theme.isEmpty()) { 190 description.theme = theme; 191 } 192 193 String targetId = element.getAttribute(ATTR_TARGET); 194 if (!targetId.isEmpty()) { 195 IAndroidTarget target = Configuration.stringToTarget(targetId); 196 description.target = target; 197 } 198 199 String localeString = element.getAttribute(ATTR_LOCALE); 200 if (!localeString.isEmpty()) { 201 // Load locale. Note that this can get overwritten by the 202 // project-wide settings read below. 203 String locales[] = localeString.split(SEP_LOCALE); 204 if (locales[0].length() > 0 && !LocaleQualifier.FAKE_VALUE.equals(locales[0])) { 205 String language = locales[0]; 206 if (locales.length >= 2 && locales[1].length() > 0 && !LocaleQualifier.FAKE_VALUE.equals(locales[1])) { 207 description.locale = Locale.create(LocaleQualifier.getQualifier(language + "-r" + locales[1])); 208 } else { 209 description.locale = Locale.create(new LocaleQualifier(language)); 210 } 211 } else { 212 description.locale = Locale.ANY; 213 } 214 215 216 } 217 218 String activity = element.getAttribute(ATTR_ACTIVITY); 219 if (activity.isEmpty()) { 220 activity = null; 221 } 222 223 String deviceString = element.getAttribute(ATTR_DEVICE); 224 if (!deviceString.isEmpty()) { 225 for (Device d : deviceList) { 226 if (d.getName().equals(deviceString)) { 227 description.device = d; 228 String stateName = element.getAttribute(ATTR_STATE); 229 if (stateName.isEmpty() || stateName.equals("null")) { 230 description.state = Configuration.getState(d, stateName); 231 } else if (d.getAllStates().size() > 0) { 232 description.state = d.getAllStates().get(0); 233 } 234 break; 235 } 236 } 237 } 238 239 String uiModeString = element.getAttribute(ATTR_UIMODE); 240 if (!uiModeString.isEmpty()) { 241 description.uiMode = UiMode.getEnum(uiModeString); 242 if (description.uiMode == null) { 243 description.uiMode = UiMode.NORMAL; 244 } 245 } 246 247 String nightModeString = element.getAttribute(ATTR_NIGHTMODE); 248 if (!nightModeString.isEmpty()) { 249 description.nightMode = NightMode.getEnum(nightModeString); 250 if (description.nightMode == null) { 251 description.nightMode = NightMode.NOTNIGHT; 252 } 253 } 254 255 256 // Should I really be storing the FULL configuration? Might be trouble if 257 // you bring a different device 258 259 return description; 260 } 261 262 /** 263 * Write this description into the given document as a new element. 264 * 265 * @param document the document to add the description to 266 * @return the newly inserted element 267 */ 268 @NonNull toXml(Document document)269 public Element toXml(Document document) { 270 Element element = document.createElement(TAG_PREVIEW); 271 272 element.setAttribute(ATTR_NAME, displayName); 273 FolderConfiguration fullConfig = folder; 274 String folderName = fullConfig.getFolderName(ResourceFolderType.LAYOUT); 275 element.setAttribute(ATTR_CONFIG, folderName); 276 if (theme != null) { 277 element.setAttribute(ATTR_THEME, theme); 278 } 279 if (target != null) { 280 element.setAttribute(ATTR_TARGET, Configuration.targetToString(target)); 281 } 282 283 if (locale != null && (locale.hasLanguage() || locale.hasRegion())) { 284 String value; 285 if (locale.hasRegion()) { 286 value = locale.qualifier.getLanguage() + SEP_LOCALE + locale.qualifier.getRegion(); 287 } else { 288 value = locale.qualifier.getLanguage(); 289 } 290 element.setAttribute(ATTR_LOCALE, value); 291 } 292 293 if (device != null) { 294 element.setAttribute(ATTR_DEVICE, device.getName()); 295 if (state != null) { 296 element.setAttribute(ATTR_STATE, state.getName()); 297 } 298 } 299 300 if (activity != null) { 301 element.setAttribute(ATTR_ACTIVITY, activity); 302 } 303 304 if (uiMode != null && uiMode != UiMode.NORMAL) { 305 element.setAttribute(ATTR_UIMODE, uiMode.getResourceValue()); 306 } 307 308 if (nightMode != null && nightMode != NightMode.NOTNIGHT) { 309 element.setAttribute(ATTR_NIGHTMODE, nightMode.getResourceValue()); 310 } 311 312 Element parent = document.getDocumentElement(); 313 if (parent == null) { 314 parent = document.createElement(TAG_PREVIEWS); 315 document.appendChild(parent); 316 } 317 parent.appendChild(element); 318 319 return element; 320 } 321 322 /** Returns the preferred theme, or null */ 323 @Nullable computePreferredTheme()324 String computePreferredTheme() { 325 if (project == null) { 326 return "Theme"; 327 } 328 ManifestInfo manifest = ManifestInfo.get(project); 329 330 // Look up the screen size for the current state 331 ScreenSize screenSize = null; 332 if (device != null) { 333 List<State> states = device.getAllStates(); 334 for (State s : states) { 335 FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s); 336 if (folderConfig != null) { 337 ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier(); 338 screenSize = qualifier.getValue(); 339 break; 340 } 341 } 342 } 343 344 // Look up the default/fallback theme to use for this project (which 345 // depends on the screen size when no particular theme is specified 346 // in the manifest) 347 String defaultTheme = manifest.getDefaultTheme(target, screenSize); 348 349 String preferred = defaultTheme; 350 if (theme == null) { 351 // If we are rendering a layout in included context, pick the theme 352 // from the outer layout instead 353 354 if (activity != null) { 355 ActivityAttributes attributes = manifest.getActivityAttributes(activity); 356 if (attributes != null) { 357 preferred = attributes.getTheme(); 358 } 359 } 360 if (preferred == null) { 361 preferred = defaultTheme; 362 } 363 theme = preferred; 364 } 365 366 return preferred; 367 } 368 checkThemePrefix()369 private void checkThemePrefix() { 370 if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) { 371 if (theme.isEmpty()) { 372 computePreferredTheme(); 373 return; 374 } 375 376 if (target != null) { 377 Sdk sdk = Sdk.getCurrent(); 378 if (sdk != null) { 379 AndroidTargetData data = sdk.getTargetData(target); 380 381 if (data != null) { 382 ResourceRepository resources = data.getFrameworkResources(); 383 if (resources != null 384 && resources.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) { 385 theme = ANDROID_STYLE_RESOURCE_PREFIX + theme; 386 return; 387 } 388 } 389 } 390 } 391 392 theme = STYLE_RESOURCE_PREFIX + theme; 393 } 394 } 395 } 396