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 17 package com.android.wm.shell.common; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ValueAnimator; 22 import android.annotation.IntDef; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.graphics.Point; 26 import android.graphics.Rect; 27 import android.os.RemoteException; 28 import android.os.ServiceManager; 29 import android.util.Slog; 30 import android.util.SparseArray; 31 import android.view.IDisplayWindowInsetsController; 32 import android.view.IWindowManager; 33 import android.view.InsetsSource; 34 import android.view.InsetsSourceControl; 35 import android.view.InsetsState; 36 import android.view.InsetsVisibilities; 37 import android.view.Surface; 38 import android.view.SurfaceControl; 39 import android.view.WindowInsets; 40 import android.view.animation.Interpolator; 41 import android.view.animation.PathInterpolator; 42 43 import androidx.annotation.BinderThread; 44 import androidx.annotation.VisibleForTesting; 45 46 import com.android.internal.view.IInputMethodManager; 47 48 import java.util.ArrayList; 49 import java.util.concurrent.Executor; 50 51 /** 52 * Manages IME control at the display-level. This occurs when IME comes up in multi-window mode. 53 */ 54 public class DisplayImeController implements DisplayController.OnDisplaysChangedListener { 55 private static final String TAG = "DisplayImeController"; 56 57 private static final boolean DEBUG = false; 58 59 // NOTE: All these constants came from InsetsController. 60 public static final int ANIMATION_DURATION_SHOW_MS = 275; 61 public static final int ANIMATION_DURATION_HIDE_MS = 340; 62 public static final Interpolator INTERPOLATOR = new PathInterpolator(0.4f, 0f, 0.2f, 1f); 63 private static final int DIRECTION_NONE = 0; 64 private static final int DIRECTION_SHOW = 1; 65 private static final int DIRECTION_HIDE = 2; 66 private static final int FLOATING_IME_BOTTOM_INSET = -80; 67 68 protected final IWindowManager mWmService; 69 protected final Executor mMainExecutor; 70 private final TransactionPool mTransactionPool; 71 private final DisplayController mDisplayController; 72 private final DisplayInsetsController mDisplayInsetsController; 73 private final SparseArray<PerDisplay> mImePerDisplay = new SparseArray<>(); 74 private final ArrayList<ImePositionProcessor> mPositionProcessors = new ArrayList<>(); 75 76 DisplayImeController(IWindowManager wmService, DisplayController displayController, DisplayInsetsController displayInsetsController, Executor mainExecutor, TransactionPool transactionPool)77 public DisplayImeController(IWindowManager wmService, DisplayController displayController, 78 DisplayInsetsController displayInsetsController, 79 Executor mainExecutor, TransactionPool transactionPool) { 80 mWmService = wmService; 81 mDisplayController = displayController; 82 mDisplayInsetsController = displayInsetsController; 83 mMainExecutor = mainExecutor; 84 mTransactionPool = transactionPool; 85 } 86 87 /** Starts monitor displays changes and set insets controller for each displays. */ startMonitorDisplays()88 public void startMonitorDisplays() { 89 mDisplayController.addDisplayWindowListener(this); 90 } 91 92 @Override onDisplayAdded(int displayId)93 public void onDisplayAdded(int displayId) { 94 // Add's a system-ui window-manager specifically for ime. This type is special because 95 // WM will defer IME inset handling to it in multi-window scenarious. 96 PerDisplay pd = new PerDisplay(displayId, 97 mDisplayController.getDisplayLayout(displayId).rotation()); 98 pd.register(); 99 mImePerDisplay.put(displayId, pd); 100 } 101 102 @Override onDisplayConfigurationChanged(int displayId, Configuration newConfig)103 public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { 104 PerDisplay pd = mImePerDisplay.get(displayId); 105 if (pd == null) { 106 return; 107 } 108 if (mDisplayController.getDisplayLayout(displayId).rotation() 109 != pd.mRotation && isImeShowing(displayId)) { 110 pd.startAnimation(true, false /* forceRestart */); 111 } 112 } 113 114 @Override onDisplayRemoved(int displayId)115 public void onDisplayRemoved(int displayId) { 116 PerDisplay pd = mImePerDisplay.get(displayId); 117 if (pd == null) { 118 return; 119 } 120 pd.unregister(); 121 mImePerDisplay.remove(displayId); 122 } 123 isImeShowing(int displayId)124 private boolean isImeShowing(int displayId) { 125 PerDisplay pd = mImePerDisplay.get(displayId); 126 if (pd == null) { 127 return false; 128 } 129 final InsetsSource imeSource = pd.mInsetsState.getSource(InsetsState.ITYPE_IME); 130 return imeSource != null && pd.mImeSourceControl != null && imeSource.isVisible(); 131 } 132 dispatchPositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t)133 private void dispatchPositionChanged(int displayId, int imeTop, 134 SurfaceControl.Transaction t) { 135 synchronized (mPositionProcessors) { 136 for (ImePositionProcessor pp : mPositionProcessors) { 137 pp.onImePositionChanged(displayId, imeTop, t); 138 } 139 } 140 } 141 142 @ImePositionProcessor.ImeAnimationFlags dispatchStartPositioning(int displayId, int hiddenTop, int shownTop, boolean show, boolean isFloating, SurfaceControl.Transaction t)143 private int dispatchStartPositioning(int displayId, int hiddenTop, int shownTop, 144 boolean show, boolean isFloating, SurfaceControl.Transaction t) { 145 synchronized (mPositionProcessors) { 146 int flags = 0; 147 for (ImePositionProcessor pp : mPositionProcessors) { 148 flags |= pp.onImeStartPositioning( 149 displayId, hiddenTop, shownTop, show, isFloating, t); 150 } 151 return flags; 152 } 153 } 154 dispatchEndPositioning(int displayId, boolean cancel, SurfaceControl.Transaction t)155 private void dispatchEndPositioning(int displayId, boolean cancel, 156 SurfaceControl.Transaction t) { 157 synchronized (mPositionProcessors) { 158 for (ImePositionProcessor pp : mPositionProcessors) { 159 pp.onImeEndPositioning(displayId, cancel, t); 160 } 161 } 162 } 163 dispatchImeControlTargetChanged(int displayId, boolean controlling)164 private void dispatchImeControlTargetChanged(int displayId, boolean controlling) { 165 synchronized (mPositionProcessors) { 166 for (ImePositionProcessor pp : mPositionProcessors) { 167 pp.onImeControlTargetChanged(displayId, controlling); 168 } 169 } 170 } 171 dispatchVisibilityChanged(int displayId, boolean isShowing)172 private void dispatchVisibilityChanged(int displayId, boolean isShowing) { 173 synchronized (mPositionProcessors) { 174 for (ImePositionProcessor pp : mPositionProcessors) { 175 pp.onImeVisibilityChanged(displayId, isShowing); 176 } 177 } 178 } 179 180 /** 181 * Adds an {@link ImePositionProcessor} to be called during ime position updates. 182 */ addPositionProcessor(ImePositionProcessor processor)183 public void addPositionProcessor(ImePositionProcessor processor) { 184 synchronized (mPositionProcessors) { 185 if (mPositionProcessors.contains(processor)) { 186 return; 187 } 188 mPositionProcessors.add(processor); 189 } 190 } 191 192 /** 193 * Removes an {@link ImePositionProcessor} to be called during ime position updates. 194 */ removePositionProcessor(ImePositionProcessor processor)195 public void removePositionProcessor(ImePositionProcessor processor) { 196 synchronized (mPositionProcessors) { 197 mPositionProcessors.remove(processor); 198 } 199 } 200 201 /** An implementation of {@link IDisplayWindowInsetsController} for a given display id. */ 202 public class PerDisplay implements DisplayInsetsController.OnInsetsChangedListener { 203 final int mDisplayId; 204 final InsetsState mInsetsState = new InsetsState(); 205 final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); 206 InsetsSourceControl mImeSourceControl = null; 207 int mAnimationDirection = DIRECTION_NONE; 208 ValueAnimator mAnimation = null; 209 int mRotation = Surface.ROTATION_0; 210 boolean mImeShowing = false; 211 final Rect mImeFrame = new Rect(); 212 boolean mAnimateAlpha = true; 213 PerDisplay(int displayId, int initialRotation)214 public PerDisplay(int displayId, int initialRotation) { 215 mDisplayId = displayId; 216 mRotation = initialRotation; 217 } 218 register()219 public void register() { 220 mDisplayInsetsController.addInsetsChangedListener(mDisplayId, this); 221 } 222 unregister()223 public void unregister() { 224 mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, this); 225 } 226 227 @Override insetsChanged(InsetsState insetsState)228 public void insetsChanged(InsetsState insetsState) { 229 if (mInsetsState.equals(insetsState)) { 230 return; 231 } 232 233 updateImeVisibility(insetsState.getSourceOrDefaultVisibility(InsetsState.ITYPE_IME)); 234 235 final InsetsSource newSource = insetsState.getSource(InsetsState.ITYPE_IME); 236 final Rect newFrame = newSource.getFrame(); 237 final Rect oldFrame = mInsetsState.getSource(InsetsState.ITYPE_IME).getFrame(); 238 239 mInsetsState.set(insetsState, true /* copySources */); 240 if (mImeShowing && !newFrame.equals(oldFrame) && newSource.isVisible()) { 241 if (DEBUG) Slog.d(TAG, "insetsChanged when IME showing, restart animation"); 242 startAnimation(mImeShowing, true /* forceRestart */); 243 } 244 } 245 246 @Override 247 @VisibleForTesting insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls)248 public void insetsControlChanged(InsetsState insetsState, 249 InsetsSourceControl[] activeControls) { 250 insetsChanged(insetsState); 251 InsetsSourceControl imeSourceControl = null; 252 if (activeControls != null) { 253 for (InsetsSourceControl activeControl : activeControls) { 254 if (activeControl == null) { 255 continue; 256 } 257 if (activeControl.getType() == InsetsState.ITYPE_IME) { 258 imeSourceControl = activeControl; 259 } 260 } 261 } 262 263 final boolean hadImeSourceControl = mImeSourceControl != null; 264 final boolean hasImeSourceControl = imeSourceControl != null; 265 if (hadImeSourceControl != hasImeSourceControl) { 266 dispatchImeControlTargetChanged(mDisplayId, hasImeSourceControl); 267 } 268 269 if (hasImeSourceControl) { 270 final Point lastSurfacePosition = mImeSourceControl != null 271 ? mImeSourceControl.getSurfacePosition() : null; 272 final boolean positionChanged = 273 !imeSourceControl.getSurfacePosition().equals(lastSurfacePosition); 274 final boolean leashChanged = 275 !haveSameLeash(mImeSourceControl, imeSourceControl); 276 if (mAnimation != null) { 277 if (positionChanged) { 278 startAnimation(mImeShowing, true /* forceRestart */); 279 } 280 } else { 281 if (leashChanged) { 282 applyVisibilityToLeash(imeSourceControl); 283 } 284 if (!mImeShowing) { 285 removeImeSurface(); 286 } 287 if (mImeSourceControl != null) { 288 mImeSourceControl.release(SurfaceControl::release); 289 } 290 } 291 mImeSourceControl = imeSourceControl; 292 } 293 } 294 applyVisibilityToLeash(InsetsSourceControl imeSourceControl)295 private void applyVisibilityToLeash(InsetsSourceControl imeSourceControl) { 296 SurfaceControl leash = imeSourceControl.getLeash(); 297 if (leash != null) { 298 SurfaceControl.Transaction t = mTransactionPool.acquire(); 299 if (mImeShowing) { 300 t.show(leash); 301 } else { 302 t.hide(leash); 303 } 304 t.apply(); 305 mTransactionPool.release(t); 306 } 307 } 308 309 @Override showInsets(int types, boolean fromIme)310 public void showInsets(int types, boolean fromIme) { 311 if ((types & WindowInsets.Type.ime()) == 0) { 312 return; 313 } 314 if (DEBUG) Slog.d(TAG, "Got showInsets for ime"); 315 startAnimation(true /* show */, false /* forceRestart */); 316 } 317 318 @Override hideInsets(int types, boolean fromIme)319 public void hideInsets(int types, boolean fromIme) { 320 if ((types & WindowInsets.Type.ime()) == 0) { 321 return; 322 } 323 if (DEBUG) Slog.d(TAG, "Got hideInsets for ime"); 324 startAnimation(false /* show */, false /* forceRestart */); 325 } 326 327 @Override topFocusedWindowChanged(String packageName)328 public void topFocusedWindowChanged(String packageName) { 329 // Do nothing 330 } 331 332 /** 333 * Sends the local visibility state back to window manager. Needed for legacy adjustForIme. 334 */ setVisibleDirectly(boolean visible)335 private void setVisibleDirectly(boolean visible) { 336 mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible); 337 mRequestedVisibilities.setVisibility(InsetsState.ITYPE_IME, visible); 338 try { 339 mWmService.updateDisplayWindowRequestedVisibilities(mDisplayId, 340 mRequestedVisibilities); 341 } catch (RemoteException e) { 342 } 343 } 344 imeTop(float surfaceOffset)345 private int imeTop(float surfaceOffset) { 346 return mImeFrame.top + (int) surfaceOffset; 347 } 348 calcIsFloating(InsetsSource imeSource)349 private boolean calcIsFloating(InsetsSource imeSource) { 350 final Rect frame = imeSource.getFrame(); 351 if (frame.height() == 0) { 352 return true; 353 } 354 // Some Floating Input Methods will still report a frame, but the frame is actually 355 // a nav-bar inset created by WM and not part of the IME (despite being reported as 356 // an IME inset). For now, we assume that no non-floating IME will be <= this nav bar 357 // frame height so any reported frame that is <= nav-bar frame height is assumed to 358 // be floating. 359 return frame.height() <= mDisplayController.getDisplayLayout(mDisplayId) 360 .navBarFrameHeight(); 361 } 362 startAnimation(final boolean show, final boolean forceRestart)363 private void startAnimation(final boolean show, final boolean forceRestart) { 364 final InsetsSource imeSource = mInsetsState.getSource(InsetsState.ITYPE_IME); 365 if (imeSource == null || mImeSourceControl == null) { 366 return; 367 } 368 final Rect newFrame = imeSource.getFrame(); 369 final boolean isFloating = calcIsFloating(imeSource) && show; 370 if (isFloating) { 371 // This is a "floating" or "expanded" IME, so to get animations, just 372 // pretend the ime has some size just below the screen. 373 mImeFrame.set(newFrame); 374 final int floatingInset = (int) (mDisplayController.getDisplayLayout(mDisplayId) 375 .density() * FLOATING_IME_BOTTOM_INSET); 376 mImeFrame.bottom -= floatingInset; 377 } else if (newFrame.height() != 0) { 378 // Don't set a new frame if it's empty and hiding -- this maintains continuity 379 mImeFrame.set(newFrame); 380 } 381 if (DEBUG) { 382 Slog.d(TAG, "Run startAnim show:" + show + " was:" 383 + (mAnimationDirection == DIRECTION_SHOW ? "SHOW" 384 : (mAnimationDirection == DIRECTION_HIDE ? "HIDE" : "NONE"))); 385 } 386 if (!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show) 387 || (mAnimationDirection == DIRECTION_HIDE && !show)) { 388 return; 389 } 390 boolean seek = false; 391 float seekValue = 0; 392 if (mAnimation != null) { 393 if (mAnimation.isRunning()) { 394 seekValue = (float) mAnimation.getAnimatedValue(); 395 seek = true; 396 } 397 mAnimation.cancel(); 398 } 399 final float defaultY = mImeSourceControl.getSurfacePosition().y; 400 final float x = mImeSourceControl.getSurfacePosition().x; 401 final float hiddenY = defaultY + mImeFrame.height(); 402 final float shownY = defaultY; 403 final float startY = show ? hiddenY : shownY; 404 final float endY = show ? shownY : hiddenY; 405 if (mAnimationDirection == DIRECTION_NONE && mImeShowing && show) { 406 // IME is already showing, so set seek to end 407 seekValue = shownY; 408 seek = true; 409 } 410 mAnimationDirection = show ? DIRECTION_SHOW : DIRECTION_HIDE; 411 updateImeVisibility(show); 412 mAnimation = ValueAnimator.ofFloat(startY, endY); 413 mAnimation.setDuration( 414 show ? ANIMATION_DURATION_SHOW_MS : ANIMATION_DURATION_HIDE_MS); 415 if (seek) { 416 mAnimation.setCurrentFraction((seekValue - startY) / (endY - startY)); 417 } 418 419 mAnimation.addUpdateListener(animation -> { 420 SurfaceControl.Transaction t = mTransactionPool.acquire(); 421 float value = (float) animation.getAnimatedValue(); 422 t.setPosition(mImeSourceControl.getLeash(), x, value); 423 final float alpha = (mAnimateAlpha || isFloating) 424 ? (value - hiddenY) / (shownY - hiddenY) : 1.f; 425 t.setAlpha(mImeSourceControl.getLeash(), alpha); 426 dispatchPositionChanged(mDisplayId, imeTop(value), t); 427 t.apply(); 428 mTransactionPool.release(t); 429 }); 430 mAnimation.setInterpolator(INTERPOLATOR); 431 mAnimation.addListener(new AnimatorListenerAdapter() { 432 private boolean mCancelled = false; 433 434 @Override 435 public void onAnimationStart(Animator animation) { 436 SurfaceControl.Transaction t = mTransactionPool.acquire(); 437 t.setPosition(mImeSourceControl.getLeash(), x, startY); 438 if (DEBUG) { 439 Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:" 440 + imeTop(hiddenY) + "->" + imeTop(shownY) 441 + " showing:" + (mAnimationDirection == DIRECTION_SHOW)); 442 } 443 int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY), 444 imeTop(shownY), mAnimationDirection == DIRECTION_SHOW, isFloating, t); 445 mAnimateAlpha = (flags & ImePositionProcessor.IME_ANIMATION_NO_ALPHA) == 0; 446 final float alpha = (mAnimateAlpha || isFloating) 447 ? (startY - hiddenY) / (shownY - hiddenY) 448 : 1.f; 449 t.setAlpha(mImeSourceControl.getLeash(), alpha); 450 if (mAnimationDirection == DIRECTION_SHOW) { 451 t.show(mImeSourceControl.getLeash()); 452 } 453 t.apply(); 454 mTransactionPool.release(t); 455 } 456 457 @Override 458 public void onAnimationCancel(Animator animation) { 459 mCancelled = true; 460 } 461 462 @Override 463 public void onAnimationEnd(Animator animation) { 464 if (DEBUG) Slog.d(TAG, "onAnimationEnd " + mCancelled); 465 SurfaceControl.Transaction t = mTransactionPool.acquire(); 466 if (!mCancelled) { 467 t.setPosition(mImeSourceControl.getLeash(), x, endY); 468 t.setAlpha(mImeSourceControl.getLeash(), 1.f); 469 } 470 dispatchEndPositioning(mDisplayId, mCancelled, t); 471 if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) { 472 t.hide(mImeSourceControl.getLeash()); 473 removeImeSurface(); 474 } 475 t.apply(); 476 mTransactionPool.release(t); 477 478 mAnimationDirection = DIRECTION_NONE; 479 mAnimation = null; 480 } 481 }); 482 if (!show) { 483 // When going away, queue up insets change first, otherwise any bounds changes 484 // can have a "flicker" of ime-provided insets. 485 setVisibleDirectly(false /* visible */); 486 } 487 mAnimation.start(); 488 if (show) { 489 // When showing away, queue up insets change last, otherwise any bounds changes 490 // can have a "flicker" of ime-provided insets. 491 setVisibleDirectly(true /* visible */); 492 } 493 } 494 updateImeVisibility(boolean isShowing)495 private void updateImeVisibility(boolean isShowing) { 496 if (mImeShowing != isShowing) { 497 mImeShowing = isShowing; 498 dispatchVisibilityChanged(mDisplayId, isShowing); 499 } 500 } 501 } 502 removeImeSurface()503 void removeImeSurface() { 504 final IInputMethodManager imms = getImms(); 505 if (imms != null) { 506 try { 507 // Remove the IME surface to make the insets invisible for 508 // non-client controlled insets. 509 imms.removeImeSurface(); 510 } catch (RemoteException e) { 511 Slog.e(TAG, "Failed to remove IME surface.", e); 512 } 513 } 514 } 515 516 /** 517 * Allows other things to synchronize with the ime position 518 */ 519 public interface ImePositionProcessor { 520 /** 521 * Indicates that ime shouldn't animate alpha. It will always be opaque. Used when stuff 522 * behind the IME shouldn't be visible (for example during split-screen adjustment where 523 * there is nothing behind the ime). 524 */ 525 int IME_ANIMATION_NO_ALPHA = 1; 526 527 /** @hide */ 528 @IntDef(prefix = {"IME_ANIMATION_"}, value = { 529 IME_ANIMATION_NO_ALPHA, 530 }) 531 @interface ImeAnimationFlags { 532 } 533 534 /** 535 * Called when the IME position is starting to animate. 536 * 537 * @param hiddenTop The y position of the top of the IME surface when it is hidden. 538 * @param shownTop The y position of the top of the IME surface when it is shown. 539 * @param showing {@code true} when we are animating from hidden to shown, {@code false} 540 * when animating from shown to hidden. 541 * @param isFloating {@code true} when the ime is a floating ime (doesn't inset). 542 * @return flags that may alter how ime itself is animated (eg. no-alpha). 543 */ 544 @ImeAnimationFlags onImeStartPositioning(int displayId, int hiddenTop, int shownTop, boolean showing, boolean isFloating, SurfaceControl.Transaction t)545 default int onImeStartPositioning(int displayId, int hiddenTop, int shownTop, 546 boolean showing, boolean isFloating, SurfaceControl.Transaction t) { 547 return 0; 548 } 549 550 /** 551 * Called when the ime position changed. This is expected to be a synchronous call on the 552 * animation thread. Operations can be added to the transaction to be applied in sync. 553 * 554 * @param imeTop The current y position of the top of the IME surface. 555 */ onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t)556 default void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) { 557 } 558 559 /** 560 * Called when the IME position is done animating. 561 * 562 * @param cancel {@code true} if this was cancelled. This implies another start is coming. 563 */ onImeEndPositioning(int displayId, boolean cancel, SurfaceControl.Transaction t)564 default void onImeEndPositioning(int displayId, boolean cancel, 565 SurfaceControl.Transaction t) { 566 } 567 568 /** 569 * Called when the IME control target changed. So that the processor can restore its 570 * adjusted layout when the IME insets is not controlling by the current controller anymore. 571 * 572 * @param controlling indicates whether the current controller is controlling IME insets. 573 */ onImeControlTargetChanged(int displayId, boolean controlling)574 default void onImeControlTargetChanged(int displayId, boolean controlling) { 575 } 576 577 /** 578 * Called when the IME visibility changed. 579 * 580 * @param isShowing {@code true} if the IME is shown. 581 */ onImeVisibilityChanged(int displayId, boolean isShowing)582 default void onImeVisibilityChanged(int displayId, boolean isShowing) { 583 584 } 585 } 586 getImms()587 public IInputMethodManager getImms() { 588 return IInputMethodManager.Stub.asInterface( 589 ServiceManager.getService(Context.INPUT_METHOD_SERVICE)); 590 } 591 haveSameLeash(InsetsSourceControl a, InsetsSourceControl b)592 private static boolean haveSameLeash(InsetsSourceControl a, InsetsSourceControl b) { 593 if (a == b) { 594 return true; 595 } 596 if (a == null || b == null) { 597 return false; 598 } 599 if (a.getLeash() == b.getLeash()) { 600 return true; 601 } 602 if (a.getLeash() == null || b.getLeash() == null) { 603 return false; 604 } 605 return a.getLeash().isSameSurface(b.getLeash()); 606 } 607 } 608