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