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