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.view.Display.DEFAULT_DISPLAY; 19 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; 20 21 import static com.android.launcher3.Flags.enableOverviewOnConnectedDisplays; 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.content.ComponentCallbacks; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.content.res.Configuration; 40 import android.graphics.Point; 41 import android.graphics.Rect; 42 import android.hardware.display.DisplayManager; 43 import android.util.ArrayMap; 44 import android.util.ArraySet; 45 import android.util.Log; 46 import android.util.SparseArray; 47 import android.view.Display; 48 49 import androidx.annotation.AnyThread; 50 import androidx.annotation.NonNull; 51 import androidx.annotation.Nullable; 52 import androidx.annotation.UiThread; 53 import androidx.annotation.VisibleForTesting; 54 55 import com.android.launcher3.InvariantDeviceProfile.DeviceType; 56 import com.android.launcher3.LauncherPrefChangeListener; 57 import com.android.launcher3.LauncherPrefs; 58 import com.android.launcher3.Utilities; 59 import com.android.launcher3.dagger.ApplicationContext; 60 import com.android.launcher3.dagger.LauncherAppComponent; 61 import com.android.launcher3.dagger.LauncherAppSingleton; 62 import com.android.launcher3.logging.FileLog; 63 import com.android.launcher3.util.window.CachedDisplayInfo; 64 import com.android.launcher3.util.window.WindowManagerProxy; 65 import com.android.launcher3.util.window.WindowManagerProxy.DesktopVisibilityListener; 66 67 import java.io.PrintWriter; 68 import java.util.ArrayList; 69 import java.util.Arrays; 70 import java.util.Collections; 71 import java.util.List; 72 import java.util.Locale; 73 import java.util.Map; 74 import java.util.Objects; 75 import java.util.Set; 76 import java.util.StringJoiner; 77 import java.util.concurrent.CopyOnWriteArrayList; 78 79 import javax.inject.Inject; 80 81 /** 82 * Utility class to cache properties of default display to avoid a system RPC on every call. 83 */ 84 @SuppressLint("NewApi") 85 @LauncherAppSingleton 86 public class DisplayController implements DesktopVisibilityListener { 87 88 private static final String TAG = "DisplayController"; 89 private static final boolean DEBUG = false; 90 private static boolean sTaskbarModePreferenceStatusForTests = false; 91 private static boolean sTransientTaskbarStatusForTests = true; 92 93 // TODO(b/254119092) remove all logs with this tag 94 public static final String TASKBAR_NOT_DESTROYED_TAG = "b/254119092"; 95 96 public static final DaggerSingletonObject<DisplayController> INSTANCE = 97 new DaggerSingletonObject<>(LauncherAppComponent::getDisplayController); 98 99 public static final int CHANGE_ACTIVE_SCREEN = 1 << 0; 100 public static final int CHANGE_ROTATION = 1 << 1; 101 public static final int CHANGE_DENSITY = 1 << 2; 102 public static final int CHANGE_SUPPORTED_BOUNDS = 1 << 3; 103 public static final int CHANGE_NAVIGATION_MODE = 1 << 4; 104 public static final int CHANGE_TASKBAR_PINNING = 1 << 5; 105 public static final int CHANGE_DESKTOP_MODE = 1 << 6; 106 public static final int CHANGE_SHOW_LOCKED_TASKBAR = 1 << 7; 107 108 public static final int CHANGE_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION 109 | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS | CHANGE_NAVIGATION_MODE 110 | CHANGE_TASKBAR_PINNING | CHANGE_DESKTOP_MODE | CHANGE_SHOW_LOCKED_TASKBAR; 111 112 private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED"; 113 private static final String TARGET_OVERLAY_PACKAGE = "android"; 114 115 private final WindowManagerProxy mWMProxy; 116 117 private final @ApplicationContext Context mAppContext; 118 119 // The callback in this listener updates DeviceProfile, which other listeners might depend on 120 private DisplayInfoChangeListener mPriorityListener; 121 122 private final SparseArray<PerDisplayInfo> mPerDisplayInfo = 123 new SparseArray<>(); 124 125 // We will register broadcast receiver on main thread to ensure not missing changes on 126 // TARGET_OVERLAY_PACKAGE and ACTION_OVERLAY_CHANGED. 127 private final SimpleBroadcastReceiver mReceiver; 128 129 private boolean mDestroyed = false; 130 131 @Inject DisplayController(@pplicationContext Context context, WindowManagerProxy wmProxy, LauncherPrefs prefs, DaggerSingletonTracker lifecycle)132 protected DisplayController(@ApplicationContext Context context, 133 WindowManagerProxy wmProxy, 134 LauncherPrefs prefs, 135 DaggerSingletonTracker lifecycle) { 136 mAppContext = context; 137 mWMProxy = wmProxy; 138 139 if (enableTaskbarPinning()) { 140 LauncherPrefChangeListener prefListener = key -> { 141 Info info = getInfo(); 142 boolean isTaskbarPinningChanged = TASKBAR_PINNING_KEY.equals(key) 143 && info.mIsTaskbarPinned != prefs.get(TASKBAR_PINNING); 144 boolean isTaskbarPinningDesktopModeChanged = 145 TASKBAR_PINNING_DESKTOP_MODE_KEY.equals(key) 146 && info.mIsTaskbarPinnedInDesktopMode != prefs.get( 147 TASKBAR_PINNING_IN_DESKTOP_MODE); 148 if (isTaskbarPinningChanged || isTaskbarPinningDesktopModeChanged) { 149 notifyConfigChange(DEFAULT_DISPLAY); 150 } 151 }; 152 153 prefs.addListener(prefListener, TASKBAR_PINNING); 154 prefs.addListener(prefListener, TASKBAR_PINNING_IN_DESKTOP_MODE); 155 lifecycle.addCloseable(() -> prefs.removeListener( 156 prefListener, TASKBAR_PINNING, TASKBAR_PINNING_IN_DESKTOP_MODE)); 157 } 158 159 DisplayManager displayManager = context.getSystemService(DisplayManager.class); 160 Display defaultDisplay = displayManager.getDisplay(DEFAULT_DISPLAY); 161 PerDisplayInfo defaultPerDisplayInfo = getOrCreatePerDisplayInfo(defaultDisplay); 162 163 // Initialize navigation mode change listener 164 mReceiver = new SimpleBroadcastReceiver(context, MAIN_EXECUTOR, this::onIntent); 165 mReceiver.registerPkgActions(TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED); 166 167 wmProxy.registerDesktopVisibilityListener(this); 168 FileLog.i(TAG, "(CTOR) perDisplayBounds: " 169 + defaultPerDisplayInfo.mInfo.mPerDisplayBounds); 170 171 if (enableOverviewOnConnectedDisplays()) { 172 final DisplayManager.DisplayListener displayListener = 173 new DisplayManager.DisplayListener() { 174 @Override 175 public void onDisplayAdded(int displayId) { 176 getOrCreatePerDisplayInfo(displayManager.getDisplay(displayId)); 177 } 178 179 @Override 180 public void onDisplayChanged(int displayId) { 181 } 182 183 @Override 184 public void onDisplayRemoved(int displayId) { 185 removePerDisplayInfo(displayId); 186 } 187 }; 188 displayManager.registerDisplayListener(displayListener, MAIN_EXECUTOR.getHandler()); 189 lifecycle.addCloseable(() -> { 190 displayManager.unregisterDisplayListener(displayListener); 191 }); 192 // Add any PerDisplayInfos for already-connected displays. 193 Arrays.stream(displayManager.getDisplays()) 194 .forEach((it) -> 195 getOrCreatePerDisplayInfo( 196 displayManager.getDisplay(it.getDisplayId()))); 197 } 198 199 lifecycle.addCloseable(() -> { 200 mDestroyed = true; 201 defaultPerDisplayInfo.cleanup(); 202 mReceiver.unregisterReceiverSafely(); 203 wmProxy.unregisterDesktopVisibilityListener(this); 204 }); 205 } 206 207 /** 208 * Returns the current navigation mode 209 */ getNavigationMode(Context context)210 public static NavigationMode getNavigationMode(Context context) { 211 return INSTANCE.get(context).getInfo().getNavigationMode(); 212 } 213 214 /** 215 * Returns whether taskbar is transient or persistent. 216 * 217 * @return {@code true} if transient, {@code false} if persistent. 218 */ isTransientTaskbar(Context context)219 public static boolean isTransientTaskbar(Context context) { 220 return INSTANCE.get(context).getInfo().isTransientTaskbar(); 221 } 222 223 /** 224 * Enables transient taskbar status for tests. 225 */ 226 @VisibleForTesting enableTransientTaskbarForTests(boolean enable)227 public static void enableTransientTaskbarForTests(boolean enable) { 228 sTransientTaskbarStatusForTests = enable; 229 } 230 231 /** 232 * Enables respecting taskbar mode preference during test. 233 */ 234 @VisibleForTesting enableTaskbarModePreferenceForTests(boolean enable)235 public static void enableTaskbarModePreferenceForTests(boolean enable) { 236 sTaskbarModePreferenceStatusForTests = enable; 237 } 238 239 /** 240 * Returns whether the taskbar is pinned in gesture navigation mode. 241 */ isPinnedTaskbar(Context context)242 public static boolean isPinnedTaskbar(Context context) { 243 return INSTANCE.get(context).getInfo().isPinnedTaskbar(); 244 } 245 246 /** 247 * Returns whether the taskbar is pinned in gesture navigation mode. 248 */ isInDesktopMode(Context context)249 public static boolean isInDesktopMode(Context context) { 250 return INSTANCE.get(context).getInfo().isInDesktopMode(); 251 } 252 253 /** 254 * Returns whether the taskbar is forced to be pinned when home is visible. 255 */ showLockedTaskbarOnHome(Context context)256 public static boolean showLockedTaskbarOnHome(Context context) { 257 return INSTANCE.get(context).getInfo().showLockedTaskbarOnHome(); 258 } 259 260 /** 261 * Returns whether desktop taskbar (pinned taskbar that shows desktop tasks) is to be used 262 * on the display because the display is a freeform display. 263 */ showDesktopTaskbarForFreeformDisplay(Context context)264 public static boolean showDesktopTaskbarForFreeformDisplay(Context context) { 265 return INSTANCE.get(context).getInfo().showDesktopTaskbarForFreeformDisplay(); 266 } 267 268 @Override onIsInDesktopModeChanged(int displayId, boolean isInDesktopModeAndNotInOverview)269 public void onIsInDesktopModeChanged(int displayId, boolean isInDesktopModeAndNotInOverview) { 270 notifyConfigChange(displayId); 271 } 272 273 /** 274 * Interface for listening for display changes 275 */ 276 public interface DisplayInfoChangeListener { 277 278 /** 279 * Invoked when display info has changed. 280 * @param context updated context associated with the display. 281 * @param info updated display information. 282 * @param flags bitmask indicating type of change. 283 */ onDisplayInfoChanged(Context context, Info info, int flags)284 void onDisplayInfoChanged(Context context, Info info, int flags); 285 } 286 onIntent(Intent intent)287 private void onIntent(Intent intent) { 288 if (mDestroyed) { 289 return; 290 } 291 if (ACTION_OVERLAY_CHANGED.equals(intent.getAction())) { 292 Log.d(TAG, "Overlay changed, notifying listeners"); 293 notifyConfigChange(DEFAULT_DISPLAY); 294 } 295 } 296 297 @VisibleForTesting onConfigurationChanged(Configuration config)298 public void onConfigurationChanged(Configuration config) { 299 onConfigurationChanged(config, DEFAULT_DISPLAY); 300 } 301 302 @UiThread onConfigurationChanged(Configuration config, int displayId)303 private void onConfigurationChanged(Configuration config, int displayId) { 304 Log.d(TASKBAR_NOT_DESTROYED_TAG, "DisplayController#onConfigurationChanged: " + config); 305 PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId); 306 Context windowContext = perDisplayInfo.mWindowContext; 307 Info info = perDisplayInfo.mInfo; 308 if (config.densityDpi != info.densityDpi 309 || config.fontScale != info.fontScale 310 || !info.mScreenSizeDp.equals( 311 new PortraitSize(config.screenHeightDp, config.screenWidthDp)) 312 || windowContext.getDisplay().getRotation() != info.rotation 313 || mWMProxy.showLockedTaskbarOnHome(windowContext) 314 != info.showLockedTaskbarOnHome() 315 || mWMProxy.showDesktopTaskbarForFreeformDisplay(windowContext) 316 != info.showDesktopTaskbarForFreeformDisplay()) { 317 notifyConfigChange(displayId); 318 } 319 } 320 setPriorityListener(DisplayInfoChangeListener listener)321 public void setPriorityListener(DisplayInfoChangeListener listener) { 322 mPriorityListener = listener; 323 } 324 addChangeListener(DisplayInfoChangeListener listener)325 public void addChangeListener(DisplayInfoChangeListener listener) { 326 addChangeListenerForDisplay(listener, DEFAULT_DISPLAY); 327 } 328 removeChangeListener(DisplayInfoChangeListener listener)329 public void removeChangeListener(DisplayInfoChangeListener listener) { 330 removeChangeListenerForDisplay(listener, DEFAULT_DISPLAY); 331 } 332 addChangeListenerForDisplay(DisplayInfoChangeListener listener, int displayId)333 public void addChangeListenerForDisplay(DisplayInfoChangeListener listener, int displayId) { 334 PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId); 335 if (perDisplayInfo != null) { 336 perDisplayInfo.addListener(listener); 337 } 338 } 339 removeChangeListenerForDisplay(DisplayInfoChangeListener listener, int displayId)340 public void removeChangeListenerForDisplay(DisplayInfoChangeListener listener, int displayId) { 341 PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId); 342 if (perDisplayInfo != null) { 343 perDisplayInfo.removeListener(listener); 344 } 345 } 346 getInfo()347 public Info getInfo() { 348 return mPerDisplayInfo.get(DEFAULT_DISPLAY).mInfo; 349 } 350 getInfoForDisplay(int displayId)351 public @Nullable Info getInfoForDisplay(int displayId) { 352 if (enableOverviewOnConnectedDisplays()) { 353 PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId); 354 if (perDisplayInfo != null) { 355 return perDisplayInfo.mInfo; 356 } else { 357 return null; 358 } 359 } else { 360 return getInfo(); 361 } 362 } 363 364 @AnyThread notifyConfigChange()365 public void notifyConfigChange() { 366 notifyConfigChange(DEFAULT_DISPLAY); 367 } 368 369 @AnyThread notifyConfigChange(int displayId)370 public void notifyConfigChange(int displayId) { 371 notifyConfigChangeForDisplay(displayId); 372 } 373 calculateChange(Info oldInfo, Info newInfo)374 private int calculateChange(Info oldInfo, Info newInfo) { 375 int change = 0; 376 if (!newInfo.normalizedDisplayInfo.equals(oldInfo.normalizedDisplayInfo)) { 377 change |= CHANGE_ACTIVE_SCREEN; 378 } 379 if (newInfo.rotation != oldInfo.rotation) { 380 change |= CHANGE_ROTATION; 381 } 382 if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale) { 383 change |= CHANGE_DENSITY; 384 } 385 if (newInfo.getNavigationMode() != oldInfo.getNavigationMode()) { 386 change |= CHANGE_NAVIGATION_MODE; 387 } 388 if (!newInfo.supportedBounds.equals(oldInfo.supportedBounds) 389 || !newInfo.mPerDisplayBounds.equals(oldInfo.mPerDisplayBounds)) { 390 change |= CHANGE_SUPPORTED_BOUNDS; 391 FileLog.w(TAG, 392 "(CHANGE_SUPPORTED_BOUNDS) perDisplayBounds: " + newInfo.mPerDisplayBounds); 393 } 394 if ((newInfo.mIsTaskbarPinned != oldInfo.mIsTaskbarPinned) 395 || (newInfo.mIsTaskbarPinnedInDesktopMode 396 != oldInfo.mIsTaskbarPinnedInDesktopMode) 397 || newInfo.isPinnedTaskbar() != oldInfo.isPinnedTaskbar()) { 398 change |= CHANGE_TASKBAR_PINNING; 399 } 400 if (newInfo.mIsInDesktopMode != oldInfo.mIsInDesktopMode) { 401 change |= CHANGE_DESKTOP_MODE; 402 } 403 if (newInfo.mShowLockedTaskbarOnHome != oldInfo.mShowLockedTaskbarOnHome) { 404 change |= CHANGE_SHOW_LOCKED_TASKBAR; 405 } 406 407 if (DEBUG) { 408 Log.d(TAG, "handleInfoChange - change: " + getChangeFlagsString(change)); 409 } 410 return change; 411 } 412 getNewInfo(Info oldInfo, Context displayInfoContext)413 private Info getNewInfo(Info oldInfo, Context displayInfoContext) { 414 Info newInfo = new Info(displayInfoContext, mWMProxy, oldInfo.mPerDisplayBounds); 415 416 if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale 417 || newInfo.getNavigationMode() != oldInfo.getNavigationMode()) { 418 // Cache may not be valid anymore, recreate without cache 419 newInfo = new Info(displayInfoContext, mWMProxy, 420 mWMProxy.estimateInternalDisplayBounds(displayInfoContext)); 421 } 422 return newInfo; 423 } 424 425 @AnyThread notifyConfigChangeForDisplay(int displayId)426 public void notifyConfigChangeForDisplay(int displayId) { 427 PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId); 428 if (perDisplayInfo == null) return; 429 Info oldInfo = perDisplayInfo.mInfo; 430 final Info newInfo = getNewInfo(oldInfo, perDisplayInfo.mWindowContext); 431 final int flags = calculateChange(oldInfo, newInfo); 432 if (flags != 0) { 433 MAIN_EXECUTOR.execute(() -> { 434 perDisplayInfo.mInfo = newInfo; 435 if (displayId == DEFAULT_DISPLAY && mPriorityListener != null) { 436 mPriorityListener.onDisplayInfoChanged(perDisplayInfo.mWindowContext, newInfo, 437 flags); 438 } 439 perDisplayInfo.notifyListeners(newInfo, flags); 440 }); 441 } 442 } 443 getOrCreatePerDisplayInfo(Display display)444 private PerDisplayInfo getOrCreatePerDisplayInfo(Display display) { 445 int displayId = display.getDisplayId(); 446 PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId); 447 if (perDisplayInfo != null) { 448 return perDisplayInfo; 449 } 450 if (DEBUG) { 451 Log.d(TAG, 452 String.format("getOrCreatePerDisplayInfo - no cached value found for %d", 453 displayId)); 454 } 455 Context windowContext = mAppContext.createWindowContext(display, TYPE_APPLICATION, null); 456 Info info = new Info(windowContext, mWMProxy, 457 mWMProxy.estimateInternalDisplayBounds(windowContext)); 458 perDisplayInfo = new PerDisplayInfo(displayId, windowContext, info); 459 mPerDisplayInfo.put(displayId, perDisplayInfo); 460 return perDisplayInfo; 461 } 462 463 /** 464 * Clean up resources for the given display id. 465 * @param displayId The display id 466 */ removePerDisplayInfo(int displayId)467 void removePerDisplayInfo(int displayId) { 468 PerDisplayInfo info = mPerDisplayInfo.get(displayId); 469 if (info == null) return; 470 info.cleanup(); 471 mPerDisplayInfo.remove(displayId); 472 } 473 474 public static class Info { 475 476 // Cached property 477 public final CachedDisplayInfo normalizedDisplayInfo; 478 public final int rotation; 479 public final Point currentSize; 480 public final Rect cutout; 481 482 // Configuration property 483 public final float fontScale; 484 private final int densityDpi; 485 private final NavigationMode navigationMode; 486 private final PortraitSize mScreenSizeDp; 487 488 // WindowBounds 489 public final WindowBounds realBounds; 490 public final Set<WindowBounds> supportedBounds = new ArraySet<>(); 491 private final ArrayMap<CachedDisplayInfo, List<WindowBounds>> mPerDisplayBounds = 492 new ArrayMap<>(); 493 494 private final boolean mIsTaskbarPinned; 495 private final boolean mIsTaskbarPinnedInDesktopMode; 496 497 private final boolean mIsInDesktopMode; 498 499 private final boolean mShowLockedTaskbarOnHome; 500 private final boolean mIsHomeVisible; 501 502 private final boolean mShowDesktopTaskbarForFreeformDisplay; 503 Info(Context displayInfoContext)504 public Info(Context displayInfoContext) { 505 /* don't need system overrides for external displays */ 506 this(displayInfoContext, new WindowManagerProxy(), new ArrayMap<>()); 507 } 508 509 // Used for testing Info(Context displayInfoContext, WindowManagerProxy wmProxy, Map<CachedDisplayInfo, List<WindowBounds>> perDisplayBoundsCache)510 public Info(Context displayInfoContext, 511 WindowManagerProxy wmProxy, 512 Map<CachedDisplayInfo, List<WindowBounds>> perDisplayBoundsCache) { 513 CachedDisplayInfo displayInfo = wmProxy.getDisplayInfo(displayInfoContext); 514 normalizedDisplayInfo = displayInfo.normalize(wmProxy); 515 rotation = displayInfo.rotation; 516 currentSize = displayInfo.size; 517 cutout = WindowManagerProxy.getSafeInsets(displayInfo.cutout); 518 519 Configuration config = displayInfoContext.getResources().getConfiguration(); 520 fontScale = config.fontScale; 521 densityDpi = config.densityDpi; 522 mScreenSizeDp = new PortraitSize(config.screenHeightDp, config.screenWidthDp); 523 navigationMode = wmProxy.getNavigationMode(displayInfoContext); 524 525 mPerDisplayBounds.putAll(perDisplayBoundsCache); 526 List<WindowBounds> cachedValue = getCurrentBounds(); 527 528 realBounds = wmProxy.getRealBounds(displayInfoContext, displayInfo); 529 if (cachedValue == null) { 530 // Unexpected normalizedDisplayInfo is found, recreate the cache 531 FileLog.e(TAG, "Unexpected normalizedDisplayInfo found, invalidating cache: " 532 + normalizedDisplayInfo); 533 FileLog.e(TAG, "(Invalid Cache) perDisplayBounds : " + mPerDisplayBounds); 534 mPerDisplayBounds.clear(); 535 mPerDisplayBounds.putAll(wmProxy.estimateInternalDisplayBounds(displayInfoContext)); 536 cachedValue = getCurrentBounds(); 537 if (cachedValue == null) { 538 FileLog.e(TAG, "normalizedDisplayInfo not found in estimation: " 539 + normalizedDisplayInfo); 540 supportedBounds.add(realBounds); 541 } 542 } 543 544 if (cachedValue != null) { 545 // Verify that the real bounds are a match 546 WindowBounds expectedBounds = cachedValue.get(displayInfo.rotation); 547 if (!realBounds.equals(expectedBounds)) { 548 List<WindowBounds> clone = new ArrayList<>(cachedValue); 549 clone.set(displayInfo.rotation, realBounds); 550 mPerDisplayBounds.put(normalizedDisplayInfo, clone); 551 } 552 } 553 mPerDisplayBounds.values().forEach(supportedBounds::addAll); 554 if (DEBUG) { 555 Log.d(TAG, "displayInfo: " + displayInfo); 556 Log.d(TAG, "realBounds: " + realBounds); 557 Log.d(TAG, "normalizedDisplayInfo: " + normalizedDisplayInfo); 558 Log.d(TAG, "perDisplayBounds: " + mPerDisplayBounds); 559 } 560 561 mIsTaskbarPinned = LauncherPrefs.get(displayInfoContext).get(TASKBAR_PINNING); 562 mIsTaskbarPinnedInDesktopMode = LauncherPrefs.get(displayInfoContext).get( 563 TASKBAR_PINNING_IN_DESKTOP_MODE); 564 mIsInDesktopMode = wmProxy.isInDesktopMode(DEFAULT_DISPLAY); 565 mShowLockedTaskbarOnHome = wmProxy.showLockedTaskbarOnHome(displayInfoContext); 566 mShowDesktopTaskbarForFreeformDisplay = wmProxy.showDesktopTaskbarForFreeformDisplay( 567 displayInfoContext); 568 mIsHomeVisible = wmProxy.isHomeVisible(displayInfoContext); 569 } 570 571 /** 572 * Returns whether taskbar is transient. 573 */ isTransientTaskbar()574 public boolean isTransientTaskbar() { 575 if (navigationMode != NavigationMode.NO_BUTTON) { 576 return false; 577 } 578 if (Utilities.isRunningInTestHarness() && !sTaskbarModePreferenceStatusForTests) { 579 // TODO(b/258604917): Once ENABLE_TASKBAR_PINNING is enabled, remove usage of 580 // sTransientTaskbarStatusForTests and update test to directly 581 // toggle shared preference to switch transient taskbar on/off. 582 return sTransientTaskbarStatusForTests; 583 } 584 if (enableTaskbarPinning()) { 585 // If "freeform" display taskbar is enabled, ensure the taskbar is pinned. 586 if (mShowDesktopTaskbarForFreeformDisplay) { 587 return false; 588 } 589 590 // If Launcher is visible on the freeform display, ensure the taskbar is pinned. 591 if (mShowLockedTaskbarOnHome && mIsHomeVisible) { 592 return false; 593 } 594 if (mIsInDesktopMode) { 595 return !mIsTaskbarPinnedInDesktopMode; 596 } 597 return !mIsTaskbarPinned; 598 } 599 return true; 600 } 601 602 /** 603 * Returns whether the taskbar is pinned in gesture navigation mode. 604 */ isPinnedTaskbar()605 public boolean isPinnedTaskbar() { 606 return navigationMode == NavigationMode.NO_BUTTON && !isTransientTaskbar(); 607 } 608 609 /** 610 * Returns whether the taskbar is in desktop mode. 611 */ isInDesktopMode()612 public boolean isInDesktopMode() { 613 return mIsInDesktopMode; 614 } 615 616 /** 617 * Returns {@code true} if the bounds represent a tablet. 618 */ isTablet(WindowBounds bounds)619 public boolean isTablet(WindowBounds bounds) { 620 return smallestSizeDp(bounds) >= MIN_TABLET_WIDTH; 621 } 622 623 /** Getter for {@link #navigationMode} to allow mocking. */ getNavigationMode()624 public NavigationMode getNavigationMode() { 625 return navigationMode; 626 } 627 628 /** 629 * Returns smallest size in dp for given bounds. 630 */ smallestSizeDp(WindowBounds bounds)631 public float smallestSizeDp(WindowBounds bounds) { 632 return dpiFromPx(Math.min(bounds.bounds.width(), bounds.bounds.height()), densityDpi); 633 } 634 635 /** 636 * Returns all displays for the device 637 */ getAllDisplays()638 public Set<CachedDisplayInfo> getAllDisplays() { 639 return Collections.unmodifiableSet(mPerDisplayBounds.keySet()); 640 } 641 642 /** Returns all {@link WindowBounds}s for the current display. */ 643 @Nullable getCurrentBounds()644 public List<WindowBounds> getCurrentBounds() { 645 return mPerDisplayBounds.get(normalizedDisplayInfo); 646 } 647 getDensityDpi()648 public int getDensityDpi() { 649 return densityDpi; 650 } 651 getDeviceType()652 public @DeviceType int getDeviceType() { 653 int flagPhone = 1 << 0; 654 int flagTablet = 1 << 1; 655 656 int type = supportedBounds.stream() 657 .mapToInt(bounds -> isTablet(bounds) ? flagTablet : flagPhone) 658 .reduce(0, (a, b) -> a | b); 659 if (type == (flagPhone | flagTablet)) { 660 // device has profiles supporting both phone and tablet modes 661 return TYPE_MULTI_DISPLAY; 662 } else if (type == flagTablet) { 663 return TYPE_TABLET; 664 } else { 665 return TYPE_PHONE; 666 } 667 } 668 669 /** 670 * Returns whether the taskbar is forced to be pinned when home is visible. 671 */ showLockedTaskbarOnHome()672 public boolean showLockedTaskbarOnHome() { 673 return mShowLockedTaskbarOnHome; 674 } 675 676 /** 677 * Returns whether the taskbar should be pinned, and showing desktop tasks, because the 678 * display is a "freeform" display. 679 */ showDesktopTaskbarForFreeformDisplay()680 public boolean showDesktopTaskbarForFreeformDisplay() { 681 return mShowDesktopTaskbarForFreeformDisplay; 682 } 683 } 684 685 /** 686 * Returns the given binary flags as a human-readable string. 687 * @see #CHANGE_ALL 688 */ getChangeFlagsString(int change)689 public String getChangeFlagsString(int change) { 690 StringJoiner result = new StringJoiner("|"); 691 appendFlag(result, change, CHANGE_ACTIVE_SCREEN, "CHANGE_ACTIVE_SCREEN"); 692 appendFlag(result, change, CHANGE_ROTATION, "CHANGE_ROTATION"); 693 appendFlag(result, change, CHANGE_DENSITY, "CHANGE_DENSITY"); 694 appendFlag(result, change, CHANGE_SUPPORTED_BOUNDS, "CHANGE_SUPPORTED_BOUNDS"); 695 appendFlag(result, change, CHANGE_NAVIGATION_MODE, "CHANGE_NAVIGATION_MODE"); 696 appendFlag(result, change, CHANGE_TASKBAR_PINNING, "CHANGE_TASKBAR_VARIANT"); 697 appendFlag(result, change, CHANGE_DESKTOP_MODE, "CHANGE_DESKTOP_MODE"); 698 appendFlag(result, change, CHANGE_SHOW_LOCKED_TASKBAR, "CHANGE_SHOW_LOCKED_TASKBAR"); 699 return result.toString(); 700 } 701 702 /** 703 * Dumps the current state information 704 */ dump(PrintWriter pw)705 public void dump(PrintWriter pw) { 706 int count = mPerDisplayInfo.size(); 707 for (int i = 0; i < count; ++i) { 708 int displayId = mPerDisplayInfo.keyAt(i); 709 Info info = getInfoForDisplay(displayId); 710 if (info == null) { 711 continue; 712 } 713 pw.println(String.format(Locale.ENGLISH, "DisplayController.Info (displayId=%d):", 714 displayId)); 715 pw.println(" normalizedDisplayInfo=" + info.normalizedDisplayInfo); 716 pw.println(" rotation=" + info.rotation); 717 pw.println(" fontScale=" + info.fontScale); 718 pw.println(" densityDpi=" + info.densityDpi); 719 pw.println(" navigationMode=" + info.getNavigationMode().name()); 720 pw.println(" isTaskbarPinned=" + info.mIsTaskbarPinned); 721 pw.println(" isTaskbarPinnedInDesktopMode=" + info.mIsTaskbarPinnedInDesktopMode); 722 pw.println(" isInDesktopMode=" + info.mIsInDesktopMode); 723 pw.println(" showLockedTaskbarOnHome=" + info.showLockedTaskbarOnHome()); 724 pw.println(" currentSize=" + info.currentSize); 725 info.mPerDisplayBounds.forEach((key, value) -> pw.println( 726 " perDisplayBounds - " + key + ": " + value)); 727 pw.println(" isTransientTaskbar=" + info.isTransientTaskbar()); 728 } 729 } 730 731 /** 732 * Utility class to hold a size information in an orientation independent way 733 */ 734 public static class PortraitSize { 735 public final int width, height; 736 PortraitSize(int w, int h)737 public PortraitSize(int w, int h) { 738 width = Math.min(w, h); 739 height = Math.max(w, h); 740 } 741 742 @Override equals(Object o)743 public boolean equals(Object o) { 744 if (this == o) return true; 745 if (o == null || getClass() != o.getClass()) return false; 746 PortraitSize that = (PortraitSize) o; 747 return width == that.width && height == that.height; 748 } 749 750 @Override hashCode()751 public int hashCode() { 752 return Objects.hash(width, height); 753 } 754 } 755 756 private class PerDisplayInfo implements ComponentCallbacks { 757 final int mDisplayId; 758 final CopyOnWriteArrayList<DisplayInfoChangeListener> mListeners = 759 new CopyOnWriteArrayList<>(); 760 final Context mWindowContext; 761 Info mInfo; 762 PerDisplayInfo(int displayId, Context windowContext, Info info)763 PerDisplayInfo(int displayId, Context windowContext, Info info) { 764 this.mDisplayId = displayId; 765 this.mWindowContext = windowContext; 766 this.mInfo = info; 767 windowContext.registerComponentCallbacks(this); 768 } 769 addListener(DisplayInfoChangeListener listener)770 void addListener(DisplayInfoChangeListener listener) { 771 mListeners.add(listener); 772 } 773 removeListener(DisplayInfoChangeListener listener)774 void removeListener(DisplayInfoChangeListener listener) { 775 mListeners.remove(listener); 776 } 777 notifyListeners(Info info, int flags)778 void notifyListeners(Info info, int flags) { 779 int count = mListeners.size(); 780 for (int i = 0; i < count; ++i) { 781 mListeners.get(i).onDisplayInfoChanged(mWindowContext, info, flags); 782 } 783 } 784 785 @Override onConfigurationChanged(@onNull Configuration newConfig)786 public void onConfigurationChanged(@NonNull Configuration newConfig) { 787 DisplayController.this.onConfigurationChanged(newConfig, mDisplayId); 788 } 789 790 @Override onLowMemory()791 public void onLowMemory() {} 792 cleanup()793 void cleanup() { 794 mWindowContext.unregisterComponentCallbacks(this); 795 mListeners.clear(); 796 } 797 } 798 799 } 800