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.Utilities.dpiFromPx;
22 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
23 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
24 import static com.android.launcher3.util.WindowManagerCompat.MIN_TABLET_WIDTH;
25 
26 import static java.util.Collections.emptyMap;
27 
28 import android.annotation.SuppressLint;
29 import android.annotation.TargetApi;
30 import android.content.ComponentCallbacks;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.IntentFilter;
34 import android.content.res.Configuration;
35 import android.graphics.Point;
36 import android.hardware.display.DisplayManager;
37 import android.hardware.display.DisplayManager.DisplayListener;
38 import android.os.Build;
39 import android.text.TextUtils;
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.WorkerThread;
48 
49 import com.android.launcher3.Utilities;
50 import com.android.launcher3.uioverrides.ApiWrapper;
51 
52 import java.util.ArrayList;
53 import java.util.Map;
54 import java.util.Objects;
55 import java.util.Set;
56 
57 /**
58  * Utility class to cache properties of default display to avoid a system RPC on every call.
59  */
60 @SuppressLint("NewApi")
61 public class DisplayController implements DisplayListener, ComponentCallbacks, SafeCloseable {
62 
63     private static final String TAG = "DisplayController";
64 
65     public static final MainThreadInitializedObject<DisplayController> INSTANCE =
66             new MainThreadInitializedObject<>(DisplayController::new);
67 
68     public static final int CHANGE_ACTIVE_SCREEN = 1 << 0;
69     public static final int CHANGE_ROTATION = 1 << 1;
70     public static final int CHANGE_FRAME_DELAY = 1 << 2;
71     public static final int CHANGE_DENSITY = 1 << 3;
72     public static final int CHANGE_SUPPORTED_BOUNDS = 1 << 4;
73 
74     public static final int CHANGE_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION
75             | CHANGE_FRAME_DELAY | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS;
76 
77     private final Context mContext;
78     private final DisplayManager mDM;
79 
80     // Null for SDK < S
81     private final Context mWindowContext;
82     // The callback in this listener updates DeviceProfile, which other listeners might depend on
83     private DisplayInfoChangeListener mPriorityListener;
84     private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>();
85 
86     private Info mInfo;
87     private boolean mDestroyed = false;
88 
DisplayController(Context context)89     private DisplayController(Context context) {
90         mContext = context;
91         mDM = context.getSystemService(DisplayManager.class);
92 
93         Display display = mDM.getDisplay(DEFAULT_DISPLAY);
94         if (Utilities.ATLEAST_S) {
95             mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
96             mWindowContext.registerComponentCallbacks(this);
97         } else {
98             mWindowContext = null;
99             SimpleBroadcastReceiver configChangeReceiver =
100                     new SimpleBroadcastReceiver(this::onConfigChanged);
101             mContext.registerReceiver(configChangeReceiver,
102                     new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
103         }
104         mInfo = new Info(getDisplayInfoContext(display), display,
105                 getInternalDisplays(mDM), emptyMap());
106         mDM.registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler());
107     }
108 
getInternalDisplays( DisplayManager displayManager)109     private static ArrayMap<String, PortraitSize> getInternalDisplays(
110             DisplayManager displayManager) {
111         Display[] displays = displayManager.getDisplays();
112         ArrayMap<String, PortraitSize> internalDisplays = new ArrayMap<>();
113         for (Display display : displays) {
114             if (ApiWrapper.isInternalDisplay(display)) {
115                 Point size = new Point();
116                 display.getRealSize(size);
117                 internalDisplays.put(ApiWrapper.getUniqueId(display),
118                         new PortraitSize(size.x, size.y));
119             }
120         }
121         return internalDisplays;
122     }
123 
124     @Override
close()125     public void close() {
126         mDestroyed = true;
127         if (mWindowContext != null) {
128             mWindowContext.unregisterComponentCallbacks(this);
129         } else {
130             // TODO: unregister broadcast receiver
131         }
132         mDM.unregisterDisplayListener(this);
133     }
134 
135     @Override
onDisplayAdded(int displayId)136     public final void onDisplayAdded(int displayId) { }
137 
138     @Override
onDisplayRemoved(int displayId)139     public final void onDisplayRemoved(int displayId) { }
140 
141     @WorkerThread
142     @Override
onDisplayChanged(int displayId)143     public final void onDisplayChanged(int displayId) {
144         if (displayId != DEFAULT_DISPLAY) {
145             return;
146         }
147         Display display = mDM.getDisplay(DEFAULT_DISPLAY);
148         if (display == null) {
149             return;
150         }
151         if (Utilities.ATLEAST_S) {
152             // Only check for refresh rate. Everything else comes from component callbacks
153             if (getSingleFrameMs(display) == mInfo.singleFrameMs) {
154                 return;
155             }
156         }
157         handleInfoChange(display);
158     }
159 
getSingleFrameMs(Context context)160     public static int getSingleFrameMs(Context context) {
161         return INSTANCE.get(context).getInfo().singleFrameMs;
162     }
163 
164     /**
165      * Interface for listening for display changes
166      */
167     public interface DisplayInfoChangeListener {
168 
169         /**
170          * Invoked when display info has changed.
171          * @param context updated context associated with the display.
172          * @param info updated display information.
173          * @param flags bitmask indicating type of change.
174          */
onDisplayInfoChanged(Context context, Info info, int flags)175         void onDisplayInfoChanged(Context context, Info info, int flags);
176     }
177 
178     /**
179      * Only used for pre-S
180      */
onConfigChanged(Intent intent)181     private void onConfigChanged(Intent intent) {
182         if (mDestroyed) {
183             return;
184         }
185         Configuration config = mContext.getResources().getConfiguration();
186         if (mInfo.fontScale != config.fontScale || mInfo.densityDpi != config.densityDpi) {
187             Log.d(TAG, "Configuration changed, notifying listeners");
188             Display display = mDM.getDisplay(DEFAULT_DISPLAY);
189             if (display != null) {
190                 handleInfoChange(display);
191             }
192         }
193     }
194 
195     @UiThread
196     @Override
197     @TargetApi(Build.VERSION_CODES.S)
onConfigurationChanged(Configuration config)198     public final void onConfigurationChanged(Configuration config) {
199         Display display = mWindowContext.getDisplay();
200         if (config.densityDpi != mInfo.densityDpi
201                 || config.fontScale != mInfo.fontScale
202                 || display.getRotation() != mInfo.rotation
203                 || !mInfo.mScreenSizeDp.equals(
204                         new PortraitSize(config.screenHeightDp, config.screenWidthDp))) {
205             handleInfoChange(display);
206         }
207     }
208 
209     @Override
onLowMemory()210     public final void onLowMemory() { }
211 
setPriorityListener(DisplayInfoChangeListener listener)212     public void setPriorityListener(DisplayInfoChangeListener listener) {
213         mPriorityListener = listener;
214     }
215 
addChangeListener(DisplayInfoChangeListener listener)216     public void addChangeListener(DisplayInfoChangeListener listener) {
217         mListeners.add(listener);
218     }
219 
removeChangeListener(DisplayInfoChangeListener listener)220     public void removeChangeListener(DisplayInfoChangeListener listener) {
221         mListeners.remove(listener);
222     }
223 
getInfo()224     public Info getInfo() {
225         return mInfo;
226     }
227 
getDisplayInfoContext(Display display)228     private Context getDisplayInfoContext(Display display) {
229         return Utilities.ATLEAST_S ? mWindowContext : mContext.createDisplayContext(display);
230     }
231 
232     @AnyThread
handleInfoChange(Display display)233     private void handleInfoChange(Display display) {
234         Info oldInfo = mInfo;
235 
236         Context displayContext = getDisplayInfoContext(display);
237         Info newInfo = new Info(displayContext, display,
238                 oldInfo.mInternalDisplays, oldInfo.mPerDisplayBounds);
239 
240         if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale) {
241             // Cache may not be valid anymore, recreate without cache
242             newInfo = new Info(displayContext, display, getInternalDisplays(mDM), emptyMap());
243         }
244 
245         int change = 0;
246         if (!newInfo.displayId.equals(oldInfo.displayId)) {
247             change |= CHANGE_ACTIVE_SCREEN;
248         }
249         if (newInfo.rotation != oldInfo.rotation) {
250             change |= CHANGE_ROTATION;
251         }
252         if (newInfo.singleFrameMs != oldInfo.singleFrameMs) {
253             change |= CHANGE_FRAME_DELAY;
254         }
255         if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale) {
256             change |= CHANGE_DENSITY;
257         }
258         if (!newInfo.supportedBounds.equals(oldInfo.supportedBounds)) {
259             change |= CHANGE_SUPPORTED_BOUNDS;
260 
261             PortraitSize realSize = new PortraitSize(newInfo.currentSize.x, newInfo.currentSize.y);
262             PortraitSize expectedSize = oldInfo.mInternalDisplays.get(
263                     ApiWrapper.getUniqueId(display));
264             if (newInfo.supportedBounds.size() != oldInfo.supportedBounds.size()) {
265                 Log.e("b/198965093",
266                         "Inconsistent number of displays"
267                                 + "\ndisplay state: " + display.getState()
268                                 + "\noldInfo.supportedBounds: " + oldInfo.supportedBounds
269                                 + "\nnewInfo.supportedBounds: " + newInfo.supportedBounds);
270             }
271             if (!realSize.equals(expectedSize) && display.getState() == Display.STATE_OFF) {
272                 Log.e("b/198965093", "Display size changed while display is off, ignoring change");
273                 return;
274             }
275         }
276 
277         if (change != 0) {
278             mInfo = newInfo;
279             final int flags = change;
280             MAIN_EXECUTOR.execute(() -> notifyChange(displayContext, flags));
281         }
282     }
283 
notifyChange(Context context, int flags)284     private void notifyChange(Context context, int flags) {
285         if (mPriorityListener != null) {
286             mPriorityListener.onDisplayInfoChanged(context, mInfo, flags);
287         }
288         for (int i = mListeners.size() - 1; i >= 0; i--) {
289             mListeners.get(i).onDisplayInfoChanged(context, mInfo, flags);
290         }
291     }
292 
293     public static class Info {
294 
295         public final int singleFrameMs;
296 
297         // Configuration properties
298         public final int rotation;
299         public final float fontScale;
300         public final int densityDpi;
301 
302         private final PortraitSize mScreenSizeDp;
303 
304         public final Point currentSize;
305 
306         public String displayId;
307         public final Set<WindowBounds> supportedBounds = new ArraySet<>();
308         private final Map<String, Set<WindowBounds>> mPerDisplayBounds = new ArrayMap<>();
309         private final ArrayMap<String, PortraitSize> mInternalDisplays;
310 
Info(Context context, Display display)311         public Info(Context context, Display display) {
312             this(context, display, new ArrayMap<>(), emptyMap());
313         }
314 
Info(Context context, Display display, ArrayMap<String, PortraitSize> internalDisplays, Map<String, Set<WindowBounds>> perDisplayBoundsCache)315         private Info(Context context, Display display,
316                 ArrayMap<String, PortraitSize> internalDisplays,
317                 Map<String, Set<WindowBounds>> perDisplayBoundsCache) {
318             mInternalDisplays = internalDisplays;
319             rotation = display.getRotation();
320 
321             Configuration config = context.getResources().getConfiguration();
322             fontScale = config.fontScale;
323             densityDpi = config.densityDpi;
324             mScreenSizeDp = new PortraitSize(config.screenHeightDp, config.screenWidthDp);
325 
326             singleFrameMs = getSingleFrameMs(display);
327             currentSize = new Point();
328             display.getRealSize(currentSize);
329 
330             displayId = ApiWrapper.getUniqueId(display);
331             Set<WindowBounds> currentSupportedBounds =
332                     getSupportedBoundsForDisplay(display, currentSize);
333             mPerDisplayBounds.put(displayId, currentSupportedBounds);
334             supportedBounds.addAll(currentSupportedBounds);
335 
336             if (ApiWrapper.isInternalDisplay(display) && internalDisplays.size() > 1) {
337                 int displayCount = internalDisplays.size();
338                 for (int i = 0; i < displayCount; i++) {
339                     String displayKey = internalDisplays.keyAt(i);
340                     if (TextUtils.equals(displayId, displayKey)) {
341                         continue;
342                     }
343 
344                     Set<WindowBounds> displayBounds = perDisplayBoundsCache.get(displayKey);
345                     if (displayBounds == null) {
346                         // We assume densityDpi is the same across all internal displays
347                         displayBounds = WindowManagerCompat.estimateDisplayProfiles(
348                                 context, internalDisplays.valueAt(i), densityDpi,
349                                 ApiWrapper.TASKBAR_DRAWN_IN_PROCESS);
350                     }
351 
352                     supportedBounds.addAll(displayBounds);
353                     mPerDisplayBounds.put(displayKey, displayBounds);
354                 }
355             }
356         }
357 
getSupportedBoundsForDisplay(Display display, Point size)358         private static Set<WindowBounds> getSupportedBoundsForDisplay(Display display, Point size) {
359             Point smallestSize = new Point();
360             Point largestSize = new Point();
361             display.getCurrentSizeRange(smallestSize, largestSize);
362 
363             int portraitWidth = Math.min(size.x, size.y);
364             int portraitHeight = Math.max(size.x, size.y);
365             Set<WindowBounds> result = new ArraySet<>();
366             result.add(new WindowBounds(portraitWidth, portraitHeight,
367                     smallestSize.x, largestSize.y));
368             result.add(new WindowBounds(portraitHeight, portraitWidth,
369                     largestSize.x, smallestSize.y));
370             return result;
371         }
372 
373         /**
374          * Returns true if the bounds represent a tablet
375          */
isTablet(WindowBounds bounds)376         public boolean isTablet(WindowBounds bounds) {
377             return dpiFromPx(Math.min(bounds.bounds.width(), bounds.bounds.height()),
378                     densityDpi) >= MIN_TABLET_WIDTH;
379         }
380     }
381 
382     /**
383      * Utility class to hold a size information in an orientation independent way
384      */
385     public static class PortraitSize {
386         public final int width, height;
387 
PortraitSize(int w, int h)388         public PortraitSize(int w, int h) {
389             width = Math.min(w, h);
390             height = Math.max(w, h);
391         }
392 
393         @Override
equals(Object o)394         public boolean equals(Object o) {
395             if (this == o) return true;
396             if (o == null || getClass() != o.getClass()) return false;
397             PortraitSize that = (PortraitSize) o;
398             return width == that.width && height == that.height;
399         }
400 
401         @Override
hashCode()402         public int hashCode() {
403             return Objects.hash(width, height);
404         }
405     }
406 
getSingleFrameMs(Display display)407     private static int getSingleFrameMs(Display display) {
408         float refreshRate = display.getRefreshRate();
409         return refreshRate > 0 ? (int) (1000 / refreshRate) : 16;
410     }
411 }
412