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.LanguageQualifier; 31 import com.android.ide.common.resources.configuration.RegionQualifier; 32 import com.android.ide.common.resources.configuration.ScreenSizeQualifier; 33 import com.android.ide.eclipse.adt.AdtPlugin; 34 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; 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.List; 53 import java.util.Map; 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 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 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 */ 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 */ 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 169 public static ConfigurationDescription fromXml( 170 @Nullable IProject project, 171 @NonNull Element element, 172 @NonNull List<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 LanguageQualifier language = Locale.ANY_LANGUAGE; 204 RegionQualifier region = Locale.ANY_REGION; 205 String locales[] = localeString.split(SEP_LOCALE); 206 if (locales[0].length() > 0) { 207 language = new LanguageQualifier(locales[0]); 208 } 209 if (locales.length > 1 && locales[1].length() > 0) { 210 region = new RegionQualifier(locales[1]); 211 } 212 description.locale = Locale.create(language, region); 213 } 214 215 String activity = element.getAttribute(ATTR_ACTIVITY); 216 if (activity.isEmpty()) { 217 activity = null; 218 } 219 220 String deviceString = element.getAttribute(ATTR_DEVICE); 221 if (!deviceString.isEmpty()) { 222 for (Device d : deviceList) { 223 if (d.getName().equals(deviceString)) { 224 description.device = d; 225 String stateName = element.getAttribute(ATTR_STATE); 226 if (stateName.isEmpty() || stateName.equals("null")) { 227 description.state = Configuration.getState(d, stateName); 228 } else if (d.getAllStates().size() > 0) { 229 description.state = d.getAllStates().get(0); 230 } 231 break; 232 } 233 } 234 } 235 236 String uiModeString = element.getAttribute(ATTR_UIMODE); 237 if (!uiModeString.isEmpty()) { 238 description.uiMode = UiMode.getEnum(uiModeString); 239 if (description.uiMode == null) { 240 description.uiMode = UiMode.NORMAL; 241 } 242 } 243 244 String nightModeString = element.getAttribute(ATTR_NIGHTMODE); 245 if (!nightModeString.isEmpty()) { 246 description.nightMode = NightMode.getEnum(nightModeString); 247 if (description.nightMode == null) { 248 description.nightMode = NightMode.NOTNIGHT; 249 } 250 } 251 252 253 // Should I really be storing the FULL configuration? Might be trouble if 254 // you bring a different device 255 256 return description; 257 } 258 259 /** 260 * Write this description into the given document as a new element. 261 * 262 * @param document the document to add the description to 263 * @return the newly inserted element 264 */ 265 @NonNull 266 public Element toXml(Document document) { 267 Element element = document.createElement(TAG_PREVIEW); 268 269 element.setAttribute(ATTR_NAME, displayName); 270 FolderConfiguration fullConfig = folder; 271 String folderName = fullConfig.getFolderName(ResourceFolderType.LAYOUT); 272 element.setAttribute(ATTR_CONFIG, folderName); 273 if (theme != null) { 274 element.setAttribute(ATTR_THEME, theme); 275 } 276 if (target != null) { 277 element.setAttribute(ATTR_TARGET, Configuration.targetToString(target)); 278 } 279 280 if (locale != null && (locale.hasLanguage() || locale.hasRegion())) { 281 String value; 282 if (locale.hasRegion()) { 283 value = locale.language.getValue() + SEP_LOCALE + locale.region.getValue(); 284 } else { 285 value = locale.language.getValue(); 286 } 287 element.setAttribute(ATTR_LOCALE, value); 288 } 289 290 if (device != null) { 291 element.setAttribute(ATTR_DEVICE, device.getName()); 292 if (state != null) { 293 element.setAttribute(ATTR_STATE, state.getName()); 294 } 295 } 296 297 if (activity != null) { 298 element.setAttribute(ATTR_ACTIVITY, activity); 299 } 300 301 if (uiMode != null && uiMode != UiMode.NORMAL) { 302 element.setAttribute(ATTR_UIMODE, uiMode.getResourceValue()); 303 } 304 305 if (nightMode != null && nightMode != NightMode.NOTNIGHT) { 306 element.setAttribute(ATTR_NIGHTMODE, nightMode.getResourceValue()); 307 } 308 309 Element parent = document.getDocumentElement(); 310 if (parent == null) { 311 parent = document.createElement(TAG_PREVIEWS); 312 document.appendChild(parent); 313 } 314 parent.appendChild(element); 315 316 return element; 317 } 318 319 /** Returns the preferred theme, or null */ 320 @Nullable 321 String computePreferredTheme() { 322 if (project == null) { 323 return "Theme"; 324 } 325 ManifestInfo manifest = ManifestInfo.get(project); 326 327 // Look up the screen size for the current state 328 ScreenSize screenSize = null; 329 if (device != null) { 330 List<State> states = device.getAllStates(); 331 for (State s : states) { 332 FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s); 333 if (folderConfig != null) { 334 ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier(); 335 screenSize = qualifier.getValue(); 336 break; 337 } 338 } 339 } 340 341 // Look up the default/fallback theme to use for this project (which 342 // depends on the screen size when no particular theme is specified 343 // in the manifest) 344 String defaultTheme = manifest.getDefaultTheme(target, screenSize); 345 346 String preferred = defaultTheme; 347 if (theme == null) { 348 // If we are rendering a layout in included context, pick the theme 349 // from the outer layout instead 350 351 if (activity != null) { 352 Map<String, String> activityThemes = manifest.getActivityThemes(); 353 preferred = activityThemes.get(activity); 354 } 355 if (preferred == null) { 356 preferred = defaultTheme; 357 } 358 theme = preferred; 359 } 360 361 return preferred; 362 } 363 364 private void checkThemePrefix() { 365 if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) { 366 if (theme.isEmpty()) { 367 computePreferredTheme(); 368 return; 369 } 370 371 if (target != null) { 372 Sdk sdk = Sdk.getCurrent(); 373 if (sdk != null) { 374 AndroidTargetData data = sdk.getTargetData(target); 375 376 if (data != null) { 377 ResourceRepository resources = data.getFrameworkResources(); 378 if (resources != null 379 && resources.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) { 380 theme = ANDROID_STYLE_RESOURCE_PREFIX + theme; 381 return; 382 } 383 } 384 } 385 } 386 387 theme = STYLE_RESOURCE_PREFIX + theme; 388 } 389 } 390 } 391