• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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