Home | History | Annotate | Download | only in configuration
      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      */
     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
     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
     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      */
    117     public int getAlternateFlags() {
    118         return mAlternate;
    119     }
    120 
    121     @Override
    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      */
    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      */
    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      */
    158     public void updateDisplayName() {
    159         setDisplayName(computeDisplayName());
    160     }
    161 
    162     @Override
    163     @NonNull
    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
    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
    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
    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      */
    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
    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
    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
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    505     public final boolean isAlternatingUiMode() {
    506         return (mAlternate & CFG_UI_MODE) != 0;
    507     }
    508 
    509 }