| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.wm; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ValueAnimator; |
| import android.annotation.IntDef; |
| import android.content.Context; |
| import android.content.res.Configuration; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.os.Handler; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| import android.view.IDisplayWindowInsetsController; |
| import android.view.InsetsSource; |
| import android.view.InsetsSourceControl; |
| import android.view.InsetsState; |
| import android.view.Surface; |
| import android.view.SurfaceControl; |
| import android.view.WindowInsets; |
| import android.view.animation.Interpolator; |
| import android.view.animation.PathInterpolator; |
| |
| import com.android.internal.view.IInputMethodManager; |
| import com.android.systemui.TransactionPool; |
| import com.android.systemui.dagger.qualifiers.Main; |
| |
| import java.util.ArrayList; |
| |
| import javax.inject.Inject; |
| import javax.inject.Singleton; |
| |
| /** |
| * Manages IME control at the display-level. This occurs when IME comes up in multi-window mode. |
| */ |
| @Singleton |
| public class DisplayImeController implements DisplayController.OnDisplaysChangedListener { |
| private static final String TAG = "DisplayImeController"; |
| |
| private static final boolean DEBUG = false; |
| |
| // NOTE: All these constants came from InsetsController. |
| public static final int ANIMATION_DURATION_SHOW_MS = 275; |
| public static final int ANIMATION_DURATION_HIDE_MS = 340; |
| public static final Interpolator INTERPOLATOR = new PathInterpolator(0.4f, 0f, 0.2f, 1f); |
| private static final int DIRECTION_NONE = 0; |
| private static final int DIRECTION_SHOW = 1; |
| private static final int DIRECTION_HIDE = 2; |
| private static final int FLOATING_IME_BOTTOM_INSET = -80; |
| |
| SystemWindows mSystemWindows; |
| final Handler mHandler; |
| final TransactionPool mTransactionPool; |
| |
| final SparseArray<PerDisplay> mImePerDisplay = new SparseArray<>(); |
| |
| final ArrayList<ImePositionProcessor> mPositionProcessors = new ArrayList<>(); |
| |
| @Inject |
| public DisplayImeController(SystemWindows syswin, DisplayController displayController, |
| @Main Handler mainHandler, TransactionPool transactionPool) { |
| mHandler = mainHandler; |
| mSystemWindows = syswin; |
| mTransactionPool = transactionPool; |
| displayController.addDisplayWindowListener(this); |
| } |
| |
| @Override |
| public void onDisplayAdded(int displayId) { |
| // Add's a system-ui window-manager specifically for ime. This type is special because |
| // WM will defer IME inset handling to it in multi-window scenarious. |
| PerDisplay pd = new PerDisplay(displayId, |
| mSystemWindows.mDisplayController.getDisplayLayout(displayId).rotation()); |
| try { |
| mSystemWindows.mWmService.setDisplayWindowInsetsController(displayId, pd); |
| } catch (RemoteException e) { |
| Slog.w(TAG, "Unable to set insets controller on display " + displayId); |
| } |
| mImePerDisplay.put(displayId, pd); |
| } |
| |
| @Override |
| public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { |
| PerDisplay pd = mImePerDisplay.get(displayId); |
| if (pd == null) { |
| return; |
| } |
| if (mSystemWindows.mDisplayController.getDisplayLayout(displayId).rotation() |
| != pd.mRotation && isImeShowing(displayId)) { |
| pd.startAnimation(true, false /* forceRestart */); |
| } |
| } |
| |
| @Override |
| public void onDisplayRemoved(int displayId) { |
| try { |
| mSystemWindows.mWmService.setDisplayWindowInsetsController(displayId, null); |
| } catch (RemoteException e) { |
| Slog.w(TAG, "Unable to remove insets controller on display " + displayId); |
| } |
| mImePerDisplay.remove(displayId); |
| } |
| |
| private boolean isImeShowing(int displayId) { |
| PerDisplay pd = mImePerDisplay.get(displayId); |
| if (pd == null) { |
| return false; |
| } |
| final InsetsSource imeSource = pd.mInsetsState.getSource(InsetsState.ITYPE_IME); |
| return imeSource != null && pd.mImeSourceControl != null && imeSource.isVisible(); |
| } |
| |
| private void dispatchPositionChanged(int displayId, int imeTop, |
| SurfaceControl.Transaction t) { |
| synchronized (mPositionProcessors) { |
| for (ImePositionProcessor pp : mPositionProcessors) { |
| pp.onImePositionChanged(displayId, imeTop, t); |
| } |
| } |
| } |
| |
| @ImePositionProcessor.ImeAnimationFlags |
| private int dispatchStartPositioning(int displayId, int hiddenTop, int shownTop, |
| boolean show, boolean isFloating, SurfaceControl.Transaction t) { |
| synchronized (mPositionProcessors) { |
| int flags = 0; |
| for (ImePositionProcessor pp : mPositionProcessors) { |
| flags |= pp.onImeStartPositioning( |
| displayId, hiddenTop, shownTop, show, isFloating, t); |
| } |
| return flags; |
| } |
| } |
| |
| private void dispatchEndPositioning(int displayId, boolean cancel, |
| SurfaceControl.Transaction t) { |
| synchronized (mPositionProcessors) { |
| for (ImePositionProcessor pp : mPositionProcessors) { |
| pp.onImeEndPositioning(displayId, cancel, t); |
| } |
| } |
| } |
| |
| /** |
| * Adds an {@link ImePositionProcessor} to be called during ime position updates. |
| */ |
| public void addPositionProcessor(ImePositionProcessor processor) { |
| synchronized (mPositionProcessors) { |
| if (mPositionProcessors.contains(processor)) { |
| return; |
| } |
| mPositionProcessors.add(processor); |
| } |
| } |
| |
| /** |
| * Removes an {@link ImePositionProcessor} to be called during ime position updates. |
| */ |
| public void removePositionProcessor(ImePositionProcessor processor) { |
| synchronized (mPositionProcessors) { |
| mPositionProcessors.remove(processor); |
| } |
| } |
| |
| class PerDisplay extends IDisplayWindowInsetsController.Stub { |
| final int mDisplayId; |
| final InsetsState mInsetsState = new InsetsState(); |
| InsetsSourceControl mImeSourceControl = null; |
| int mAnimationDirection = DIRECTION_NONE; |
| ValueAnimator mAnimation = null; |
| int mRotation = Surface.ROTATION_0; |
| boolean mImeShowing = false; |
| final Rect mImeFrame = new Rect(); |
| boolean mAnimateAlpha = true; |
| |
| PerDisplay(int displayId, int initialRotation) { |
| mDisplayId = displayId; |
| mRotation = initialRotation; |
| } |
| |
| @Override |
| public void insetsChanged(InsetsState insetsState) { |
| mHandler.post(() -> { |
| if (mInsetsState.equals(insetsState)) { |
| return; |
| } |
| |
| final InsetsSource newSource = insetsState.getSource(InsetsState.ITYPE_IME); |
| final Rect newFrame = newSource.getFrame(); |
| final Rect oldFrame = mInsetsState.getSource(InsetsState.ITYPE_IME).getFrame(); |
| |
| mInsetsState.set(insetsState, true /* copySources */); |
| if (mImeShowing && !newFrame.equals(oldFrame) && newSource.isVisible()) { |
| if (DEBUG) Slog.d(TAG, "insetsChanged when IME showing, restart animation"); |
| startAnimation(mImeShowing, true /* forceRestart */); |
| } |
| }); |
| } |
| |
| @Override |
| public void insetsControlChanged(InsetsState insetsState, |
| InsetsSourceControl[] activeControls) { |
| insetsChanged(insetsState); |
| if (activeControls != null) { |
| for (InsetsSourceControl activeControl : activeControls) { |
| if (activeControl == null) { |
| continue; |
| } |
| if (activeControl.getType() == InsetsState.ITYPE_IME) { |
| mHandler.post(() -> { |
| final Point lastSurfacePosition = mImeSourceControl != null |
| ? mImeSourceControl.getSurfacePosition() : null; |
| mImeSourceControl = activeControl; |
| if (!activeControl.getSurfacePosition().equals(lastSurfacePosition) |
| && mAnimation != null) { |
| startAnimation(mImeShowing, true /* forceRestart */); |
| } else if (!mImeShowing) { |
| removeImeSurface(); |
| } |
| }); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void showInsets(int types, boolean fromIme) { |
| if ((types & WindowInsets.Type.ime()) == 0) { |
| return; |
| } |
| if (DEBUG) Slog.d(TAG, "Got showInsets for ime"); |
| mHandler.post(() -> startAnimation(true /* show */, false /* forceRestart */)); |
| } |
| |
| @Override |
| public void hideInsets(int types, boolean fromIme) { |
| if ((types & WindowInsets.Type.ime()) == 0) { |
| return; |
| } |
| if (DEBUG) Slog.d(TAG, "Got hideInsets for ime"); |
| mHandler.post(() -> startAnimation(false /* show */, false /* forceRestart */)); |
| } |
| |
| /** |
| * Sends the local visibility state back to window manager. Needed for legacy adjustForIme. |
| */ |
| private void setVisibleDirectly(boolean visible) { |
| mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible); |
| try { |
| mSystemWindows.mWmService.modifyDisplayWindowInsets(mDisplayId, mInsetsState); |
| } catch (RemoteException e) { |
| } |
| } |
| |
| private int imeTop(float surfaceOffset) { |
| return mImeFrame.top + (int) surfaceOffset; |
| } |
| |
| private boolean calcIsFloating(InsetsSource imeSource) { |
| final Rect frame = imeSource.getFrame(); |
| if (frame.height() == 0) { |
| return true; |
| } |
| // Some Floating Input Methods will still report a frame, but the frame is actually |
| // a nav-bar inset created by WM and not part of the IME (despite being reported as |
| // an IME inset). For now, we assume that no non-floating IME will be <= this nav bar |
| // frame height so any reported frame that is <= nav-bar frame height is assumed to |
| // be floating. |
| return frame.height() <= mSystemWindows.mDisplayController.getDisplayLayout(mDisplayId) |
| .navBarFrameHeight(); |
| } |
| |
| private void startAnimation(final boolean show, final boolean forceRestart) { |
| final InsetsSource imeSource = mInsetsState.getSource(InsetsState.ITYPE_IME); |
| if (imeSource == null || mImeSourceControl == null) { |
| return; |
| } |
| final Rect newFrame = imeSource.getFrame(); |
| final boolean isFloating = calcIsFloating(imeSource) && show; |
| if (isFloating) { |
| // This is a "floating" or "expanded" IME, so to get animations, just |
| // pretend the ime has some size just below the screen. |
| mImeFrame.set(newFrame); |
| final int floatingInset = (int) ( |
| mSystemWindows.mDisplayController.getDisplayLayout(mDisplayId).density() |
| * FLOATING_IME_BOTTOM_INSET); |
| mImeFrame.bottom -= floatingInset; |
| } else if (newFrame.height() != 0) { |
| // Don't set a new frame if it's empty and hiding -- this maintains continuity |
| mImeFrame.set(newFrame); |
| } |
| if (DEBUG) { |
| Slog.d(TAG, "Run startAnim show:" + show + " was:" |
| + (mAnimationDirection == DIRECTION_SHOW ? "SHOW" |
| : (mAnimationDirection == DIRECTION_HIDE ? "HIDE" : "NONE"))); |
| } |
| if (!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show) |
| || (mAnimationDirection == DIRECTION_HIDE && !show)) { |
| return; |
| } |
| boolean seek = false; |
| float seekValue = 0; |
| if (mAnimation != null) { |
| if (mAnimation.isRunning()) { |
| seekValue = (float) mAnimation.getAnimatedValue(); |
| seek = true; |
| } |
| mAnimation.cancel(); |
| } |
| final float defaultY = mImeSourceControl.getSurfacePosition().y; |
| final float x = mImeSourceControl.getSurfacePosition().x; |
| final float hiddenY = defaultY + mImeFrame.height(); |
| final float shownY = defaultY; |
| final float startY = show ? hiddenY : shownY; |
| final float endY = show ? shownY : hiddenY; |
| if (mAnimationDirection == DIRECTION_NONE && mImeShowing && show) { |
| // IME is already showing, so set seek to end |
| seekValue = shownY; |
| seek = true; |
| } |
| mAnimationDirection = show ? DIRECTION_SHOW : DIRECTION_HIDE; |
| mImeShowing = show; |
| mAnimation = ValueAnimator.ofFloat(startY, endY); |
| mAnimation.setDuration( |
| show ? ANIMATION_DURATION_SHOW_MS : ANIMATION_DURATION_HIDE_MS); |
| if (seek) { |
| mAnimation.setCurrentFraction((seekValue - startY) / (endY - startY)); |
| } |
| |
| mAnimation.addUpdateListener(animation -> { |
| SurfaceControl.Transaction t = mTransactionPool.acquire(); |
| float value = (float) animation.getAnimatedValue(); |
| t.setPosition(mImeSourceControl.getLeash(), x, value); |
| final float alpha = (mAnimateAlpha || isFloating) |
| ? (value - hiddenY) / (shownY - hiddenY) : 1.f; |
| t.setAlpha(mImeSourceControl.getLeash(), alpha); |
| dispatchPositionChanged(mDisplayId, imeTop(value), t); |
| t.apply(); |
| mTransactionPool.release(t); |
| }); |
| mAnimation.setInterpolator(INTERPOLATOR); |
| mAnimation.addListener(new AnimatorListenerAdapter() { |
| private boolean mCancelled = false; |
| @Override |
| public void onAnimationStart(Animator animation) { |
| SurfaceControl.Transaction t = mTransactionPool.acquire(); |
| t.setPosition(mImeSourceControl.getLeash(), x, startY); |
| if (DEBUG) { |
| Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:" |
| + imeTop(hiddenY) + "->" + imeTop(shownY) |
| + " showing:" + (mAnimationDirection == DIRECTION_SHOW)); |
| } |
| int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY), |
| imeTop(shownY), mAnimationDirection == DIRECTION_SHOW, isFloating, t); |
| mAnimateAlpha = (flags & ImePositionProcessor.IME_ANIMATION_NO_ALPHA) == 0; |
| final float alpha = (mAnimateAlpha || isFloating) |
| ? (startY - hiddenY) / (shownY - hiddenY) |
| : 1.f; |
| t.setAlpha(mImeSourceControl.getLeash(), alpha); |
| if (mAnimationDirection == DIRECTION_SHOW) { |
| t.show(mImeSourceControl.getLeash()); |
| } |
| t.apply(); |
| mTransactionPool.release(t); |
| } |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| mCancelled = true; |
| } |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (DEBUG) Slog.d(TAG, "onAnimationEnd " + mCancelled); |
| SurfaceControl.Transaction t = mTransactionPool.acquire(); |
| if (!mCancelled) { |
| t.setPosition(mImeSourceControl.getLeash(), x, endY); |
| t.setAlpha(mImeSourceControl.getLeash(), 1.f); |
| } |
| dispatchEndPositioning(mDisplayId, mCancelled, t); |
| if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) { |
| t.hide(mImeSourceControl.getLeash()); |
| removeImeSurface(); |
| } |
| t.apply(); |
| mTransactionPool.release(t); |
| |
| mAnimationDirection = DIRECTION_NONE; |
| mAnimation = null; |
| } |
| }); |
| if (!show) { |
| // When going away, queue up insets change first, otherwise any bounds changes |
| // can have a "flicker" of ime-provided insets. |
| setVisibleDirectly(false /* visible */); |
| } |
| mAnimation.start(); |
| if (show) { |
| // When showing away, queue up insets change last, otherwise any bounds changes |
| // can have a "flicker" of ime-provided insets. |
| setVisibleDirectly(true /* visible */); |
| } |
| } |
| } |
| |
| void removeImeSurface() { |
| final IInputMethodManager imms = getImms(); |
| if (imms != null) { |
| try { |
| // Remove the IME surface to make the insets invisible for |
| // non-client controlled insets. |
| imms.removeImeSurface(); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Failed to remove IME surface.", e); |
| } |
| } |
| } |
| |
| /** |
| * Allows other things to synchronize with the ime position |
| */ |
| public interface ImePositionProcessor { |
| /** |
| * Indicates that ime shouldn't animate alpha. It will always be opaque. Used when stuff |
| * behind the IME shouldn't be visible (for example during split-screen adjustment where |
| * there is nothing behind the ime). |
| */ |
| int IME_ANIMATION_NO_ALPHA = 1; |
| |
| /** @hide */ |
| @IntDef(prefix = { "IME_ANIMATION_" }, value = { |
| IME_ANIMATION_NO_ALPHA, |
| }) |
| @interface ImeAnimationFlags {} |
| |
| /** |
| * Called when the IME position is starting to animate. |
| * |
| * @param hiddenTop The y position of the top of the IME surface when it is hidden. |
| * @param shownTop The y position of the top of the IME surface when it is shown. |
| * @param showing {@code true} when we are animating from hidden to shown, {@code false} |
| * when animating from shown to hidden. |
| * @param isFloating {@code true} when the ime is a floating ime (doesn't inset). |
| * @return flags that may alter how ime itself is animated (eg. no-alpha). |
| */ |
| @ImeAnimationFlags |
| default int onImeStartPositioning(int displayId, int hiddenTop, int shownTop, |
| boolean showing, boolean isFloating, SurfaceControl.Transaction t) { |
| return 0; |
| } |
| |
| /** |
| * Called when the ime position changed. This is expected to be a synchronous call on the |
| * animation thread. Operations can be added to the transaction to be applied in sync. |
| * |
| * @param imeTop The current y position of the top of the IME surface. |
| */ |
| default void onImePositionChanged(int displayId, int imeTop, |
| SurfaceControl.Transaction t) {} |
| |
| /** |
| * Called when the IME position is done animating. |
| * |
| * @param cancel {@code true} if this was cancelled. This implies another start is coming. |
| */ |
| default void onImeEndPositioning(int displayId, boolean cancel, |
| SurfaceControl.Transaction t) {} |
| } |
| |
| public IInputMethodManager getImms() { |
| return IInputMethodManager.Stub.asInterface( |
| ServiceManager.getService(Context.INPUT_METHOD_SERVICE)); |
| } |
| } |