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
17package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
18
19import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
20import static com.android.SdkConstants.ATTR_NAME;
21import static com.android.SdkConstants.ATTR_THEME;
22import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
23import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
24
25import com.android.annotations.NonNull;
26import com.android.annotations.Nullable;
27import com.android.ide.common.resources.ResourceRepository;
28import com.android.ide.common.resources.configuration.DeviceConfigHelper;
29import com.android.ide.common.resources.configuration.FolderConfiguration;
30import com.android.ide.common.resources.configuration.LanguageQualifier;
31import com.android.ide.common.resources.configuration.RegionQualifier;
32import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
33import com.android.ide.eclipse.adt.AdtPlugin;
34import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
35import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
36import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
37import com.android.ide.eclipse.adt.internal.sdk.Sdk;
38import com.android.resources.NightMode;
39import com.android.resources.ResourceFolderType;
40import com.android.resources.ScreenSize;
41import com.android.resources.UiMode;
42import com.android.sdklib.IAndroidTarget;
43import com.android.sdklib.devices.Device;
44import com.android.sdklib.devices.State;
45import com.google.common.base.Splitter;
46
47import org.eclipse.core.resources.IFile;
48import org.eclipse.core.resources.IProject;
49import org.eclipse.core.runtime.QualifiedName;
50import org.w3c.dom.Document;
51import org.w3c.dom.Element;
52
53import java.util.List;
54
55/** A description of a configuration, used for persistence */
56public 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                ActivityAttributes attributes = manifest.getActivityAttributes(activity);
353                if (attributes != null) {
354                    preferred = attributes.getTheme();
355                }
356            }
357            if (preferred == null) {
358                preferred = defaultTheme;
359            }
360            theme = preferred;
361        }
362
363        return preferred;
364    }
365
366    private void checkThemePrefix() {
367        if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) {
368            if (theme.isEmpty()) {
369                computePreferredTheme();
370                return;
371            }
372
373            if (target != null) {
374                Sdk sdk = Sdk.getCurrent();
375                if (sdk != null) {
376                    AndroidTargetData data = sdk.getTargetData(target);
377
378                    if (data != null) {
379                        ResourceRepository resources = data.getFrameworkResources();
380                        if (resources != null
381                            && resources.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) {
382                            theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
383                            return;
384                        }
385                    }
386                }
387            }
388
389            theme = STYLE_RESOURCE_PREFIX + theme;
390        }
391    }
392}
393