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