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