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