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