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 }