• 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.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.Utilities.dpiFromPx;
23 import static com.android.launcher3.config.FeatureFlags.ENABLE_TRANSIENT_TASKBAR;
24 import static com.android.launcher3.config.FeatureFlags.FORCE_PERSISTENT_TASKBAR;
25 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
26 import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
27 import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH;
28 
29 import android.annotation.SuppressLint;
30 import android.annotation.TargetApi;
31 import android.content.ComponentCallbacks;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.res.Configuration;
35 import android.graphics.Point;
36 import android.graphics.Rect;
37 import android.hardware.display.DisplayManager;
38 import android.os.Build;
39 import android.util.ArrayMap;
40 import android.util.ArraySet;
41 import android.util.Log;
42 import android.view.Display;
43 
44 import androidx.annotation.AnyThread;
45 import androidx.annotation.UiThread;
46 import androidx.annotation.VisibleForTesting;
47 
48 import com.android.launcher3.Utilities;
49 import com.android.launcher3.util.window.CachedDisplayInfo;
50 import com.android.launcher3.util.window.WindowManagerProxy;
51 
52 import java.io.PrintWriter;
53 import java.util.ArrayList;
54 import java.util.Arrays;
55 import java.util.Collections;
56 import java.util.Map;
57 import java.util.Objects;
58 import java.util.Set;
59 import java.util.StringJoiner;
60 
61 /**
62  * Utility class to cache properties of default display to avoid a system RPC on every call.
63  */
64 @SuppressLint("NewApi")
65 public class DisplayController implements ComponentCallbacks, SafeCloseable {
66 
67     private static final String TAG = "DisplayController";
68     private static final boolean DEBUG = false;
69     private static boolean sTransientTaskbarStatusForTests;
70 
71     // TODO(b/254119092) remove all logs with this tag
72     public static final String TASKBAR_NOT_DESTROYED_TAG = "b/254119092";
73 
74     public static final MainThreadInitializedObject<DisplayController> INSTANCE =
75             new MainThreadInitializedObject<>(DisplayController::new);
76 
77     public static final int CHANGE_ACTIVE_SCREEN = 1 << 0;
78     public static final int CHANGE_ROTATION = 1 << 1;
79     public static final int CHANGE_DENSITY = 1 << 2;
80     public static final int CHANGE_SUPPORTED_BOUNDS = 1 << 3;
81     public static final int CHANGE_NAVIGATION_MODE = 1 << 4;
82 
83     public static final int CHANGE_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION
84             | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS | CHANGE_NAVIGATION_MODE;
85 
86     private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
87     private static final String TARGET_OVERLAY_PACKAGE = "android";
88 
89     private final Context mContext;
90     private final DisplayManager mDM;
91 
92     // Null for SDK < S
93     private final Context mWindowContext;
94 
95     // The callback in this listener updates DeviceProfile, which other listeners might depend on
96     private DisplayInfoChangeListener mPriorityListener;
97     private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>();
98 
99     private final SimpleBroadcastReceiver mReceiver = new SimpleBroadcastReceiver(this::onIntent);
100 
101     private Info mInfo;
102     private boolean mDestroyed = false;
103 
DisplayController(Context context)104     private DisplayController(Context context) {
105         mContext = context;
106         mDM = context.getSystemService(DisplayManager.class);
107 
108         Display display = mDM.getDisplay(DEFAULT_DISPLAY);
109         if (Utilities.ATLEAST_S) {
110             mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
111             mWindowContext.registerComponentCallbacks(this);
112         } else {
113             mWindowContext = null;
114             mReceiver.register(mContext, ACTION_CONFIGURATION_CHANGED);
115         }
116 
117         // Initialize navigation mode change listener
118         mReceiver.registerPkgActions(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
119 
120         WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(context);
121         Context displayInfoContext = getDisplayInfoContext(display);
122         mInfo = new Info(displayInfoContext, wmProxy,
123                 wmProxy.estimateInternalDisplayBounds(displayInfoContext));
124     }
125 
126     /**
127      * Returns the current navigation mode
128      */
getNavigationMode(Context context)129     public static NavigationMode getNavigationMode(Context context) {
130         return INSTANCE.get(context).getInfo().navigationMode;
131     }
132 
133     /**
134      * Returns whether taskbar is transient.
135      */
isTransientTaskbar(Context context)136     public static boolean isTransientTaskbar(Context context) {
137         return INSTANCE.get(context).isTransientTaskbar();
138     }
139 
140     /**
141      * Returns whether taskbar is transient.
142      */
isTransientTaskbar()143     public boolean isTransientTaskbar() {
144         // TODO(b/258604917): When running in test harness, use !sTransientTaskbarStatusForTests
145         //  once tests are updated to expect new persistent behavior such as not allowing long press
146         //  to stash.
147         if (!Utilities.isRunningInTestHarness() && FORCE_PERSISTENT_TASKBAR.get()) {
148             return false;
149         }
150         return getInfo().navigationMode == NavigationMode.NO_BUTTON
151                 && (Utilities.isRunningInTestHarness()
152                     ? sTransientTaskbarStatusForTests
153                     : ENABLE_TRANSIENT_TASKBAR.get());
154     }
155 
156     /**
157      * Enables transient taskbar status for tests.
158      */
159     @VisibleForTesting
enableTransientTaskbarForTests(boolean enable)160     public static void enableTransientTaskbarForTests(boolean enable) {
161         sTransientTaskbarStatusForTests = enable;
162     }
163 
164     @Override
close()165     public void close() {
166         mDestroyed = true;
167         if (mWindowContext != null) {
168             mWindowContext.unregisterComponentCallbacks(this);
169         } else {
170             // TODO: unregister broadcast receiver
171         }
172     }
173 
174     /**
175      * Interface for listening for display changes
176      */
177     public interface DisplayInfoChangeListener {
178 
179         /**
180          * Invoked when display info has changed.
181          * @param context updated context associated with the display.
182          * @param info updated display information.
183          * @param flags bitmask indicating type of change.
184          */
onDisplayInfoChanged(Context context, Info info, int flags)185         void onDisplayInfoChanged(Context context, Info info, int flags);
186     }
187 
onIntent(Intent intent)188     private void onIntent(Intent intent) {
189         if (mDestroyed) {
190             return;
191         }
192         boolean reconfigure = false;
193         if (ACTION_OVERLAY_CHANGED.equals(intent.getAction())) {
194             reconfigure = true;
195         } else if (ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
196             Configuration config = mContext.getResources().getConfiguration();
197             reconfigure = mInfo.fontScale != config.fontScale
198                     || mInfo.densityDpi != config.densityDpi;
199         }
200 
201         if (reconfigure) {
202             Log.d(TAG, "Configuration changed, notifying listeners");
203             Display display = mDM.getDisplay(DEFAULT_DISPLAY);
204             if (display != null) {
205                 handleInfoChange(display);
206             }
207         }
208     }
209 
210     @UiThread
211     @Override
212     @TargetApi(Build.VERSION_CODES.S)
onConfigurationChanged(Configuration config)213     public final void onConfigurationChanged(Configuration config) {
214         Log.d(TASKBAR_NOT_DESTROYED_TAG, "DisplayController#onConfigurationChanged: " + config);
215         Display display = mWindowContext.getDisplay();
216         if (config.densityDpi != mInfo.densityDpi
217                 || config.fontScale != mInfo.fontScale
218                 || display.getRotation() != mInfo.rotation
219                 || !mInfo.mScreenSizeDp.equals(
220                         new PortraitSize(config.screenHeightDp, config.screenWidthDp))) {
221             handleInfoChange(display);
222         }
223     }
224 
225     @Override
onLowMemory()226     public final void onLowMemory() { }
227 
setPriorityListener(DisplayInfoChangeListener listener)228     public void setPriorityListener(DisplayInfoChangeListener listener) {
229         mPriorityListener = listener;
230     }
231 
addChangeListener(DisplayInfoChangeListener listener)232     public void addChangeListener(DisplayInfoChangeListener listener) {
233         mListeners.add(listener);
234     }
235 
removeChangeListener(DisplayInfoChangeListener listener)236     public void removeChangeListener(DisplayInfoChangeListener listener) {
237         mListeners.remove(listener);
238     }
239 
getInfo()240     public Info getInfo() {
241         return mInfo;
242     }
243 
getDisplayInfoContext(Display display)244     private Context getDisplayInfoContext(Display display) {
245         return Utilities.ATLEAST_S ? mWindowContext : mContext.createDisplayContext(display);
246     }
247 
248     @AnyThread
handleInfoChange(Display display)249     private void handleInfoChange(Display display) {
250         WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(mContext);
251         Info oldInfo = mInfo;
252 
253         Context displayInfoContext = getDisplayInfoContext(display);
254         Info newInfo = new Info(displayInfoContext, wmProxy, oldInfo.mPerDisplayBounds);
255 
256         if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale
257                 || newInfo.navigationMode != oldInfo.navigationMode) {
258             // Cache may not be valid anymore, recreate without cache
259             newInfo = new Info(displayInfoContext, wmProxy,
260                     wmProxy.estimateInternalDisplayBounds(displayInfoContext));
261         }
262 
263         int change = 0;
264         if (!newInfo.normalizedDisplayInfo.equals(oldInfo.normalizedDisplayInfo)) {
265             change |= CHANGE_ACTIVE_SCREEN;
266         }
267         if (newInfo.rotation != oldInfo.rotation) {
268             change |= CHANGE_ROTATION;
269         }
270         if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale) {
271             change |= CHANGE_DENSITY;
272         }
273         if (newInfo.navigationMode != oldInfo.navigationMode) {
274             change |= CHANGE_NAVIGATION_MODE;
275         }
276         if (!newInfo.supportedBounds.equals(oldInfo.supportedBounds)
277                 || !newInfo.mPerDisplayBounds.equals(oldInfo.mPerDisplayBounds)) {
278             change |= CHANGE_SUPPORTED_BOUNDS;
279         }
280         if (DEBUG) {
281             Log.d(TAG, "handleInfoChange - change: " + getChangeFlagsString(change));
282         }
283 
284         if (change != 0) {
285             mInfo = newInfo;
286             final int flags = change;
287             MAIN_EXECUTOR.execute(() -> notifyChange(displayInfoContext, flags));
288         }
289     }
290 
notifyChange(Context context, int flags)291     private void notifyChange(Context context, int flags) {
292         if (mPriorityListener != null) {
293             mPriorityListener.onDisplayInfoChanged(context, mInfo, flags);
294         }
295 
296         int count = mListeners.size();
297         for (int i = 0; i < count; i++) {
298             mListeners.get(i).onDisplayInfoChanged(context, mInfo, flags);
299         }
300     }
301 
302     public static class Info {
303 
304         // Cached property
305         public final CachedDisplayInfo normalizedDisplayInfo;
306         public final int rotation;
307         public final Point currentSize;
308         public final Rect cutout;
309 
310         // Configuration property
311         public final float fontScale;
312         private final int densityDpi;
313         public final NavigationMode navigationMode;
314         private final PortraitSize mScreenSizeDp;
315 
316         // WindowBounds
317         public final WindowBounds realBounds;
318         public final Set<WindowBounds> supportedBounds = new ArraySet<>();
319         private final ArrayMap<CachedDisplayInfo, WindowBounds[]> mPerDisplayBounds =
320                 new ArrayMap<>();
321 
Info(Context displayInfoContext)322         public Info(Context displayInfoContext) {
323             /* don't need system overrides for external displays */
324             this(displayInfoContext, new WindowManagerProxy(), new ArrayMap<>());
325         }
326 
327         // Used for testing
Info(Context displayInfoContext, WindowManagerProxy wmProxy, Map<CachedDisplayInfo, WindowBounds[]> perDisplayBoundsCache)328         public Info(Context displayInfoContext,
329                 WindowManagerProxy wmProxy,
330                 Map<CachedDisplayInfo, WindowBounds[]> perDisplayBoundsCache) {
331             CachedDisplayInfo displayInfo = wmProxy.getDisplayInfo(displayInfoContext);
332             normalizedDisplayInfo = displayInfo.normalize();
333             rotation = displayInfo.rotation;
334             currentSize = displayInfo.size;
335             cutout = displayInfo.cutout;
336 
337             Configuration config = displayInfoContext.getResources().getConfiguration();
338             fontScale = config.fontScale;
339             densityDpi = config.densityDpi;
340             mScreenSizeDp = new PortraitSize(config.screenHeightDp, config.screenWidthDp);
341             navigationMode = wmProxy.getNavigationMode(displayInfoContext);
342 
343             mPerDisplayBounds.putAll(perDisplayBoundsCache);
344             WindowBounds[] cachedValue = mPerDisplayBounds.get(normalizedDisplayInfo);
345 
346             realBounds = wmProxy.getRealBounds(displayInfoContext, displayInfo);
347             if (cachedValue == null) {
348                 // Unexpected normalizedDisplayInfo is found, recreate the cache
349                 Log.e(TAG, "Unexpected normalizedDisplayInfo found, invalidating cache");
350                 mPerDisplayBounds.clear();
351                 mPerDisplayBounds.putAll(wmProxy.estimateInternalDisplayBounds(displayInfoContext));
352                 cachedValue = mPerDisplayBounds.get(normalizedDisplayInfo);
353                 if (cachedValue == null) {
354                     Log.e(TAG, "normalizedDisplayInfo not found in estimation: "
355                             + normalizedDisplayInfo);
356                     supportedBounds.add(realBounds);
357                 }
358             }
359 
360             if (cachedValue != null) {
361                 // Verify that the real bounds are a match
362                 WindowBounds expectedBounds = cachedValue[displayInfo.rotation];
363                 if (!realBounds.equals(expectedBounds)) {
364                     WindowBounds[] clone = new WindowBounds[4];
365                     System.arraycopy(cachedValue, 0, clone, 0, 4);
366                     clone[displayInfo.rotation] = realBounds;
367                     mPerDisplayBounds.put(normalizedDisplayInfo, clone);
368                 }
369             }
370             mPerDisplayBounds.values().forEach(
371                     windowBounds -> Collections.addAll(supportedBounds, windowBounds));
372             if (DEBUG) {
373                 Log.d(TAG, "displayInfo: " + displayInfo);
374                 Log.d(TAG, "realBounds: " + realBounds);
375                 Log.d(TAG, "normalizedDisplayInfo: " + normalizedDisplayInfo);
376                 mPerDisplayBounds.forEach((key, value) -> Log.d(TAG,
377                         "perDisplayBounds - " + key + ": " + Arrays.deepToString(value)));
378             }
379         }
380 
381         /**
382          * Returns {@code true} if the bounds represent a tablet.
383          */
isTablet(WindowBounds bounds)384         public boolean isTablet(WindowBounds bounds) {
385             return smallestSizeDp(bounds) >= MIN_TABLET_WIDTH;
386         }
387 
388         /**
389          * Returns smallest size in dp for given bounds.
390          */
smallestSizeDp(WindowBounds bounds)391         public float smallestSizeDp(WindowBounds bounds) {
392             return dpiFromPx(Math.min(bounds.bounds.width(), bounds.bounds.height()), densityDpi);
393         }
394 
395         /**
396          * Returns all displays for the device
397          */
getAllDisplays()398         public Set<CachedDisplayInfo> getAllDisplays() {
399             return Collections.unmodifiableSet(mPerDisplayBounds.keySet());
400         }
401 
getDensityDpi()402         public int getDensityDpi() {
403             return densityDpi;
404         }
405     }
406 
407     /**
408      * Returns the given binary flags as a human-readable string.
409      * @see #CHANGE_ALL
410      */
getChangeFlagsString(int change)411     public String getChangeFlagsString(int change) {
412         StringJoiner result = new StringJoiner("|");
413         appendFlag(result, change, CHANGE_ACTIVE_SCREEN, "CHANGE_ACTIVE_SCREEN");
414         appendFlag(result, change, CHANGE_ROTATION, "CHANGE_ROTATION");
415         appendFlag(result, change, CHANGE_DENSITY, "CHANGE_DENSITY");
416         appendFlag(result, change, CHANGE_SUPPORTED_BOUNDS, "CHANGE_SUPPORTED_BOUNDS");
417         appendFlag(result, change, CHANGE_NAVIGATION_MODE, "CHANGE_NAVIGATION_MODE");
418         return result.toString();
419     }
420 
421     /**
422      * Dumps the current state information
423      */
dump(PrintWriter pw)424     public void dump(PrintWriter pw) {
425         Info info = mInfo;
426         pw.println("DisplayController.Info:");
427         pw.println("  normalizedDisplayInfo=" + info.normalizedDisplayInfo);
428         pw.println("  rotation=" + info.rotation);
429         pw.println("  fontScale=" + info.fontScale);
430         pw.println("  densityDpi=" + info.densityDpi);
431         pw.println("  navigationMode=" + info.navigationMode.name());
432         pw.println("  currentSize=" + info.currentSize);
433         info.mPerDisplayBounds.forEach((key, value) -> pw.println(
434                 "  perDisplayBounds - " + key + ": " + Arrays.deepToString(value)));
435     }
436 
437     /**
438      * Utility class to hold a size information in an orientation independent way
439      */
440     public static class PortraitSize {
441         public final int width, height;
442 
PortraitSize(int w, int h)443         public PortraitSize(int w, int h) {
444             width = Math.min(w, h);
445             height = Math.max(w, h);
446         }
447 
448         @Override
equals(Object o)449         public boolean equals(Object o) {
450             if (this == o) return true;
451             if (o == null || getClass() != o.getClass()) return false;
452             PortraitSize that = (PortraitSize) o;
453             return width == that.width && height == that.height;
454         }
455 
456         @Override
hashCode()457         public int hashCode() {
458             return Objects.hash(width, height);
459         }
460     }
461 
462 }
463