1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 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 17 package android.content.res; 18 19 import android.compat.annotation.UnsupportedAppUsage; 20 import android.content.pm.ApplicationInfo; 21 import android.graphics.Canvas; 22 import android.graphics.PointF; 23 import android.graphics.Rect; 24 import android.graphics.Region; 25 import android.os.Build; 26 import android.os.Build.VERSION_CODES; 27 import android.os.Parcel; 28 import android.os.Parcelable; 29 import android.util.DisplayMetrics; 30 import android.view.MotionEvent; 31 import android.view.WindowManager; 32 import android.view.WindowManager.LayoutParams; 33 34 /** 35 * CompatibilityInfo class keeps the information about the screen compatibility mode that the 36 * application is running under. 37 * 38 * {@hide} 39 */ 40 public class CompatibilityInfo implements Parcelable { 41 /** default compatibility info object for compatible applications */ 42 @UnsupportedAppUsage 43 public static final CompatibilityInfo DEFAULT_COMPATIBILITY_INFO = new CompatibilityInfo() { 44 }; 45 46 /** 47 * This is the number of pixels we would like to have along the 48 * short axis of an app that needs to run on a normal size screen. 49 */ 50 public static final int DEFAULT_NORMAL_SHORT_DIMENSION = 320; 51 52 /** 53 * This is the maximum aspect ratio we will allow while keeping 54 * applications in a compatible screen size. 55 */ 56 public static final float MAXIMUM_ASPECT_RATIO = (854f/480f); 57 58 /** 59 * A compatibility flags 60 */ 61 private final int mCompatibilityFlags; 62 63 /** 64 * A flag mask to tell if the application needs scaling (when mApplicationScale != 1.0f) 65 * {@see compatibilityFlag} 66 */ 67 private static final int SCALING_REQUIRED = 1; 68 69 /** 70 * Application must always run in compatibility mode? 71 */ 72 private static final int ALWAYS_NEEDS_COMPAT = 2; 73 74 /** 75 * Application never should run in compatibility mode? 76 */ 77 private static final int NEVER_NEEDS_COMPAT = 4; 78 79 /** 80 * Set if the application needs to run in screen size compatibility mode. 81 */ 82 private static final int NEEDS_SCREEN_COMPAT = 8; 83 84 /** 85 * Set if the application needs to run in with compat resources. 86 */ 87 private static final int NEEDS_COMPAT_RES = 16; 88 89 /** 90 * The effective screen density we have selected for this application. 91 */ 92 public final int applicationDensity; 93 94 /** 95 * Application's scale. 96 */ 97 @UnsupportedAppUsage 98 public final float applicationScale; 99 100 /** 101 * Application's inverted scale. 102 */ 103 public final float applicationInvertedScale; 104 105 @UnsupportedAppUsage CompatibilityInfo(ApplicationInfo appInfo, int screenLayout, int sw, boolean forceCompat)106 public CompatibilityInfo(ApplicationInfo appInfo, int screenLayout, int sw, 107 boolean forceCompat) { 108 int compatFlags = 0; 109 110 if (appInfo.targetSdkVersion < VERSION_CODES.O) { 111 compatFlags |= NEEDS_COMPAT_RES; 112 } 113 if (appInfo.requiresSmallestWidthDp != 0 || appInfo.compatibleWidthLimitDp != 0 114 || appInfo.largestWidthLimitDp != 0) { 115 // New style screen requirements spec. 116 int required = appInfo.requiresSmallestWidthDp != 0 117 ? appInfo.requiresSmallestWidthDp 118 : appInfo.compatibleWidthLimitDp; 119 if (required == 0) { 120 required = appInfo.largestWidthLimitDp; 121 } 122 int compat = appInfo.compatibleWidthLimitDp != 0 123 ? appInfo.compatibleWidthLimitDp : required; 124 if (compat < required) { 125 compat = required; 126 } 127 int largest = appInfo.largestWidthLimitDp; 128 129 if (required > DEFAULT_NORMAL_SHORT_DIMENSION) { 130 // For now -- if they require a size larger than the only 131 // size we can do in compatibility mode, then don't ever 132 // allow the app to go in to compat mode. Trying to run 133 // it at a smaller size it can handle will make it far more 134 // broken than running at a larger size than it wants or 135 // thinks it can handle. 136 compatFlags |= NEVER_NEEDS_COMPAT; 137 } else if (largest != 0 && sw > largest) { 138 // If the screen size is larger than the largest size the 139 // app thinks it can work with, then always force it in to 140 // compatibility mode. 141 compatFlags |= NEEDS_SCREEN_COMPAT | ALWAYS_NEEDS_COMPAT; 142 } else if (compat >= sw) { 143 // The screen size is something the app says it was designed 144 // for, so never do compatibility mode. 145 compatFlags |= NEVER_NEEDS_COMPAT; 146 } else if (forceCompat) { 147 // The app may work better with or without compatibility mode. 148 // Let the user decide. 149 compatFlags |= NEEDS_SCREEN_COMPAT; 150 } 151 152 // Modern apps always support densities. 153 applicationDensity = DisplayMetrics.DENSITY_DEVICE; 154 applicationScale = 1.0f; 155 applicationInvertedScale = 1.0f; 156 157 } else { 158 /** 159 * Has the application said that its UI is expandable? Based on the 160 * <supports-screen> android:expandible in the manifest. 161 */ 162 final int EXPANDABLE = 2; 163 164 /** 165 * Has the application said that its UI supports large screens? Based on the 166 * <supports-screen> android:largeScreens in the manifest. 167 */ 168 final int LARGE_SCREENS = 8; 169 170 /** 171 * Has the application said that its UI supports xlarge screens? Based on the 172 * <supports-screen> android:xlargeScreens in the manifest. 173 */ 174 final int XLARGE_SCREENS = 32; 175 176 int sizeInfo = 0; 177 178 // We can't rely on the application always setting 179 // FLAG_RESIZEABLE_FOR_SCREENS so will compute it based on various input. 180 boolean anyResizeable = false; 181 182 if ((appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS) != 0) { 183 sizeInfo |= LARGE_SCREENS; 184 anyResizeable = true; 185 if (!forceCompat) { 186 // If we aren't forcing the app into compatibility mode, then 187 // assume if it supports large screens that we should allow it 188 // to use the full space of an xlarge screen as well. 189 sizeInfo |= XLARGE_SCREENS | EXPANDABLE; 190 } 191 } 192 if ((appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_XLARGE_SCREENS) != 0) { 193 anyResizeable = true; 194 if (!forceCompat) { 195 sizeInfo |= XLARGE_SCREENS | EXPANDABLE; 196 } 197 } 198 if ((appInfo.flags & ApplicationInfo.FLAG_RESIZEABLE_FOR_SCREENS) != 0) { 199 anyResizeable = true; 200 sizeInfo |= EXPANDABLE; 201 } 202 203 if (forceCompat) { 204 // If we are forcing compatibility mode, then ignore an app that 205 // just says it is resizable for screens. We'll only have it fill 206 // the screen if it explicitly says it supports the screen size we 207 // are running in. 208 sizeInfo &= ~EXPANDABLE; 209 } 210 211 compatFlags |= NEEDS_SCREEN_COMPAT; 212 switch (screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK) { 213 case Configuration.SCREENLAYOUT_SIZE_XLARGE: 214 if ((sizeInfo&XLARGE_SCREENS) != 0) { 215 compatFlags &= ~NEEDS_SCREEN_COMPAT; 216 } 217 if ((appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_XLARGE_SCREENS) != 0) { 218 compatFlags |= NEVER_NEEDS_COMPAT; 219 } 220 break; 221 case Configuration.SCREENLAYOUT_SIZE_LARGE: 222 if ((sizeInfo&LARGE_SCREENS) != 0) { 223 compatFlags &= ~NEEDS_SCREEN_COMPAT; 224 } 225 if ((appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS) != 0) { 226 compatFlags |= NEVER_NEEDS_COMPAT; 227 } 228 break; 229 } 230 231 if ((screenLayout&Configuration.SCREENLAYOUT_COMPAT_NEEDED) != 0) { 232 if ((sizeInfo&EXPANDABLE) != 0) { 233 compatFlags &= ~NEEDS_SCREEN_COMPAT; 234 } else if (!anyResizeable) { 235 compatFlags |= ALWAYS_NEEDS_COMPAT; 236 } 237 } else { 238 compatFlags &= ~NEEDS_SCREEN_COMPAT; 239 compatFlags |= NEVER_NEEDS_COMPAT; 240 } 241 242 if ((appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES) != 0) { 243 applicationDensity = DisplayMetrics.DENSITY_DEVICE; 244 applicationScale = 1.0f; 245 applicationInvertedScale = 1.0f; 246 } else { 247 applicationDensity = DisplayMetrics.DENSITY_DEFAULT; 248 applicationScale = DisplayMetrics.DENSITY_DEVICE 249 / (float) DisplayMetrics.DENSITY_DEFAULT; 250 applicationInvertedScale = 1.0f / applicationScale; 251 compatFlags |= SCALING_REQUIRED; 252 } 253 } 254 255 mCompatibilityFlags = compatFlags; 256 } 257 CompatibilityInfo(int compFlags, int dens, float scale, float invertedScale)258 private CompatibilityInfo(int compFlags, 259 int dens, float scale, float invertedScale) { 260 mCompatibilityFlags = compFlags; 261 applicationDensity = dens; 262 applicationScale = scale; 263 applicationInvertedScale = invertedScale; 264 } 265 266 @UnsupportedAppUsage CompatibilityInfo()267 private CompatibilityInfo() { 268 this(NEVER_NEEDS_COMPAT, DisplayMetrics.DENSITY_DEVICE, 269 1.0f, 270 1.0f); 271 } 272 273 /** 274 * @return true if the scaling is required 275 */ 276 @UnsupportedAppUsage isScalingRequired()277 public boolean isScalingRequired() { 278 return (mCompatibilityFlags&SCALING_REQUIRED) != 0; 279 } 280 281 @UnsupportedAppUsage supportsScreen()282 public boolean supportsScreen() { 283 return (mCompatibilityFlags&NEEDS_SCREEN_COMPAT) == 0; 284 } 285 neverSupportsScreen()286 public boolean neverSupportsScreen() { 287 return (mCompatibilityFlags&ALWAYS_NEEDS_COMPAT) != 0; 288 } 289 alwaysSupportsScreen()290 public boolean alwaysSupportsScreen() { 291 return (mCompatibilityFlags&NEVER_NEEDS_COMPAT) != 0; 292 } 293 needsCompatResources()294 public boolean needsCompatResources() { 295 return (mCompatibilityFlags&NEEDS_COMPAT_RES) != 0; 296 } 297 298 /** 299 * Returns the translator which translates the coordinates in compatibility mode. 300 * @param params the window's parameter 301 */ 302 @UnsupportedAppUsage getTranslator()303 public Translator getTranslator() { 304 return isScalingRequired() ? new Translator() : null; 305 } 306 307 /** 308 * A helper object to translate the screen and window coordinates back and forth. 309 * @hide 310 */ 311 public class Translator { 312 @UnsupportedAppUsage 313 final public float applicationScale; 314 @UnsupportedAppUsage 315 final public float applicationInvertedScale; 316 317 private Rect mContentInsetsBuffer = null; 318 private Rect mVisibleInsetsBuffer = null; 319 private Region mTouchableAreaBuffer = null; 320 Translator(float applicationScale, float applicationInvertedScale)321 Translator(float applicationScale, float applicationInvertedScale) { 322 this.applicationScale = applicationScale; 323 this.applicationInvertedScale = applicationInvertedScale; 324 } 325 Translator()326 Translator() { 327 this(CompatibilityInfo.this.applicationScale, 328 CompatibilityInfo.this.applicationInvertedScale); 329 } 330 331 /** 332 * Translate the screen rect to the application frame. 333 */ 334 @UnsupportedAppUsage translateRectInScreenToAppWinFrame(Rect rect)335 public void translateRectInScreenToAppWinFrame(Rect rect) { 336 rect.scale(applicationInvertedScale); 337 } 338 339 /** 340 * Translate the region in window to screen. 341 */ 342 @UnsupportedAppUsage translateRegionInWindowToScreen(Region transparentRegion)343 public void translateRegionInWindowToScreen(Region transparentRegion) { 344 transparentRegion.scale(applicationScale); 345 } 346 347 /** 348 * Apply translation to the canvas that is necessary to draw the content. 349 */ 350 @UnsupportedAppUsage translateCanvas(Canvas canvas)351 public void translateCanvas(Canvas canvas) { 352 if (applicationScale == 1.5f) { 353 /* When we scale for compatibility, we can put our stretched 354 bitmaps and ninepatches on exacty 1/2 pixel boundaries, 355 which can give us inconsistent drawing due to imperfect 356 float precision in the graphics engine's inverse matrix. 357 358 As a work-around, we translate by a tiny amount to avoid 359 landing on exact pixel centers and boundaries, giving us 360 the slop we need to draw consistently. 361 362 This constant is meant to resolve to 1/255 after it is 363 scaled by 1.5 (applicationScale). Note, this is just a guess 364 as to what is small enough not to create its own artifacts, 365 and big enough to avoid the precision problems. Feel free 366 to experiment with smaller values as you choose. 367 */ 368 final float tinyOffset = 2.0f / (3 * 255); 369 canvas.translate(tinyOffset, tinyOffset); 370 } 371 canvas.scale(applicationScale, applicationScale); 372 } 373 374 /** 375 * Translate the motion event captured on screen to the application's window. 376 */ 377 @UnsupportedAppUsage translateEventInScreenToAppWindow(MotionEvent event)378 public void translateEventInScreenToAppWindow(MotionEvent event) { 379 event.scale(applicationInvertedScale); 380 } 381 382 /** 383 * Translate the window's layout parameter, from application's view to 384 * Screen's view. 385 */ 386 @UnsupportedAppUsage translateWindowLayout(WindowManager.LayoutParams params)387 public void translateWindowLayout(WindowManager.LayoutParams params) { 388 params.scale(applicationScale); 389 } 390 391 /** 392 * Translate a Rect in application's window to screen. 393 */ 394 @UnsupportedAppUsage translateRectInAppWindowToScreen(Rect rect)395 public void translateRectInAppWindowToScreen(Rect rect) { 396 rect.scale(applicationScale); 397 } 398 399 /** 400 * Translate a Rect in screen coordinates into the app window's coordinates. 401 */ 402 @UnsupportedAppUsage translateRectInScreenToAppWindow(Rect rect)403 public void translateRectInScreenToAppWindow(Rect rect) { 404 rect.scale(applicationInvertedScale); 405 } 406 407 /** 408 * Translate a Point in screen coordinates into the app window's coordinates. 409 */ translatePointInScreenToAppWindow(PointF point)410 public void translatePointInScreenToAppWindow(PointF point) { 411 final float scale = applicationInvertedScale; 412 if (scale != 1.0f) { 413 point.x *= scale; 414 point.y *= scale; 415 } 416 } 417 418 /** 419 * Translate the location of the sub window. 420 * @param params 421 */ translateLayoutParamsInAppWindowToScreen(LayoutParams params)422 public void translateLayoutParamsInAppWindowToScreen(LayoutParams params) { 423 params.scale(applicationScale); 424 } 425 426 /** 427 * Translate the content insets in application window to Screen. This uses 428 * the internal buffer for content insets to avoid extra object allocation. 429 */ 430 @UnsupportedAppUsage getTranslatedContentInsets(Rect contentInsets)431 public Rect getTranslatedContentInsets(Rect contentInsets) { 432 if (mContentInsetsBuffer == null) mContentInsetsBuffer = new Rect(); 433 mContentInsetsBuffer.set(contentInsets); 434 translateRectInAppWindowToScreen(mContentInsetsBuffer); 435 return mContentInsetsBuffer; 436 } 437 438 /** 439 * Translate the visible insets in application window to Screen. This uses 440 * the internal buffer for visible insets to avoid extra object allocation. 441 */ getTranslatedVisibleInsets(Rect visibleInsets)442 public Rect getTranslatedVisibleInsets(Rect visibleInsets) { 443 if (mVisibleInsetsBuffer == null) mVisibleInsetsBuffer = new Rect(); 444 mVisibleInsetsBuffer.set(visibleInsets); 445 translateRectInAppWindowToScreen(mVisibleInsetsBuffer); 446 return mVisibleInsetsBuffer; 447 } 448 449 /** 450 * Translate the touchable area in application window to Screen. This uses 451 * the internal buffer for touchable area to avoid extra object allocation. 452 */ getTranslatedTouchableArea(Region touchableArea)453 public Region getTranslatedTouchableArea(Region touchableArea) { 454 if (mTouchableAreaBuffer == null) mTouchableAreaBuffer = new Region(); 455 mTouchableAreaBuffer.set(touchableArea); 456 mTouchableAreaBuffer.scale(applicationScale); 457 return mTouchableAreaBuffer; 458 } 459 } 460 applyToDisplayMetrics(DisplayMetrics inoutDm)461 public void applyToDisplayMetrics(DisplayMetrics inoutDm) { 462 if (!supportsScreen()) { 463 // This is a larger screen device and the app is not 464 // compatible with large screens, so diddle it. 465 CompatibilityInfo.computeCompatibleScaling(inoutDm, inoutDm); 466 } else { 467 inoutDm.widthPixels = inoutDm.noncompatWidthPixels; 468 inoutDm.heightPixels = inoutDm.noncompatHeightPixels; 469 } 470 471 if (isScalingRequired()) { 472 float invertedRatio = applicationInvertedScale; 473 inoutDm.density = inoutDm.noncompatDensity * invertedRatio; 474 inoutDm.densityDpi = (int)((inoutDm.noncompatDensityDpi * invertedRatio) + .5f); 475 inoutDm.scaledDensity = inoutDm.noncompatScaledDensity * invertedRatio; 476 inoutDm.xdpi = inoutDm.noncompatXdpi * invertedRatio; 477 inoutDm.ydpi = inoutDm.noncompatYdpi * invertedRatio; 478 inoutDm.widthPixels = (int) (inoutDm.widthPixels * invertedRatio + 0.5f); 479 inoutDm.heightPixels = (int) (inoutDm.heightPixels * invertedRatio + 0.5f); 480 } 481 } 482 applyToConfiguration(int displayDensity, Configuration inoutConfig)483 public void applyToConfiguration(int displayDensity, Configuration inoutConfig) { 484 if (!supportsScreen()) { 485 // This is a larger screen device and the app is not 486 // compatible with large screens, so we are forcing it to 487 // run as if the screen is normal size. 488 inoutConfig.screenLayout = 489 (inoutConfig.screenLayout&~Configuration.SCREENLAYOUT_SIZE_MASK) 490 | Configuration.SCREENLAYOUT_SIZE_NORMAL; 491 inoutConfig.screenWidthDp = inoutConfig.compatScreenWidthDp; 492 inoutConfig.screenHeightDp = inoutConfig.compatScreenHeightDp; 493 inoutConfig.smallestScreenWidthDp = inoutConfig.compatSmallestScreenWidthDp; 494 } 495 inoutConfig.densityDpi = displayDensity; 496 if (isScalingRequired()) { 497 float invertedRatio = applicationInvertedScale; 498 inoutConfig.densityDpi = (int)((inoutConfig.densityDpi * invertedRatio) + .5f); 499 } 500 } 501 502 /** 503 * Compute the frame Rect for applications runs under compatibility mode. 504 * 505 * @param dm the display metrics used to compute the frame size. 506 * @param outDm If non-null the width and height will be set to their scaled values. 507 * @return Returns the scaling factor for the window. 508 */ 509 @UnsupportedAppUsage computeCompatibleScaling(DisplayMetrics dm, DisplayMetrics outDm)510 public static float computeCompatibleScaling(DisplayMetrics dm, DisplayMetrics outDm) { 511 final int width = dm.noncompatWidthPixels; 512 final int height = dm.noncompatHeightPixels; 513 int shortSize, longSize; 514 if (width < height) { 515 shortSize = width; 516 longSize = height; 517 } else { 518 shortSize = height; 519 longSize = width; 520 } 521 int newShortSize = (int)(DEFAULT_NORMAL_SHORT_DIMENSION * dm.density + 0.5f); 522 float aspect = ((float)longSize) / shortSize; 523 if (aspect > MAXIMUM_ASPECT_RATIO) { 524 aspect = MAXIMUM_ASPECT_RATIO; 525 } 526 int newLongSize = (int)(newShortSize * aspect + 0.5f); 527 int newWidth, newHeight; 528 if (width < height) { 529 newWidth = newShortSize; 530 newHeight = newLongSize; 531 } else { 532 newWidth = newLongSize; 533 newHeight = newShortSize; 534 } 535 536 float sw = width/(float)newWidth; 537 float sh = height/(float)newHeight; 538 float scale = sw < sh ? sw : sh; 539 if (scale < 1) { 540 scale = 1; 541 } 542 543 if (outDm != null) { 544 outDm.widthPixels = newWidth; 545 outDm.heightPixels = newHeight; 546 } 547 548 return scale; 549 } 550 551 @Override 552 public boolean equals(Object o) { 553 if (this == o) { 554 return true; 555 } 556 try { 557 CompatibilityInfo oc = (CompatibilityInfo)o; 558 if (mCompatibilityFlags != oc.mCompatibilityFlags) return false; 559 if (applicationDensity != oc.applicationDensity) return false; 560 if (applicationScale != oc.applicationScale) return false; 561 if (applicationInvertedScale != oc.applicationInvertedScale) return false; 562 return true; 563 } catch (ClassCastException e) { 564 return false; 565 } 566 } 567 568 @Override 569 public String toString() { 570 StringBuilder sb = new StringBuilder(128); 571 sb.append("{"); 572 sb.append(applicationDensity); 573 sb.append("dpi"); 574 if (isScalingRequired()) { 575 sb.append(" "); 576 sb.append(applicationScale); 577 sb.append("x"); 578 } 579 if (!supportsScreen()) { 580 sb.append(" resizing"); 581 } 582 if (neverSupportsScreen()) { 583 sb.append(" never-compat"); 584 } 585 if (alwaysSupportsScreen()) { 586 sb.append(" always-compat"); 587 } 588 sb.append("}"); 589 return sb.toString(); 590 } 591 592 @Override 593 public int hashCode() { 594 int result = 17; 595 result = 31 * result + mCompatibilityFlags; 596 result = 31 * result + applicationDensity; 597 result = 31 * result + Float.floatToIntBits(applicationScale); 598 result = 31 * result + Float.floatToIntBits(applicationInvertedScale); 599 return result; 600 } 601 602 @Override 603 public int describeContents() { 604 return 0; 605 } 606 607 @Override 608 public void writeToParcel(Parcel dest, int flags) { 609 dest.writeInt(mCompatibilityFlags); 610 dest.writeInt(applicationDensity); 611 dest.writeFloat(applicationScale); 612 dest.writeFloat(applicationInvertedScale); 613 } 614 615 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 616 public static final @android.annotation.NonNull Parcelable.Creator<CompatibilityInfo> CREATOR 617 = new Parcelable.Creator<CompatibilityInfo>() { 618 @Override 619 public CompatibilityInfo createFromParcel(Parcel source) { 620 return new CompatibilityInfo(source); 621 } 622 623 @Override 624 public CompatibilityInfo[] newArray(int size) { 625 return new CompatibilityInfo[size]; 626 } 627 }; 628 629 private CompatibilityInfo(Parcel source) { 630 mCompatibilityFlags = source.readInt(); 631 applicationDensity = source.readInt(); 632 applicationScale = source.readFloat(); 633 applicationInvertedScale = source.readFloat(); 634 } 635 } 636