• 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.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 android.annotation.SuppressLint;
27 import android.annotation.TargetApi;
28 import android.content.ComponentCallbacks;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.content.res.Configuration;
33 import android.graphics.Point;
34 import android.hardware.display.DisplayManager;
35 import android.hardware.display.DisplayManager.DisplayListener;
36 import android.os.Build;
37 import android.util.ArraySet;
38 import android.util.Log;
39 import android.view.Display;
40 import android.view.WindowMetrics;
41 
42 import androidx.annotation.AnyThread;
43 import androidx.annotation.UiThread;
44 import androidx.annotation.WorkerThread;
45 
46 import com.android.launcher3.Utilities;
47 import com.android.launcher3.uioverrides.ApiWrapper;
48 
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.Objects;
52 import java.util.Set;
53 
54 /**
55  * Utility class to cache properties of default display to avoid a system RPC on every call.
56  */
57 @SuppressLint("NewApi")
58 public class DisplayController implements DisplayListener, ComponentCallbacks {
59 
60     private static final String TAG = "DisplayController";
61 
62     public static final MainThreadInitializedObject<DisplayController> INSTANCE =
63             new MainThreadInitializedObject<>(DisplayController::new);
64 
65     public static final int CHANGE_ACTIVE_SCREEN = 1 << 0;
66     public static final int CHANGE_ROTATION = 1 << 1;
67     public static final int CHANGE_FRAME_DELAY = 1 << 2;
68     public static final int CHANGE_DENSITY = 1 << 3;
69     public static final int CHANGE_SUPPORTED_BOUNDS = 1 << 4;
70 
71     public static final int CHANGE_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION
72             | CHANGE_FRAME_DELAY | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS;
73 
74     private final Context mContext;
75     private final DisplayManager mDM;
76 
77     // Null for SDK < S
78     private final Context mWindowContext;
79 
80     private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>();
81     private Info mInfo;
82 
DisplayController(Context context)83     private DisplayController(Context context) {
84         mContext = context;
85         mDM = context.getSystemService(DisplayManager.class);
86 
87         Display display = mDM.getDisplay(DEFAULT_DISPLAY);
88         if (Utilities.ATLEAST_S) {
89             mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
90             mWindowContext.registerComponentCallbacks(this);
91         } else {
92             mWindowContext = null;
93             SimpleBroadcastReceiver configChangeReceiver =
94                     new SimpleBroadcastReceiver(this::onConfigChanged);
95             mContext.registerReceiver(configChangeReceiver,
96                     new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
97         }
98 
99         // Create a single holder for all internal displays. External display holders created
100         // lazily.
101         Set<PortraitSize> extraInternalDisplays = new ArraySet<>();
102         for (Display d : mDM.getDisplays()) {
103             if (ApiWrapper.isInternalDisplay(display) && d.getDisplayId() != DEFAULT_DISPLAY) {
104                 Point size = new Point();
105                 d.getRealSize(size);
106                 extraInternalDisplays.add(new PortraitSize(size.x, size.y));
107             }
108         }
109         mInfo = new Info(getDisplayInfoContext(display), display, extraInternalDisplays);
110         mDM.registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler());
111     }
112 
113     @Override
onDisplayAdded(int displayId)114     public final void onDisplayAdded(int displayId) { }
115 
116     @Override
onDisplayRemoved(int displayId)117     public final void onDisplayRemoved(int displayId) { }
118 
119     @WorkerThread
120     @Override
onDisplayChanged(int displayId)121     public final void onDisplayChanged(int displayId) {
122         if (displayId != DEFAULT_DISPLAY) {
123             return;
124         }
125         Display display = mDM.getDisplay(DEFAULT_DISPLAY);
126         if (display == null) {
127             return;
128         }
129         if (Utilities.ATLEAST_S) {
130             // Only check for refresh rate. Everything else comes from component callbacks
131             if (getSingleFrameMs(display) == mInfo.singleFrameMs) {
132                 return;
133             }
134         }
135         handleInfoChange(display);
136     }
137 
getSingleFrameMs(Context context)138     public static int getSingleFrameMs(Context context) {
139         return INSTANCE.get(context).getInfo().singleFrameMs;
140     }
141 
142     /**
143      * Interface for listening for display changes
144      */
145     public interface DisplayInfoChangeListener {
146 
147         /**
148          * Invoked when display info has changed.
149          * @param context updated context associated with the display.
150          * @param info updated display information.
151          * @param flags bitmask indicating type of change.
152          */
onDisplayInfoChanged(Context context, Info info, int flags)153         void onDisplayInfoChanged(Context context, Info info, int flags);
154     }
155 
156     /**
157      * Only used for pre-S
158      */
onConfigChanged(Intent intent)159     private void onConfigChanged(Intent intent) {
160         Configuration config = mContext.getResources().getConfiguration();
161         if (mInfo.fontScale != config.fontScale || mInfo.densityDpi != config.densityDpi) {
162             Log.d(TAG, "Configuration changed, notifying listeners");
163             Display display = mDM.getDisplay(DEFAULT_DISPLAY);
164             if (display != null) {
165                 handleInfoChange(display);
166             }
167         }
168     }
169 
170     @UiThread
171     @Override
172     @TargetApi(Build.VERSION_CODES.S)
onConfigurationChanged(Configuration config)173     public final void onConfigurationChanged(Configuration config) {
174         Display display = mWindowContext.getDisplay();
175         if (config.densityDpi != mInfo.densityDpi
176                 || config.fontScale != mInfo.fontScale
177                 || display.getRotation() != mInfo.rotation
178                 || !mInfo.mScreenSizeDp.equals(
179                         new PortraitSize(config.screenHeightDp, config.screenWidthDp))) {
180             handleInfoChange(display);
181         }
182     }
183 
184     @Override
onLowMemory()185     public final void onLowMemory() { }
186 
addChangeListener(DisplayInfoChangeListener listener)187     public void addChangeListener(DisplayInfoChangeListener listener) {
188         mListeners.add(listener);
189     }
190 
removeChangeListener(DisplayInfoChangeListener listener)191     public void removeChangeListener(DisplayInfoChangeListener listener) {
192         mListeners.remove(listener);
193     }
194 
getInfo()195     public Info getInfo() {
196         return mInfo;
197     }
198 
getDisplayInfoContext(Display display)199     private Context getDisplayInfoContext(Display display) {
200         return Utilities.ATLEAST_S ? mWindowContext : mContext.createDisplayContext(display);
201     }
202 
203     @AnyThread
handleInfoChange(Display display)204     private void handleInfoChange(Display display) {
205         Info oldInfo = mInfo;
206         Set<PortraitSize> extraDisplaysSizes = oldInfo.mAllSizes.size() > 1
207                 ? oldInfo.mAllSizes : Collections.emptySet();
208 
209         Context displayContext = getDisplayInfoContext(display);
210         Info newInfo = new Info(displayContext, display, extraDisplaysSizes);
211         int change = 0;
212         if (!newInfo.mScreenSizeDp.equals(oldInfo.mScreenSizeDp)) {
213             change |= CHANGE_ACTIVE_SCREEN;
214         }
215         if (newInfo.rotation != oldInfo.rotation) {
216             change |= CHANGE_ROTATION;
217         }
218         if (newInfo.singleFrameMs != oldInfo.singleFrameMs) {
219             change |= CHANGE_FRAME_DELAY;
220         }
221         if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale) {
222             change |= CHANGE_DENSITY;
223         }
224         if (!newInfo.supportedBounds.equals(oldInfo.supportedBounds)) {
225             change |= CHANGE_SUPPORTED_BOUNDS;
226         }
227 
228         if (change != 0) {
229             mInfo = newInfo;
230             final int flags = change;
231             MAIN_EXECUTOR.execute(() -> notifyChange(displayContext, flags));
232         }
233     }
234 
notifyChange(Context context, int flags)235     private void notifyChange(Context context, int flags) {
236         for (int i = mListeners.size() - 1; i >= 0; i--) {
237             mListeners.get(i).onDisplayInfoChanged(context, mInfo, flags);
238         }
239     }
240 
241     public static class Info {
242 
243         public final int id;
244         public final int singleFrameMs;
245 
246         // Configuration properties
247         public final int rotation;
248         public final float fontScale;
249         public final int densityDpi;
250 
251         private final PortraitSize mScreenSizeDp;
252         private final Set<PortraitSize> mAllSizes;
253 
254         public final Point currentSize;
255 
256         public final Set<WindowBounds> supportedBounds = new ArraySet<>();
257 
Info(Context context, Display display)258         public Info(Context context, Display display) {
259             this(context, display, Collections.emptySet());
260         }
261 
Info(Context context, Display display, Set<PortraitSize> extraDisplaysSizes)262         private Info(Context context, Display display, Set<PortraitSize> extraDisplaysSizes) {
263             id = display.getDisplayId();
264 
265             rotation = display.getRotation();
266 
267             Configuration config = context.getResources().getConfiguration();
268             fontScale = config.fontScale;
269             densityDpi = config.densityDpi;
270             mScreenSizeDp = new PortraitSize(config.screenHeightDp, config.screenWidthDp);
271 
272             singleFrameMs = getSingleFrameMs(display);
273             currentSize = new Point();
274 
275             display.getRealSize(currentSize);
276 
277             if (extraDisplaysSizes.isEmpty() || !Utilities.ATLEAST_S) {
278                 Point smallestSize = new Point();
279                 Point largestSize = new Point();
280                 display.getCurrentSizeRange(smallestSize, largestSize);
281 
282                 int portraitWidth = Math.min(currentSize.x, currentSize.y);
283                 int portraitHeight = Math.max(currentSize.x, currentSize.y);
284 
285                 supportedBounds.add(new WindowBounds(portraitWidth, portraitHeight,
286                         smallestSize.x, largestSize.y));
287                 supportedBounds.add(new WindowBounds(portraitHeight, portraitWidth,
288                         largestSize.x, smallestSize.y));
289                 mAllSizes = Collections.singleton(new PortraitSize(currentSize.x, currentSize.y));
290             } else {
291                 mAllSizes = new ArraySet<>(extraDisplaysSizes);
292                 mAllSizes.add(new PortraitSize(currentSize.x, currentSize.y));
293                 Set<WindowMetrics> metrics = WindowManagerCompat.getDisplayProfiles(
294                         context, mAllSizes, densityDpi,
295                         ApiWrapper.TASKBAR_DRAWN_IN_PROCESS);
296                 metrics.forEach(wm -> supportedBounds.add(WindowBounds.fromWindowMetrics(wm)));
297             }
298         }
299 
300         /**
301          * Returns true if the bounds represent a tablet
302          */
isTablet(WindowBounds bounds)303         public boolean isTablet(WindowBounds bounds) {
304             return dpiFromPx(Math.min(bounds.bounds.width(), bounds.bounds.height()),
305                     densityDpi) >= MIN_TABLET_WIDTH;
306         }
307     }
308 
309     /**
310      * Utility class to hold a size information in an orientation independent way
311      */
312     public static class PortraitSize {
313         public final int width, height;
314 
PortraitSize(int w, int h)315         public PortraitSize(int w, int h) {
316             width = Math.min(w, h);
317             height = Math.max(w, h);
318         }
319 
320         @Override
equals(Object o)321         public boolean equals(Object o) {
322             if (this == o) return true;
323             if (o == null || getClass() != o.getClass()) return false;
324             PortraitSize that = (PortraitSize) o;
325             return width == that.width && height == that.height;
326         }
327 
328         @Override
hashCode()329         public int hashCode() {
330             return Objects.hash(width, height);
331         }
332     }
333 
getSingleFrameMs(Display display)334     private static int getSingleFrameMs(Display display) {
335         float refreshRate = display.getRefreshRate();
336         return refreshRate > 0 ? (int) (1000 / refreshRate) : 16;
337     }
338 }
339