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 package com.android.ide.eclipse.adt.internal.editors.layout.configuration;
17 
18 import com.android.annotations.NonNull;
19 import com.android.annotations.Nullable;
20 import com.android.ide.common.rendering.api.Capability;
21 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
22 import com.android.resources.Density;
23 import com.android.resources.NightMode;
24 import com.android.resources.UiMode;
25 import com.android.sdklib.IAndroidTarget;
26 import com.android.sdklib.devices.Device;
27 import com.android.sdklib.devices.Hardware;
28 import com.android.sdklib.devices.Screen;
29 import com.android.sdklib.devices.State;
30 
31 import java.util.Collection;
32 import java.util.List;
33 
34 /**
35  * An {@linkplain VaryingConfiguration} is a {@link Configuration} which
36  * inherits all of its values from a different configuration, except for one or
37  * more attributes where it overrides a custom value, and the overridden value
38  * will always <b>differ</b> from the inherited value!
39  * <p>
40  * For example, a {@linkplain VaryingConfiguration} may state that it
41  * overrides the locale, and if the inherited locale is "en", then the returned
42  * locale from the {@linkplain VaryingConfiguration} may be for example "nb",
43  * but never "en".
44  * <p>
45  * The configuration will attempt to make its changed inherited value to be as
46  * different as possible from the inherited value. Thus, a configuration which
47  * overrides the device will probably return a phone-sized screen if the
48  * inherited device is a tablet, or vice versa.
49  */
50 public class VaryingConfiguration extends NestedConfiguration {
51     /** Variation version; see {@link #setVariation(int)} */
52     private int mVariation;
53 
54     /** Variation version count; see {@link #setVariationCount(int)} */
55     private int mVariationCount;
56 
57     /** Bitmask of attributes to be varied/alternated from the parent */
58     private int mAlternate;
59 
60     /**
61      * Constructs a new {@linkplain VaryingConfiguration}.
62      * Construct via
63      *
64      * @param chooser the associated chooser
65      * @param configuration the configuration to inherit from
66      */
VaryingConfiguration( @onNull ConfigurationChooser chooser, @NonNull Configuration configuration)67     private VaryingConfiguration(
68             @NonNull ConfigurationChooser chooser,
69             @NonNull Configuration configuration) {
70         super(chooser, configuration);
71     }
72 
73     /**
74      * Creates a new {@linkplain Configuration} which inherits values from the
75      * given parent {@linkplain Configuration}, possibly overriding some as
76      * well.
77      *
78      * @param chooser the associated chooser
79      * @param parent the configuration to inherit values from
80      * @return a new configuration
81      */
82     @NonNull
create(@onNull ConfigurationChooser chooser, @NonNull Configuration parent)83     public static VaryingConfiguration create(@NonNull ConfigurationChooser chooser,
84             @NonNull Configuration parent) {
85         return new VaryingConfiguration(chooser, parent);
86     }
87 
88     /**
89      * Creates a new {@linkplain VaryingConfiguration} that has the same overriding
90      * attributes as the given other {@linkplain VaryingConfiguration}.
91      *
92      * @param other the configuration to copy overrides from
93      * @param parent the parent to tie the configuration to for inheriting values
94      * @return a new configuration
95      */
96     @NonNull
create( @onNull VaryingConfiguration other, @NonNull Configuration parent)97     public static VaryingConfiguration create(
98             @NonNull VaryingConfiguration other,
99             @NonNull Configuration parent) {
100         VaryingConfiguration configuration =
101                 new VaryingConfiguration(other.mConfigChooser, parent);
102         initFrom(configuration, other, other, false);
103         configuration.mAlternate = other.mAlternate;
104         configuration.mVariation = other.mVariation;
105         configuration.mVariationCount = other.mVariationCount;
106         configuration.syncFolderConfig();
107 
108         return configuration;
109     }
110 
111     /**
112      * Returns the alternate flags for this configuration. Corresponds to
113      * the {@code CFG_} flags in {@link ConfigurationClient}.
114      *
115      * @return the bitmask
116      */
getAlternateFlags()117     public int getAlternateFlags() {
118         return mAlternate;
119     }
120 
121     @Override
syncFolderConfig()122     public void syncFolderConfig() {
123         super.syncFolderConfig();
124         updateDisplayName();
125     }
126 
127     /**
128      * Sets the variation version for this
129      * {@linkplain VaryingConfiguration}. There might be multiple
130      * {@linkplain VaryingConfiguration} instances inheriting from a
131      * {@link Configuration}. The variation version allows them to choose
132      * different complementing values, so they don't all flip to the same other
133      * (out of multiple choices) value. The {@link #setVariationCount(int)}
134      * value can be used to determine how to partition the buckets of values.
135      * Also updates the variation count if necessary.
136      *
137      * @param variation variation version
138      */
setVariation(int variation)139     public void setVariation(int variation) {
140         mVariation = variation;
141         mVariationCount = Math.max(mVariationCount, variation + 1);
142     }
143 
144     /**
145      * Sets the number of {@link VaryingConfiguration} variations mapped
146      * to the same parent configuration as this one. See
147      * {@link #setVariation(int)} for details.
148      *
149      * @param count the total number of variation versions
150      */
setVariationCount(int count)151     public void setVariationCount(int count) {
152         mVariationCount = count;
153     }
154 
155     /**
156      * Updates the display name in this configuration based on the values and override settings
157      */
updateDisplayName()158     public void updateDisplayName() {
159         setDisplayName(computeDisplayName());
160     }
161 
162     @Override
163     @NonNull
getLocale()164     public Locale getLocale() {
165         if (isOverridingLocale()) {
166             return super.getLocale();
167         }
168         Locale locale = mParent.getLocale();
169         if (isAlternatingLocale() && locale != null) {
170             List<Locale> locales = mConfigChooser.getLocaleList();
171             for (Locale l : locales) {
172                 // TODO: Try to be smarter about which one we pick; for example, try
173                 // to pick a language that is substantially different from the inherited
174                 // language, such as either with the strings of the largest or shortest
175                 // length, or perhaps based on some geography or population metrics
176                 if (!l.equals(locale)) {
177                     locale = l;
178                     break;
179                 }
180             }
181         }
182 
183         return locale;
184     }
185 
186     @Override
187     @Nullable
getTarget()188     public IAndroidTarget getTarget() {
189         if (isOverridingTarget()) {
190             return super.getTarget();
191         }
192         IAndroidTarget target = mParent.getTarget();
193         if (isAlternatingTarget() && target != null) {
194             List<IAndroidTarget> targets = mConfigChooser.getTargetList();
195             if (!targets.isEmpty()) {
196                 // Pick a different target: if you're showing the most recent render target,
197                 // then pick the lowest supported target, and vice versa
198                 IAndroidTarget mostRecent = targets.get(targets.size() - 1);
199                 if (target.equals(mostRecent)) {
200                     // Find oldest supported
201                     ManifestInfo info = ManifestInfo.get(mConfigChooser.getProject());
202                     int minSdkVersion = info.getMinSdkVersion();
203                     for (IAndroidTarget t : targets) {
204                         if (t.getVersion().getApiLevel() >= minSdkVersion) {
205                             target = t;
206                             break;
207                         }
208                     }
209                 } else {
210                     target = mostRecent;
211                 }
212             }
213         }
214 
215         return target;
216     }
217 
218     // Cached values, key=parent's device, cached value=device
219     private Device mPrevParentDevice;
220     private Device mPrevDevice;
221 
222     @Override
223     @Nullable
getDevice()224     public Device getDevice() {
225         if (isOverridingDevice()) {
226             return super.getDevice();
227         }
228         Device device = mParent.getDevice();
229         if (isAlternatingDevice() && device != null) {
230             if (device == mPrevParentDevice) {
231                 return mPrevDevice;
232             }
233 
234             mPrevParentDevice = device;
235 
236             // Pick a different device
237             Collection<Device> devices = mConfigChooser.getDevices();
238 
239             // Divide up the available devices into {@link #mVariationCount} + 1 buckets
240             // (the + 1 is for the bucket now taken up by the inherited value).
241             // Then assign buckets to each {@link #mVariation} version, and pick one
242             // from the bucket assigned to this current configuration's variation version.
243 
244             // I could just divide up the device list count, but that would treat a lot of
245             // very similar phones as having the same kind of variety as the 7" and 10"
246             // tablets which are sitting right next to each other in the device list.
247             // Instead, do this by screen size.
248 
249 
250             double smallest = 100;
251             double biggest = 1;
252             for (Device d : devices) {
253                 double size = getScreenSize(d);
254                 if (size < 0) {
255                     continue; // no data
256                 }
257                 if (size >= biggest) {
258                     biggest = size;
259                 }
260                 if (size <= smallest) {
261                     smallest = size;
262                 }
263             }
264 
265             int bucketCount = mVariationCount + 1;
266             double inchesPerBucket = (biggest - smallest) / bucketCount;
267 
268             double overriddenSize = getScreenSize(device);
269             int overriddenBucket = (int) ((overriddenSize - smallest) / inchesPerBucket);
270             int bucket = (mVariation < overriddenBucket) ? mVariation : mVariation + 1;
271             double from = inchesPerBucket * bucket + smallest;
272             double to = from + inchesPerBucket;
273             if (biggest - to < 0.1) {
274                 to = biggest + 0.1;
275             }
276 
277             boolean canScaleNinePatch = supports(Capability.FIXED_SCALABLE_NINE_PATCH);
278             for (Device d : devices) {
279                 double size = getScreenSize(d);
280                 if (size >= from && size < to) {
281                     if (!canScaleNinePatch) {
282                         Density density = getDensity(d);
283                         if (density == Density.TV || density == Density.LOW) {
284                             continue;
285                         }
286                     }
287 
288                     device = d;
289                     break;
290                 }
291             }
292 
293             mPrevDevice = device;
294         }
295 
296         return device;
297     }
298 
299     /**
300      * Returns the density of the given device
301      *
302      * @param device the device to check
303      * @return the density or null
304      */
305     @Nullable
getDensity(@onNull Device device)306     private static Density getDensity(@NonNull Device device) {
307         Hardware hardware = device.getDefaultHardware();
308         if (hardware != null) {
309             Screen screen = hardware.getScreen();
310             if (screen != null) {
311                 return screen.getPixelDensity();
312             }
313         }
314 
315         return null;
316     }
317 
318     /**
319      * Returns the diagonal length of the given device
320      *
321      * @param device the device to check
322      * @return the diagonal length or -1
323      */
getScreenSize(@onNull Device device)324     private static double getScreenSize(@NonNull Device device) {
325         Hardware hardware = device.getDefaultHardware();
326         if (hardware != null) {
327             Screen screen = hardware.getScreen();
328             if (screen != null) {
329                 return screen.getDiagonalLength();
330             }
331         }
332 
333         return -1;
334     }
335 
336     @Override
337     @Nullable
getDeviceState()338     public State getDeviceState() {
339         if (isOverridingDeviceState()) {
340             return super.getDeviceState();
341         }
342         State state = mParent.getDeviceState();
343         if (isAlternatingDeviceState() && state != null) {
344             State alternate = getNextDeviceState(state);
345 
346             return alternate;
347         } else {
348             if ((isAlternatingDevice() || isOverridingDevice()) && state != null) {
349                 // If the device differs, I need to look up a suitable equivalent state
350                 // on our device
351                 Device device = getDevice();
352                 if (device != null) {
353                     return device.getState(state.getName());
354                 }
355             }
356 
357             return state;
358         }
359     }
360 
361     @Override
362     @NonNull
getNightMode()363     public NightMode getNightMode() {
364         if (isOverridingNightMode()) {
365             return super.getNightMode();
366         }
367         NightMode nightMode = mParent.getNightMode();
368         if (isAlternatingNightMode() && nightMode != null) {
369             nightMode = nightMode == NightMode.NIGHT ? NightMode.NOTNIGHT : NightMode.NIGHT;
370             return nightMode;
371         } else {
372             return nightMode;
373         }
374     }
375 
376     @Override
377     @NonNull
getUiMode()378     public UiMode getUiMode() {
379         if (isOverridingUiMode()) {
380             return super.getUiMode();
381         }
382         UiMode uiMode = mParent.getUiMode();
383         if (isAlternatingUiMode() && uiMode != null) {
384             // TODO: Use manifest's supports screen to decide which are most relevant
385             // (as well as which available configuration qualifiers are present in the
386             // layout)
387             UiMode[] values = UiMode.values();
388             uiMode = values[(uiMode.ordinal() + 1) % values.length];
389             return uiMode;
390         } else {
391             return uiMode;
392         }
393     }
394 
395     @Override
396     @Nullable
computeDisplayName()397     public String computeDisplayName() {
398         return computeDisplayName(getOverrideFlags() | mAlternate, this);
399     }
400 
401     /**
402      * Sets whether the locale should be alternated by this configuration
403      *
404      * @param alternate if true, alternate the inherited value
405      */
setAlternateLocale(boolean alternate)406     public void setAlternateLocale(boolean alternate) {
407         mAlternate |= CFG_LOCALE;
408     }
409 
410     /**
411      * Returns true if the locale is alternated
412      *
413      * @return true if the locale is alternated
414      */
isAlternatingLocale()415     public final boolean isAlternatingLocale() {
416         return (mAlternate & CFG_LOCALE) != 0;
417     }
418 
419     /**
420      * Sets whether the rendering target should be alternated by this configuration
421      *
422      * @param alternate if true, alternate the inherited value
423      */
setAlternateTarget(boolean alternate)424     public void setAlternateTarget(boolean alternate) {
425         mAlternate |= CFG_TARGET;
426     }
427 
428     /**
429      * Returns true if the target is alternated
430      *
431      * @return true if the target is alternated
432      */
isAlternatingTarget()433     public final boolean isAlternatingTarget() {
434         return (mAlternate & CFG_TARGET) != 0;
435     }
436 
437     /**
438      * Sets whether the device should be alternated by this configuration
439      *
440      * @param alternate if true, alternate the inherited value
441      */
setAlternateDevice(boolean alternate)442     public void setAlternateDevice(boolean alternate) {
443         mAlternate |= CFG_DEVICE;
444     }
445 
446     /**
447      * Returns true if the device is alternated
448      *
449      * @return true if the device is alternated
450      */
isAlternatingDevice()451     public final boolean isAlternatingDevice() {
452         return (mAlternate & CFG_DEVICE) != 0;
453     }
454 
455     /**
456      * Sets whether the device state should be alternated by this configuration
457      *
458      * @param alternate if true, alternate the inherited value
459      */
setAlternateDeviceState(boolean alternate)460     public void setAlternateDeviceState(boolean alternate) {
461         mAlternate |= CFG_DEVICE_STATE;
462     }
463 
464     /**
465      * Returns true if the device state is alternated
466      *
467      * @return true if the device state is alternated
468      */
isAlternatingDeviceState()469     public final boolean isAlternatingDeviceState() {
470         return (mAlternate & CFG_DEVICE_STATE) != 0;
471     }
472 
473     /**
474      * Sets whether the night mode should be alternated by this configuration
475      *
476      * @param alternate if true, alternate the inherited value
477      */
setAlternateNightMode(boolean alternate)478     public void setAlternateNightMode(boolean alternate) {
479         mAlternate |= CFG_NIGHT_MODE;
480     }
481 
482     /**
483      * Returns true if the night mode is alternated
484      *
485      * @return true if the night mode is alternated
486      */
isAlternatingNightMode()487     public final boolean isAlternatingNightMode() {
488         return (mAlternate & CFG_NIGHT_MODE) != 0;
489     }
490 
491     /**
492      * Sets whether the UI mode should be alternated by this configuration
493      *
494      * @param alternate if true, alternate the inherited value
495      */
setAlternateUiMode(boolean alternate)496     public void setAlternateUiMode(boolean alternate) {
497         mAlternate |= CFG_UI_MODE;
498     }
499 
500     /**
501      * Returns true if the UI mode is alternated
502      *
503      * @return true if the UI mode is alternated
504      */
isAlternatingUiMode()505     public final boolean isAlternatingUiMode() {
506         return (mAlternate & CFG_UI_MODE) != 0;
507     }
508 
509 }