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 }