diff options
9 files changed, 267 insertions, 11 deletions
diff --git a/api/current.txt b/api/current.txt index 15f10faa4c4a..56846beef518 100644 --- a/api/current.txt +++ b/api/current.txt @@ -55587,10 +55587,12 @@ package android.view { } public interface WindowInsetsController { + method public void addOnControllableInsetsChangedListener(@NonNull android.view.WindowInsetsController.OnControllableInsetsChangedListener); method @NonNull public android.os.CancellationSignal controlWindowInsetsAnimation(int, long, @Nullable android.view.animation.Interpolator, @NonNull android.view.WindowInsetsAnimationControlListener); method public int getSystemBarsAppearance(); method public int getSystemBarsBehavior(); method public void hide(int); + method public void removeOnControllableInsetsChangedListener(@NonNull android.view.WindowInsetsController.OnControllableInsetsChangedListener); method public void setSystemBarsAppearance(int, int); method public void setSystemBarsBehavior(int); method public void show(int); @@ -55601,6 +55603,10 @@ package android.view { field public static final int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; // 0x2 } + public static interface WindowInsetsController.OnControllableInsetsChangedListener { + method public void onControllableInsetsChanged(@NonNull android.view.WindowInsetsController, int); + } + public interface WindowManager extends android.view.ViewManager { method @NonNull public default android.view.WindowMetrics getCurrentWindowMetrics(); method @Deprecated public android.view.Display getDefaultDisplay(); diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java index 0513feef801f..6efd03c44b9f 100644 --- a/core/java/android/inputmethodservice/SoftInputWindow.java +++ b/core/java/android/inputmethodservice/SoftInputWindow.java @@ -28,6 +28,7 @@ import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.View; import android.view.WindowManager; import java.lang.annotation.Retention; @@ -94,6 +95,13 @@ public class SoftInputWindow extends Dialog { lp.token = token; getWindow().setAttributes(lp); updateWindowState(SoftInputWindowState.TOKEN_SET); + + // As soon as we have a token, make sure the window is added (but not shown) by + // setting visibility to INVISIBLE and calling show() on Dialog. Note that + // WindowInsetsController.OnControllableInsetsChangedListener relies on the window + // being added to function. + getWindow().getDecorView().setVisibility(View.INVISIBLE); + show(); return; case SoftInputWindowState.TOKEN_SET: case SoftInputWindowState.SHOWN_AT_LEAST_ONCE: diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 302d4f3f79c8..607886f3b13b 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -52,6 +52,7 @@ import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -59,6 +60,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.function.BiFunction; /** @@ -306,6 +308,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private SyncRtSurfaceTransactionApplier mApplier; private Runnable mPendingControlTimeout = this::abortPendingImeControlRequest; + private final ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners + = new ArrayList<>(); + + /** Set of inset types for which an animation was started since last resetting this field */ + private @InsetsType int mLastStartedAnimTypes; public InsetsController(ViewRootImpl viewRoot) { this(viewRoot, (controller, type) -> { @@ -459,6 +466,13 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } mTmpControlArray.clear(); + + // Do not override any animations that the app started in the OnControllableInsetsChanged + // listeners. + int animatingTypes = invokeControllableInsetsChangedListeners(); + showTypes[0] &= ~animatingTypes; + hideTypes[0] &= ~animatingTypes; + if (showTypes[0] != 0) { applyAnimation(showTypes[0], true /* show */, false /* fromIme */); } @@ -542,9 +556,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private CancellationSignal controlWindowInsetsAnimation(@InsetsType int types, WindowInsetsAnimationControlListener listener, boolean fromIme, long durationMs, @Nullable Interpolator interpolator, @AnimationType int animationType) { - // If the frame of our window doesn't span the entire display, the control API makes very - // little sense, as we don't deal with negative insets. So just cancel immediately. - if (!mState.getDisplayFrame().equals(mFrame)) { + if (!checkDisplayFramesForControlling()) { listener.onCancelled(); CancellationSignal cancellationSignal = new CancellationSignal(); cancellationSignal.cancel(); @@ -554,6 +566,13 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation false /* fade */, animationType, getLayoutInsetsDuringAnimationMode(types)); } + private boolean checkDisplayFramesForControlling() { + + // If the frame of our window doesn't span the entire display, the control API makes very + // little sense, as we don't deal with negative insets. So just cancel immediately. + return mState.getDisplayFrame().equals(mFrame); + } + private CancellationSignal controlAnimationUnchecked(@InsetsType int types, WindowInsetsAnimationControlListener listener, Rect frame, boolean fromIme, long durationMs, Interpolator interpolator, boolean fade, @@ -567,6 +586,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation return cancellationSignal; } cancelExistingControllers(types); + mLastStartedAnimTypes |= types; final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types); final SparseArray<InsetsSourceControl> controls = new SparseArray<>(); @@ -994,6 +1014,48 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation return mViewRoot.mWindowAttributes.insetsFlags.behavior; } + private @InsetsType int calculateControllableTypes() { + if (!checkDisplayFramesForControlling()) { + return 0; + } + @InsetsType int result = 0; + for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { + InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); + if (consumer.getControl() != null) { + result |= toPublicType(consumer.mType); + } + } + return result; + } + + /** + * @return The types that are now animating due to a listener invoking control/show/hide + */ + private @InsetsType int invokeControllableInsetsChangedListeners() { + mLastStartedAnimTypes = 0; + @InsetsType int types = calculateControllableTypes(); + int size = mControllableInsetsChangedListeners.size(); + for (int i = 0; i < size; i++) { + mControllableInsetsChangedListeners.get(i).onControllableInsetsChanged(this, types); + } + return mLastStartedAnimTypes; + } + + @Override + public void addOnControllableInsetsChangedListener( + OnControllableInsetsChangedListener listener) { + Objects.requireNonNull(listener); + mControllableInsetsChangedListeners.add(listener); + listener.onControllableInsetsChanged(this, calculateControllableTypes()); + } + + @Override + public void removeOnControllableInsetsChangedListener( + OnControllableInsetsChangedListener listener) { + Objects.requireNonNull(listener); + mControllableInsetsChangedListeners.remove(listener); + } + /** * At the time we receive new leashes (e.g. InsetsSourceConsumer is processing * setControl) we need to release the old leash. But we may have already scheduled diff --git a/core/java/android/view/PendingInsetsController.java b/core/java/android/view/PendingInsetsController.java index c0ed9359c613..7f3641803770 100644 --- a/core/java/android/view/PendingInsetsController.java +++ b/core/java/android/view/PendingInsetsController.java @@ -38,6 +38,8 @@ public class PendingInsetsController implements WindowInsetsController { private @Behavior int mBehavior = KEEP_BEHAVIOR; private final InsetsState mDummyState = new InsetsState(); private InsetsController mReplayedInsetsController; + private ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners + = new ArrayList<>(); @Override public void show(int types) { @@ -112,6 +114,27 @@ public class PendingInsetsController implements WindowInsetsController { return mDummyState; } + @Override + public void addOnControllableInsetsChangedListener( + OnControllableInsetsChangedListener listener) { + if (mReplayedInsetsController != null) { + mReplayedInsetsController.addOnControllableInsetsChangedListener(listener); + } else { + mControllableInsetsChangedListeners.add(listener); + listener.onControllableInsetsChanged(this, 0); + } + } + + @Override + public void removeOnControllableInsetsChangedListener( + OnControllableInsetsChangedListener listener) { + if (mReplayedInsetsController != null) { + mReplayedInsetsController.removeOnControllableInsetsChangedListener(listener); + } else { + mControllableInsetsChangedListeners.remove(listener); + } + } + /** * Replays the commands on {@code controller} and attaches it to this instance such that any * calls will be forwarded to the real instance in the future. @@ -128,9 +151,15 @@ public class PendingInsetsController implements WindowInsetsController { for (int i = 0; i < size; i++) { mRequests.get(i).replay(controller); } + size = mControllableInsetsChangedListeners.size(); + for (int i = 0; i < size; i++) { + controller.addOnControllableInsetsChangedListener( + mControllableInsetsChangedListeners.get(i)); + } // Reset all state so it doesn't get applied twice just in case mRequests.clear(); + mControllableInsetsChangedListeners.clear(); mBehavior = KEEP_BEHAVIOR; mAppearance = 0; mAppearanceMask = 0; diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java index b7ca03798bbe..2ad557e6d9f6 100644 --- a/core/java/android/view/WindowInsetsController.java +++ b/core/java/android/view/WindowInsetsController.java @@ -20,7 +20,9 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Insets; +import android.inputmethodservice.InputMethodService; import android.os.CancellationSignal; +import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.animation.Interpolator; @@ -212,4 +214,55 @@ public interface WindowInsetsController { * @hide */ InsetsState getState(); + + /** + * Adds a {@link OnControllableInsetsChangedListener} to the window insets controller. + * + * @param listener The listener to add. + * + * @see OnControllableInsetsChangedListener + * @see #removeOnControllableInsetsChangedListener(OnControllableInsetsChangedListener) + */ + void addOnControllableInsetsChangedListener( + @NonNull OnControllableInsetsChangedListener listener); + + /** + * Removes a {@link OnControllableInsetsChangedListener} from the window insets controller. + * + * @param listener The listener to remove. + * + * @see OnControllableInsetsChangedListener + * @see #addOnControllableInsetsChangedListener(OnControllableInsetsChangedListener) + */ + void removeOnControllableInsetsChangedListener( + @NonNull OnControllableInsetsChangedListener listener); + + /** + * Listener to be notified when the set of controllable {@link InsetsType} controlled by a + * {@link WindowInsetsController} changes. + * <p> + * Once a {@link InsetsType} becomes controllable, the app will be able to control the window + * that is causing this type of insets by calling {@link #controlWindowInsetsAnimation}. + * <p> + * Note: When listening to controllability of the {@link Type#ime}, + * {@link #controlWindowInsetsAnimation} may still fail in case the {@link InputMethodService} + * decides to cancel the show request. This could happen when there is a hardware keyboard + * attached. + * + * @see #addOnControllableInsetsChangedListener(OnControllableInsetsChangedListener) + * @see #removeOnControllableInsetsChangedListener(OnControllableInsetsChangedListener) + */ + interface OnControllableInsetsChangedListener { + + /** + * Called when the set of controllable {@link InsetsType} changes. + * + * @param controller The controller for which the set of controllable {@link InsetsType}s + * are changing. + * @param typeMask Bitwise type-mask of the {@link InsetsType}s the controller is currently + * able to control. + */ + void onControllableInsetsChanged(@NonNull WindowInsetsController controller, + @InsetsType int typeMask); + } } diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index 7737b1a2a776..023fc1736aca 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -49,6 +49,7 @@ import android.os.CancellationSignal; import android.platform.test.annotations.Presubmit; import android.view.SurfaceControl.Transaction; import android.view.WindowInsets.Type; +import android.view.WindowInsetsController.OnControllableInsetsChangedListener; import android.view.WindowManager.BadTokenException; import android.view.WindowManager.LayoutParams; import android.view.animation.LinearInterpolator; @@ -67,6 +68,8 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mockito; import java.util.concurrent.CountDownLatch; import java.util.function.Supplier; @@ -171,15 +174,24 @@ public class InsetsControllerTest { mController.onControlsChanged(new InsetsSourceControl[] { control }); assertEquals(mLeash, mController.getSourceConsumer(ITYPE_STATUS_BAR).getControl().getLeash()); + mController.addOnControllableInsetsChangedListener( + ((controller, typeMask) -> assertEquals(statusBars(), typeMask))); } @Test public void testControlsRevoked() { + OnControllableInsetsChangedListener listener + = mock(OnControllableInsetsChangedListener.class); + mController.addOnControllableInsetsChangedListener(listener); InsetsSourceControl control = new InsetsSourceControl(ITYPE_STATUS_BAR, mLeash, new Point()); mController.onControlsChanged(new InsetsSourceControl[] { control }); mController.onControlsChanged(new InsetsSourceControl[0]); assertNull(mController.getSourceConsumer(ITYPE_STATUS_BAR).getControl()); + InOrder inOrder = Mockito.inOrder(listener); + inOrder.verify(listener).onControllableInsetsChanged(eq(mController), eq(0)); + inOrder.verify(listener).onControllableInsetsChanged(eq(mController), eq(statusBars())); + inOrder.verify(listener).onControllableInsetsChanged(eq(mController), eq(0)); } @Test @@ -206,10 +218,15 @@ public class InsetsControllerTest { public void testFrameDoesntMatchDisplay() { mController.onFrameChanged(new Rect(0, 0, 100, 100)); mController.getState().setDisplayFrame(new Rect(0, 0, 200, 200)); + InsetsSourceControl control = + new InsetsSourceControl(ITYPE_STATUS_BAR, mLeash, new Point()); + mController.onControlsChanged(new InsetsSourceControl[] { control }); WindowInsetsAnimationControlListener controlListener = mock(WindowInsetsAnimationControlListener.class); mController.controlWindowInsetsAnimation(0, 0 /* durationMs */, new LinearInterpolator(), controlListener); + mController.addOnControllableInsetsChangedListener( + (controller, typeMask) -> assertEquals(0, typeMask)); verify(controlListener).onCancelled(); verify(controlListener, never()).onReady(any(), anyInt()); } diff --git a/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java b/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java index 9787b7780702..9797178fca6e 100644 --- a/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java @@ -25,12 +25,14 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.os.CancellationSignal; import android.platform.test.annotations.Presubmit; +import android.view.WindowInsetsController.OnControllableInsetsChangedListener; import android.view.animation.LinearInterpolator; import org.junit.Before; @@ -163,11 +165,43 @@ public class PendingInsetsControllerTest { } @Test + public void testAddOnControllableInsetsChangedListener() { + OnControllableInsetsChangedListener listener = + mock(OnControllableInsetsChangedListener.class); + mPendingInsetsController.addOnControllableInsetsChangedListener(listener); + mPendingInsetsController.replayAndAttach(mReplayedController); + verify(mReplayedController).addOnControllableInsetsChangedListener(eq(listener)); + verify(listener).onControllableInsetsChanged(eq(mPendingInsetsController), eq(0)); + } + + @Test + public void testAddRemoveControllableInsetsChangedListener() { + OnControllableInsetsChangedListener listener = + mock(OnControllableInsetsChangedListener.class); + mPendingInsetsController.addOnControllableInsetsChangedListener(listener); + mPendingInsetsController.removeOnControllableInsetsChangedListener(listener); + mPendingInsetsController.replayAndAttach(mReplayedController); + verify(mReplayedController, never()).addOnControllableInsetsChangedListener(any()); + verify(listener).onControllableInsetsChanged(eq(mPendingInsetsController), eq(0)); + } + + @Test + public void testAddOnControllableInsetsChangedListener_direct() { + mPendingInsetsController.replayAndAttach(mReplayedController); + OnControllableInsetsChangedListener listener = + mock(OnControllableInsetsChangedListener.class); + mPendingInsetsController.addOnControllableInsetsChangedListener(listener); + verify(mReplayedController).addOnControllableInsetsChangedListener(eq(listener)); + } + + @Test public void testReplayTwice() { mPendingInsetsController.show(systemBars()); mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); mPendingInsetsController.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS); + mPendingInsetsController.addOnControllableInsetsChangedListener( + (controller, typeMask) -> {}); mPendingInsetsController.replayAndAttach(mReplayedController); InsetsController secondController = mock(InsetsController.class); mPendingInsetsController.replayAndAttach(secondController); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 42a66adff5e5..ecbbb03a7c94 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -2447,10 +2447,6 @@ public class WindowManagerService extends IWindowManager.Stub // of a transaction to avoid artifacts. win.mAnimatingExit = true; } else { - final DisplayContent displayContent = win.getDisplayContent(); - if (displayContent.mInputMethodWindow == win) { - displayContent.setInputMethodWindowLocked(null); - } boolean stopped = win.mActivityRecord != null ? win.mActivityRecord.mAppStopped : true; // We set mDestroying=true so ActivityRecord#notifyAppStopped in-to destroy surfaces // will later actually destroy the surface if we do not do so here. Normally we leave diff --git a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/WindowInsetsActivity.java b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/WindowInsetsActivity.java index 8e6f1985a7d9..e41517085c36 100644 --- a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/WindowInsetsActivity.java +++ b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/WindowInsetsActivity.java @@ -16,11 +16,15 @@ package com.google.android.test.windowinsetstests; +import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; import static java.lang.Math.max; import static java.lang.Math.min; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -37,6 +41,8 @@ import android.view.WindowInsetsAnimation; import android.view.WindowInsetsAnimation.Callback; import android.view.WindowInsetsAnimationControlListener; import android.view.WindowInsetsAnimationController; +import android.view.WindowInsetsController; +import android.view.WindowInsetsController.OnControllableInsetsChangedListener; import android.view.animation.LinearInterpolator; import android.widget.LinearLayout; @@ -82,8 +88,8 @@ public class WindowInsetsActivity extends AppCompatActivity { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mDown = event.getY(); - mDownInsets = v.getRootWindowInsets().getInsets(Type.ime()); - mShownAtDown = v.getRootWindowInsets().isVisible(Type.ime()); + mDownInsets = v.getRootWindowInsets().getInsets(ime()); + mShownAtDown = v.getRootWindowInsets().isVisible(ime()); mRequestedController = false; mCurrentRequest = null; break; @@ -94,7 +100,7 @@ public class WindowInsetsActivity extends AppCompatActivity { > mViewConfiguration.getScaledTouchSlop() && !mRequestedController) { mRequestedController = true; - v.getWindowInsetsController().controlWindowInsetsAnimation(Type.ime(), + v.getWindowInsetsController().controlWindowInsetsAnimation(ime(), 1000, new LinearInterpolator(), mCurrentRequest = new WindowInsetsAnimationControlListener() { @Override @@ -189,6 +195,51 @@ public class WindowInsetsActivity extends AppCompatActivity { getWindow().getDecorView().post(() -> getWindow().setDecorFitsSystemWindows(false)); } + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + getWindow().getInsetsController().addOnControllableInsetsChangedListener( + new OnControllableInsetsChangedListener() { + + boolean hasControl = false; + @Override + public void onControllableInsetsChanged(WindowInsetsController controller, + int types) { + if ((types & ime()) != 0 && !hasControl) { + hasControl = true; + controller.controlWindowInsetsAnimation(ime(), -1, + new LinearInterpolator(), + new WindowInsetsAnimationControlListener() { + @Override + public void onReady( + WindowInsetsAnimationController controller, + int types) { + ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); + anim.setDuration(1500); + anim.addUpdateListener(animation + -> controller.setInsetsAndAlpha( + controller.getShownStateInsets(), + (float) animation.getAnimatedValue(), + anim.getAnimatedFraction())); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + controller.finish(true); + } + }); + anim.start(); + } + + @Override + public void onCancelled() { + } + }); + } + } + }); + } + static class Transition { private int mEndBottom; private int mStartBottom; @@ -200,7 +251,7 @@ public class WindowInsetsActivity extends AppCompatActivity { } void onPrepare(WindowInsetsAnimation animation) { - if ((animation.getTypeMask() & Type.ime()) != 0) { + if ((animation.getTypeMask() & ime()) != 0) { mInsetsAnimation = animation; } mStartBottom = mView.getBottom(); |