1 /* 2 * Copyright (C) 2019 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 package com.android.launcher3.util; 17 18 import static android.content.Intent.ACTION_CONFIGURATION_CHANGED; 19 import static android.view.Display.DEFAULT_DISPLAY; 20 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; 21 22 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY; 23 import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE; 24 import static com.android.launcher3.InvariantDeviceProfile.TYPE_TABLET; 25 import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING; 26 import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_DESKTOP_MODE_KEY; 27 import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE; 28 import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_KEY; 29 import static com.android.launcher3.Utilities.dpiFromPx; 30 import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning; 31 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 32 import static com.android.launcher3.util.FlagDebugUtils.appendFlag; 33 import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH; 34 35 import android.annotation.SuppressLint; 36 import android.annotation.TargetApi; 37 import android.content.ComponentCallbacks; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.content.SharedPreferences; 41 import android.content.res.Configuration; 42 import android.graphics.Point; 43 import android.graphics.Rect; 44 import android.hardware.display.DisplayManager; 45 import android.os.Build; 46 import android.util.ArrayMap; 47 import android.util.ArraySet; 48 import android.util.Log; 49 import android.view.Display; 50 51 import androidx.annotation.AnyThread; 52 import androidx.annotation.UiThread; 53 import androidx.annotation.VisibleForTesting; 54 55 import com.android.launcher3.InvariantDeviceProfile.DeviceType; 56 import com.android.launcher3.LauncherPrefs; 57 import com.android.launcher3.Utilities; 58 import com.android.launcher3.logging.FileLog; 59 import com.android.launcher3.util.window.CachedDisplayInfo; 60 import com.android.launcher3.util.window.WindowManagerProxy; 61 62 import java.io.PrintWriter; 63 import java.util.ArrayList; 64 import java.util.Collections; 65 import java.util.List; 66 import java.util.Map; 67 import java.util.Objects; 68 import java.util.Set; 69 import java.util.StringJoiner; 70 71 /** 72 * Utility class to cache properties of default display to avoid a system RPC on every call. 73 */ 74 @SuppressLint("NewApi") 75 public class DisplayController implements ComponentCallbacks, SafeCloseable { 76 77 private static final String TAG = "DisplayController"; 78 private static final boolean DEBUG = false; 79 private static boolean sTransientTaskbarStatusForTests = true; 80 81 // TODO(b/254119092) remove all logs with this tag 82 public static final String TASKBAR_NOT_DESTROYED_TAG = "b/254119092"; 83 84 public static final MainThreadInitializedObject<DisplayController> INSTANCE = 85 new MainThreadInitializedObject<>(DisplayController::new); 86 87 public static final int CHANGE_ACTIVE_SCREEN = 1 << 0; 88 public static final int CHANGE_ROTATION = 1 << 1; 89 public static final int CHANGE_DENSITY = 1 << 2; 90 public static final int CHANGE_SUPPORTED_BOUNDS = 1 << 3; 91 public static final int CHANGE_NAVIGATION_MODE = 1 << 4; 92 public static final int CHANGE_TASKBAR_PINNING = 1 << 5; 93 public static final int CHANGE_DESKTOP_MODE = 1 << 6; 94 95 public static final int CHANGE_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION 96 | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS | CHANGE_NAVIGATION_MODE 97 | CHANGE_TASKBAR_PINNING | CHANGE_DESKTOP_MODE; 98 99 private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED"; 100 private static final String TARGET_OVERLAY_PACKAGE = "android"; 101 102 private final Context mContext; 103 private final DisplayManager mDM; 104 105 // Null for SDK < S 106 private final Context mWindowContext; 107 108 // The callback in this listener updates DeviceProfile, which other listeners might depend on 109 private DisplayInfoChangeListener mPriorityListener; 110 private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>(); 111 112 private final SimpleBroadcastReceiver mReceiver = new SimpleBroadcastReceiver(this::onIntent); 113 114 private Info mInfo; 115 private boolean mDestroyed = false; 116 117 private SharedPreferences.OnSharedPreferenceChangeListener 118 mTaskbarPinningPreferenceChangeListener; 119 120 @VisibleForTesting DisplayController(Context context)121 protected DisplayController(Context context) { 122 mContext = context; 123 mDM = context.getSystemService(DisplayManager.class); 124 125 if (enableTaskbarPinning()) { 126 attachTaskbarPinningSharedPreferenceChangeListener(mContext); 127 } 128 129 Display display = mDM.getDisplay(DEFAULT_DISPLAY); 130 if (Utilities.ATLEAST_S) { 131 mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null); 132 mWindowContext.registerComponentCallbacks(this); 133 } else { 134 mWindowContext = null; 135 mReceiver.register(mContext, ACTION_CONFIGURATION_CHANGED); 136 } 137 138 // Initialize navigation mode change listener 139 mReceiver.registerPkgActions(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED); 140 141 WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(context); 142 Context displayInfoContext = getDisplayInfoContext(display); 143 mInfo = new Info(displayInfoContext, wmProxy, 144 wmProxy.estimateInternalDisplayBounds(displayInfoContext)); 145 FileLog.i(TAG, "(CTOR) perDisplayBounds: " + mInfo.mPerDisplayBounds); 146 } 147 attachTaskbarPinningSharedPreferenceChangeListener(Context context)148 private void attachTaskbarPinningSharedPreferenceChangeListener(Context context) { 149 mTaskbarPinningPreferenceChangeListener = 150 (sharedPreferences, key) -> { 151 LauncherPrefs prefs = LauncherPrefs.get(mContext); 152 boolean isTaskbarPinningChanged = TASKBAR_PINNING_KEY.equals(key) 153 && mInfo.mIsTaskbarPinned != prefs.get(TASKBAR_PINNING); 154 boolean isTaskbarPinningDesktopModeChanged = 155 TASKBAR_PINNING_DESKTOP_MODE_KEY.equals(key) 156 && mInfo.mIsTaskbarPinnedInDesktopMode != prefs.get( 157 TASKBAR_PINNING_IN_DESKTOP_MODE); 158 if (isTaskbarPinningChanged || isTaskbarPinningDesktopModeChanged) { 159 handleInfoChange(mWindowContext.getDisplay()); 160 } 161 }; 162 163 LauncherPrefs.get(context).addListener( 164 mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING); 165 LauncherPrefs.get(context).addListener( 166 mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING_IN_DESKTOP_MODE); 167 } 168 169 /** 170 * Returns the current navigation mode 171 */ getNavigationMode(Context context)172 public static NavigationMode getNavigationMode(Context context) { 173 return INSTANCE.get(context).getInfo().getNavigationMode(); 174 } 175 176 /** 177 * Returns whether taskbar is transient or persistent. 178 * 179 * @return {@code true} if transient, {@code false} if persistent. 180 */ isTransientTaskbar(Context context)181 public static boolean isTransientTaskbar(Context context) { 182 return INSTANCE.get(context).getInfo().isTransientTaskbar(); 183 } 184 185 /** 186 * Handles info change for desktop mode. 187 */ handleInfoChangeForDesktopMode(Context context)188 public static void handleInfoChangeForDesktopMode(Context context) { 189 INSTANCE.get(context).handleInfoChange(context.getDisplay()); 190 } 191 192 /** 193 * Enables transient taskbar status for tests. 194 */ 195 @VisibleForTesting enableTransientTaskbarForTests(boolean enable)196 public static void enableTransientTaskbarForTests(boolean enable) { 197 sTransientTaskbarStatusForTests = enable; 198 } 199 200 /** 201 * Returns whether the taskbar is pinned in gesture navigation mode. 202 */ isPinnedTaskbar(Context context)203 public static boolean isPinnedTaskbar(Context context) { 204 return INSTANCE.get(context).getInfo().isPinnedTaskbar(); 205 } 206 207 @Override close()208 public void close() { 209 mDestroyed = true; 210 if (enableTaskbarPinning()) { 211 LauncherPrefs.get(mContext).removeListener( 212 mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING); 213 LauncherPrefs.get(mContext).removeListener( 214 mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING_IN_DESKTOP_MODE); 215 } 216 if (mWindowContext != null) { 217 mWindowContext.unregisterComponentCallbacks(this); 218 } else { 219 // TODO: unregister broadcast receiver 220 } 221 } 222 223 /** 224 * Interface for listening for display changes 225 */ 226 public interface DisplayInfoChangeListener { 227 228 /** 229 * Invoked when display info has changed. 230 * @param context updated context associated with the display. 231 * @param info updated display information. 232 * @param flags bitmask indicating type of change. 233 */ onDisplayInfoChanged(Context context, Info info, int flags)234 void onDisplayInfoChanged(Context context, Info info, int flags); 235 } 236 onIntent(Intent intent)237 private void onIntent(Intent intent) { 238 if (mDestroyed) { 239 return; 240 } 241 boolean reconfigure = false; 242 if (ACTION_OVERLAY_CHANGED.equals(intent.getAction())) { 243 reconfigure = true; 244 } else if (ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) { 245 Configuration config = mContext.getResources().getConfiguration(); 246 reconfigure = mInfo.fontScale != config.fontScale 247 || mInfo.densityDpi != config.densityDpi; 248 } 249 250 if (reconfigure) { 251 Log.d(TAG, "Configuration changed, notifying listeners"); 252 Display display = mDM.getDisplay(DEFAULT_DISPLAY); 253 if (display != null) { 254 handleInfoChange(display); 255 } 256 } 257 } 258 259 @UiThread 260 @Override 261 @TargetApi(Build.VERSION_CODES.S) onConfigurationChanged(Configuration config)262 public final void onConfigurationChanged(Configuration config) { 263 Log.d(TASKBAR_NOT_DESTROYED_TAG, "DisplayController#onConfigurationChanged: " + config); 264 Display display = mWindowContext.getDisplay(); 265 if (config.densityDpi != mInfo.densityDpi 266 || config.fontScale != mInfo.fontScale 267 || display.getRotation() != mInfo.rotation 268 || !mInfo.mScreenSizeDp.equals( 269 new PortraitSize(config.screenHeightDp, config.screenWidthDp))) { 270 handleInfoChange(display); 271 } 272 } 273 274 @Override onLowMemory()275 public final void onLowMemory() { } 276 setPriorityListener(DisplayInfoChangeListener listener)277 public void setPriorityListener(DisplayInfoChangeListener listener) { 278 mPriorityListener = listener; 279 } 280 addChangeListener(DisplayInfoChangeListener listener)281 public void addChangeListener(DisplayInfoChangeListener listener) { 282 mListeners.add(listener); 283 } 284 removeChangeListener(DisplayInfoChangeListener listener)285 public void removeChangeListener(DisplayInfoChangeListener listener) { 286 mListeners.remove(listener); 287 } 288 getInfo()289 public Info getInfo() { 290 return mInfo; 291 } 292 getDisplayInfoContext(Display display)293 private Context getDisplayInfoContext(Display display) { 294 return Utilities.ATLEAST_S ? mWindowContext : mContext.createDisplayContext(display); 295 } 296 297 @AnyThread 298 @VisibleForTesting handleInfoChange(Display display)299 public void handleInfoChange(Display display) { 300 WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(mContext); 301 Info oldInfo = mInfo; 302 303 Context displayInfoContext = getDisplayInfoContext(display); 304 Info newInfo = new Info(displayInfoContext, wmProxy, oldInfo.mPerDisplayBounds); 305 306 if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale 307 || newInfo.getNavigationMode() != oldInfo.getNavigationMode()) { 308 // Cache may not be valid anymore, recreate without cache 309 newInfo = new Info(displayInfoContext, wmProxy, 310 wmProxy.estimateInternalDisplayBounds(displayInfoContext)); 311 } 312 313 int change = 0; 314 if (!newInfo.normalizedDisplayInfo.equals(oldInfo.normalizedDisplayInfo)) { 315 change |= CHANGE_ACTIVE_SCREEN; 316 } 317 if (newInfo.rotation != oldInfo.rotation) { 318 change |= CHANGE_ROTATION; 319 } 320 if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale) { 321 change |= CHANGE_DENSITY; 322 } 323 if (newInfo.getNavigationMode() != oldInfo.getNavigationMode()) { 324 change |= CHANGE_NAVIGATION_MODE; 325 } 326 if (!newInfo.supportedBounds.equals(oldInfo.supportedBounds) 327 || !newInfo.mPerDisplayBounds.equals(oldInfo.mPerDisplayBounds)) { 328 change |= CHANGE_SUPPORTED_BOUNDS; 329 FileLog.w(TAG, 330 "(CHANGE_SUPPORTED_BOUNDS) perDisplayBounds: " + newInfo.mPerDisplayBounds); 331 } 332 if ((newInfo.mIsTaskbarPinned != oldInfo.mIsTaskbarPinned) 333 || (newInfo.mIsTaskbarPinnedInDesktopMode 334 != oldInfo.mIsTaskbarPinnedInDesktopMode)) { 335 change |= CHANGE_TASKBAR_PINNING; 336 } 337 if (newInfo.mIsInDesktopMode != oldInfo.mIsInDesktopMode) { 338 change |= CHANGE_DESKTOP_MODE; 339 } 340 341 if (DEBUG) { 342 Log.d(TAG, "handleInfoChange - change: " + getChangeFlagsString(change)); 343 } 344 345 if (change != 0) { 346 mInfo = newInfo; 347 final int flags = change; 348 MAIN_EXECUTOR.execute(() -> notifyChange(displayInfoContext, flags)); 349 } 350 } 351 notifyChange(Context context, int flags)352 private void notifyChange(Context context, int flags) { 353 if (mPriorityListener != null) { 354 mPriorityListener.onDisplayInfoChanged(context, mInfo, flags); 355 } 356 357 int count = mListeners.size(); 358 for (int i = 0; i < count; i++) { 359 mListeners.get(i).onDisplayInfoChanged(context, mInfo, flags); 360 } 361 } 362 363 public static class Info { 364 365 // Cached property 366 public final CachedDisplayInfo normalizedDisplayInfo; 367 public final int rotation; 368 public final Point currentSize; 369 public final Rect cutout; 370 371 // Configuration property 372 public final float fontScale; 373 private final int densityDpi; 374 private final NavigationMode navigationMode; 375 private final PortraitSize mScreenSizeDp; 376 377 // WindowBounds 378 public final WindowBounds realBounds; 379 public final Set<WindowBounds> supportedBounds = new ArraySet<>(); 380 private final ArrayMap<CachedDisplayInfo, List<WindowBounds>> mPerDisplayBounds = 381 new ArrayMap<>(); 382 383 private final boolean mIsTaskbarPinned; 384 private final boolean mIsTaskbarPinnedInDesktopMode; 385 386 private final boolean mIsInDesktopMode; 387 Info(Context displayInfoContext)388 public Info(Context displayInfoContext) { 389 /* don't need system overrides for external displays */ 390 this(displayInfoContext, new WindowManagerProxy(), new ArrayMap<>()); 391 } 392 393 // Used for testing Info(Context displayInfoContext, WindowManagerProxy wmProxy, Map<CachedDisplayInfo, List<WindowBounds>> perDisplayBoundsCache)394 public Info(Context displayInfoContext, 395 WindowManagerProxy wmProxy, 396 Map<CachedDisplayInfo, List<WindowBounds>> perDisplayBoundsCache) { 397 CachedDisplayInfo displayInfo = wmProxy.getDisplayInfo(displayInfoContext); 398 normalizedDisplayInfo = displayInfo.normalize(wmProxy); 399 rotation = displayInfo.rotation; 400 currentSize = displayInfo.size; 401 cutout = WindowManagerProxy.getSafeInsets(displayInfo.cutout); 402 403 Configuration config = displayInfoContext.getResources().getConfiguration(); 404 fontScale = config.fontScale; 405 densityDpi = config.densityDpi; 406 mScreenSizeDp = new PortraitSize(config.screenHeightDp, config.screenWidthDp); 407 navigationMode = wmProxy.getNavigationMode(displayInfoContext); 408 409 mPerDisplayBounds.putAll(perDisplayBoundsCache); 410 List<WindowBounds> cachedValue = getCurrentBounds(); 411 412 realBounds = wmProxy.getRealBounds(displayInfoContext, displayInfo); 413 if (cachedValue == null) { 414 // Unexpected normalizedDisplayInfo is found, recreate the cache 415 FileLog.e(TAG, "Unexpected normalizedDisplayInfo found, invalidating cache: " 416 + normalizedDisplayInfo); 417 FileLog.e(TAG, "(Invalid Cache) perDisplayBounds : " + mPerDisplayBounds); 418 mPerDisplayBounds.clear(); 419 mPerDisplayBounds.putAll(wmProxy.estimateInternalDisplayBounds(displayInfoContext)); 420 cachedValue = getCurrentBounds(); 421 if (cachedValue == null) { 422 FileLog.e(TAG, "normalizedDisplayInfo not found in estimation: " 423 + normalizedDisplayInfo); 424 supportedBounds.add(realBounds); 425 } 426 } 427 428 if (cachedValue != null) { 429 // Verify that the real bounds are a match 430 WindowBounds expectedBounds = cachedValue.get(displayInfo.rotation); 431 if (!realBounds.equals(expectedBounds)) { 432 List<WindowBounds> clone = new ArrayList<>(cachedValue); 433 clone.set(displayInfo.rotation, realBounds); 434 mPerDisplayBounds.put(normalizedDisplayInfo, clone); 435 } 436 } 437 mPerDisplayBounds.values().forEach(supportedBounds::addAll); 438 if (DEBUG) { 439 Log.d(TAG, "displayInfo: " + displayInfo); 440 Log.d(TAG, "realBounds: " + realBounds); 441 Log.d(TAG, "normalizedDisplayInfo: " + normalizedDisplayInfo); 442 Log.d(TAG, "perDisplayBounds: " + mPerDisplayBounds); 443 } 444 445 mIsTaskbarPinned = LauncherPrefs.get(displayInfoContext).get(TASKBAR_PINNING); 446 mIsTaskbarPinnedInDesktopMode = LauncherPrefs.get(displayInfoContext).get( 447 TASKBAR_PINNING_IN_DESKTOP_MODE); 448 mIsInDesktopMode = wmProxy.isInDesktopMode(); 449 } 450 451 /** 452 * Returns whether taskbar is transient. 453 */ isTransientTaskbar()454 public boolean isTransientTaskbar() { 455 if (navigationMode != NavigationMode.NO_BUTTON) { 456 return false; 457 } 458 if (Utilities.isRunningInTestHarness()) { 459 // TODO(b/258604917): Once ENABLE_TASKBAR_PINNING is enabled, remove usage of 460 // sTransientTaskbarStatusForTests and update test to directly 461 // toggle shared preference to switch transient taskbar on/off. 462 return sTransientTaskbarStatusForTests; 463 } 464 if (enableTaskbarPinning()) { 465 if (mIsInDesktopMode) { 466 return !mIsTaskbarPinnedInDesktopMode; 467 } 468 return !mIsTaskbarPinned; 469 } 470 return true; 471 } 472 473 /** 474 * Returns whether the taskbar is pinned in gesture navigation mode. 475 */ isPinnedTaskbar()476 public boolean isPinnedTaskbar() { 477 return navigationMode == NavigationMode.NO_BUTTON && !isTransientTaskbar(); 478 } 479 isInDesktopMode()480 public boolean isInDesktopMode() { 481 return mIsInDesktopMode; 482 } 483 484 /** 485 * Returns {@code true} if the bounds represent a tablet. 486 */ isTablet(WindowBounds bounds)487 public boolean isTablet(WindowBounds bounds) { 488 return smallestSizeDp(bounds) >= MIN_TABLET_WIDTH; 489 } 490 491 /** Getter for {@link #navigationMode} to allow mocking. */ getNavigationMode()492 public NavigationMode getNavigationMode() { 493 return navigationMode; 494 } 495 496 /** 497 * Returns smallest size in dp for given bounds. 498 */ smallestSizeDp(WindowBounds bounds)499 public float smallestSizeDp(WindowBounds bounds) { 500 return dpiFromPx(Math.min(bounds.bounds.width(), bounds.bounds.height()), densityDpi); 501 } 502 503 /** 504 * Returns all displays for the device 505 */ getAllDisplays()506 public Set<CachedDisplayInfo> getAllDisplays() { 507 return Collections.unmodifiableSet(mPerDisplayBounds.keySet()); 508 } 509 510 /** 511 * Returns all {@link WindowBounds}s for the current display. 512 */ getCurrentBounds()513 public List<WindowBounds> getCurrentBounds() { 514 return mPerDisplayBounds.get(normalizedDisplayInfo); 515 } 516 getDensityDpi()517 public int getDensityDpi() { 518 return densityDpi; 519 } 520 getDeviceType()521 public @DeviceType int getDeviceType() { 522 int flagPhone = 1 << 0; 523 int flagTablet = 1 << 1; 524 525 int type = supportedBounds.stream() 526 .mapToInt(bounds -> isTablet(bounds) ? flagTablet : flagPhone) 527 .reduce(0, (a, b) -> a | b); 528 if (type == (flagPhone | flagTablet)) { 529 // device has profiles supporting both phone and tablet modes 530 return TYPE_MULTI_DISPLAY; 531 } else if (type == flagTablet) { 532 return TYPE_TABLET; 533 } else { 534 return TYPE_PHONE; 535 } 536 } 537 } 538 539 /** 540 * Returns the given binary flags as a human-readable string. 541 * @see #CHANGE_ALL 542 */ getChangeFlagsString(int change)543 public String getChangeFlagsString(int change) { 544 StringJoiner result = new StringJoiner("|"); 545 appendFlag(result, change, CHANGE_ACTIVE_SCREEN, "CHANGE_ACTIVE_SCREEN"); 546 appendFlag(result, change, CHANGE_ROTATION, "CHANGE_ROTATION"); 547 appendFlag(result, change, CHANGE_DENSITY, "CHANGE_DENSITY"); 548 appendFlag(result, change, CHANGE_SUPPORTED_BOUNDS, "CHANGE_SUPPORTED_BOUNDS"); 549 appendFlag(result, change, CHANGE_NAVIGATION_MODE, "CHANGE_NAVIGATION_MODE"); 550 appendFlag(result, change, CHANGE_TASKBAR_PINNING, "CHANGE_TASKBAR_VARIANT"); 551 appendFlag(result, change, CHANGE_DESKTOP_MODE, "CHANGE_DESKTOP_MODE"); 552 return result.toString(); 553 } 554 555 /** 556 * Dumps the current state information 557 */ dump(PrintWriter pw)558 public void dump(PrintWriter pw) { 559 Info info = mInfo; 560 pw.println("DisplayController.Info:"); 561 pw.println(" normalizedDisplayInfo=" + info.normalizedDisplayInfo); 562 pw.println(" rotation=" + info.rotation); 563 pw.println(" fontScale=" + info.fontScale); 564 pw.println(" densityDpi=" + info.densityDpi); 565 pw.println(" navigationMode=" + info.getNavigationMode().name()); 566 pw.println(" isTaskbarPinned=" + info.mIsTaskbarPinned); 567 pw.println(" isTaskbarPinnedInDesktopMode=" + info.mIsTaskbarPinnedInDesktopMode); 568 pw.println(" isInDesktopMode=" + info.mIsInDesktopMode); 569 pw.println(" currentSize=" + info.currentSize); 570 info.mPerDisplayBounds.forEach((key, value) -> pw.println( 571 " perDisplayBounds - " + key + ": " + value)); 572 pw.println(" isTransientTaskbar=" + info.isTransientTaskbar()); 573 } 574 575 /** 576 * Utility class to hold a size information in an orientation independent way 577 */ 578 public static class PortraitSize { 579 public final int width, height; 580 PortraitSize(int w, int h)581 public PortraitSize(int w, int h) { 582 width = Math.min(w, h); 583 height = Math.max(w, h); 584 } 585 586 @Override equals(Object o)587 public boolean equals(Object o) { 588 if (this == o) return true; 589 if (o == null || getClass() != o.getClass()) return false; 590 PortraitSize that = (PortraitSize) o; 591 return width == that.width && height == that.height; 592 } 593 594 @Override hashCode()595 public int hashCode() { 596 return Objects.hash(width, height); 597 } 598 } 599 600 } 601