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