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