/* * Copyright (C) 2012 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.server.wm; import static android.app.ActivityManager.StackId.DOCKED_STACK_ID; import static android.app.ActivityManager.StackId.INVALID_STACK_ID; import static android.view.WindowManager.DOCKED_BOTTOM; import static android.view.WindowManager.DOCKED_LEFT; import static android.view.WindowManager.DOCKED_RIGHT; import static android.view.WindowManager.DOCKED_TOP; import static com.android.server.wm.AppTransition.DEFAULT_APP_TRANSITION_DURATION; import static com.android.server.wm.AppTransition.TOUCH_RESPONSE_INTERPOLATOR; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.content.Context; import android.graphics.Rect; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.util.ArraySet; import android.util.Slog; import android.view.DisplayInfo; import android.view.IDockedStackListener; import android.view.SurfaceControl; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import com.android.server.wm.DimLayer.DimLayerUser; import java.util.ArrayList; /** * Keeps information about the docked stack divider. */ public class DockedStackDividerController implements DimLayerUser { private static final String TAG = TAG_WITH_CLASS_NAME ? "DockedStackDividerController" : TAG_WM; /** * The fraction during the maximize/clip reveal animation the divider meets the edge of the clip * revealing surface at the earliest. */ private static final float CLIP_REVEAL_MEET_EARLIEST = 0.6f; /** * The fraction during the maximize/clip reveal animation the divider meets the edge of the clip * revealing surface at the latest. */ private static final float CLIP_REVEAL_MEET_LAST = 1f; /** * If the app translates at least CLIP_REVEAL_MEET_FRACTION_MIN * minimize distance, we start * meet somewhere between {@link #CLIP_REVEAL_MEET_LAST} and {@link #CLIP_REVEAL_MEET_EARLIEST}. */ private static final float CLIP_REVEAL_MEET_FRACTION_MIN = 0.4f; /** * If the app translates equals or more than CLIP_REVEAL_MEET_FRACTION_MIN * minimize distance, * we meet at {@link #CLIP_REVEAL_MEET_EARLIEST}. */ private static final float CLIP_REVEAL_MEET_FRACTION_MAX = 0.8f; private static final Interpolator IME_ADJUST_ENTRY_INTERPOLATOR = new PathInterpolator(0.1f, 0f, 0.1f, 1f); private static final long IME_ADJUST_DURATION = 280; private final WindowManagerService mService; private final DisplayContent mDisplayContent; private int mDividerWindowWidth; private int mDividerInsets; private boolean mResizing; private WindowState mWindow; private final Rect mTmpRect = new Rect(); private final Rect mTmpRect2 = new Rect(); private final Rect mLastRect = new Rect(); private boolean mLastVisibility = false; private final RemoteCallbackList mDockedStackListeners = new RemoteCallbackList<>(); private final DimLayer mDimLayer; private boolean mMinimizedDock; private boolean mAnimatingForMinimizedDockedStack; private boolean mAnimationStarted; private long mAnimationStartTime; private float mAnimationStart; private float mAnimationTarget; private long mAnimationDuration; private final Interpolator mMinimizedDockInterpolator; private float mMaximizeMeetFraction; private final Rect mTouchRegion = new Rect(); private boolean mAnimatingForIme; private boolean mAdjustedForIme; DockedStackDividerController(WindowManagerService service, DisplayContent displayContent) { mService = service; mDisplayContent = displayContent; final Context context = service.mContext; mDimLayer = new DimLayer(displayContent.mService, this, displayContent.getDisplayId(), "DockedStackDim"); mMinimizedDockInterpolator = AnimationUtils.loadInterpolator( context, android.R.interpolator.fast_out_slow_in); loadDimens(); } private void loadDimens() { final Context context = mService.mContext; mDividerWindowWidth = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.docked_stack_divider_thickness); mDividerInsets = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.docked_stack_divider_insets); } void onConfigurationChanged() { loadDimens(); } boolean isResizing() { return mResizing; } int getContentWidth() { return mDividerWindowWidth - 2 * mDividerInsets; } int getContentInsets() { return mDividerInsets; } void setResizing(boolean resizing) { if (mResizing != resizing) { mResizing = resizing; resetDragResizingChangeReported(); } } void setTouchRegion(Rect touchRegion) { mTouchRegion.set(touchRegion); } void getTouchRegion(Rect outRegion) { outRegion.set(mTouchRegion); outRegion.offset(mWindow.getFrameLw().left, mWindow.getFrameLw().top); } private void resetDragResizingChangeReported() { final WindowList windowList = mDisplayContent.getWindowList(); for (int i = windowList.size() - 1; i >= 0; i--) { windowList.get(i).resetDragResizingChangeReported(); } } void setWindow(WindowState window) { mWindow = window; reevaluateVisibility(false); } void reevaluateVisibility(boolean force) { if (mWindow == null) { return; } TaskStack stack = mDisplayContent.mService.mStackIdToStack.get(DOCKED_STACK_ID); // If the stack is invisible, we policy force hide it in WindowAnimator.shouldForceHide final boolean visible = stack != null; if (mLastVisibility == visible && !force) { return; } mLastVisibility = visible; notifyDockedDividerVisibilityChanged(visible); if (!visible) { setResizeDimLayer(false, INVALID_STACK_ID, 0f); } } boolean wasVisible() { return mLastVisibility; } void setAdjustedForIme(boolean adjusted, boolean animate) { if (mAdjustedForIme != adjusted) { mAdjustedForIme = adjusted; if (animate) { startImeAdjustAnimation(adjusted ? 0 : 1, adjusted ? 1 : 0); } } } void positionDockedStackedDivider(Rect frame) { TaskStack stack = mDisplayContent.getDockedStackLocked(); if (stack == null) { // Unfortunately we might end up with still having a divider, even though the underlying // stack was already removed. This is because we are on AM thread and the removal of the // divider was deferred to WM thread and hasn't happened yet. In that case let's just // keep putting it in the same place it was before the stack was removed to have // continuity and prevent it from jumping to the center. It will get hidden soon. frame.set(mLastRect); return; } else { stack.getDimBounds(mTmpRect); } int side = stack.getDockSide(); switch (side) { case DOCKED_LEFT: frame.set(mTmpRect.right - mDividerInsets, frame.top, mTmpRect.right + frame.width() - mDividerInsets, frame.bottom); break; case DOCKED_TOP: frame.set(frame.left, mTmpRect.bottom - mDividerInsets, mTmpRect.right, mTmpRect.bottom + frame.height() - mDividerInsets); break; case DOCKED_RIGHT: frame.set(mTmpRect.left - frame.width() + mDividerInsets, frame.top, mTmpRect.left + mDividerInsets, frame.bottom); break; case DOCKED_BOTTOM: frame.set(frame.left, mTmpRect.top - frame.height() + mDividerInsets, frame.right, mTmpRect.top + mDividerInsets); break; } mLastRect.set(frame); } void notifyDockedDividerVisibilityChanged(boolean visible) { final int size = mDockedStackListeners.beginBroadcast(); for (int i = 0; i < size; ++i) { final IDockedStackListener listener = mDockedStackListeners.getBroadcastItem(i); try { listener.onDividerVisibilityChanged(visible); } catch (RemoteException e) { Slog.e(TAG_WM, "Error delivering divider visibility changed event.", e); } } mDockedStackListeners.finishBroadcast(); } void notifyDockedStackExistsChanged(boolean exists) { final int size = mDockedStackListeners.beginBroadcast(); for (int i = 0; i < size; ++i) { final IDockedStackListener listener = mDockedStackListeners.getBroadcastItem(i); try { listener.onDockedStackExistsChanged(exists); } catch (RemoteException e) { Slog.e(TAG_WM, "Error delivering docked stack exists changed event.", e); } } mDockedStackListeners.finishBroadcast(); if (!exists) { setMinimizedDockedStack(false); } } void notifyDockedStackMinimizedChanged(boolean minimizedDock, long animDuration) { final int size = mDockedStackListeners.beginBroadcast(); for (int i = 0; i < size; ++i) { final IDockedStackListener listener = mDockedStackListeners.getBroadcastItem(i); try { listener.onDockedStackMinimizedChanged(minimizedDock, animDuration); } catch (RemoteException e) { Slog.e(TAG_WM, "Error delivering minimized dock changed event.", e); } } mDockedStackListeners.finishBroadcast(); } void notifyDockSideChanged(int newDockSide) { final int size = mDockedStackListeners.beginBroadcast(); for (int i = 0; i < size; ++i) { final IDockedStackListener listener = mDockedStackListeners.getBroadcastItem(i); try { listener.onDockSideChanged(newDockSide); } catch (RemoteException e) { Slog.e(TAG_WM, "Error delivering dock side changed event.", e); } } mDockedStackListeners.finishBroadcast(); } void registerDockedStackListener(IDockedStackListener listener) { mDockedStackListeners.register(listener); notifyDockedDividerVisibilityChanged(wasVisible()); notifyDockedStackExistsChanged( mDisplayContent.mService.mStackIdToStack.get(DOCKED_STACK_ID) != null); notifyDockedStackMinimizedChanged(mMinimizedDock, 0 /* animDuration */); } void setResizeDimLayer(boolean visible, int targetStackId, float alpha) { SurfaceControl.openTransaction(); final TaskStack stack = mDisplayContent.mService.mStackIdToStack.get(targetStackId); final TaskStack dockedStack = mDisplayContent.getDockedStackLocked(); boolean visibleAndValid = visible && stack != null && dockedStack != null; if (visibleAndValid) { stack.getDimBounds(mTmpRect); if (mTmpRect.height() > 0 && mTmpRect.width() > 0) { mDimLayer.setBounds(mTmpRect); mDimLayer.show(mDisplayContent.mService.mLayersController.getResizeDimLayer(), alpha, 0 /* duration */); } else { visibleAndValid = false; } } if (!visibleAndValid) { mDimLayer.hide(); } SurfaceControl.closeTransaction(); } /** * Notifies the docked stack divider controller of a visibility change that happens without * an animation. */ void notifyAppVisibilityChanged(AppWindowToken wtoken, boolean visible) { final Task task = wtoken.mTask; if (!task.isHomeTask() || !task.isVisibleForUser()) { return; } // If the stack is completely offscreen, this might just be an intermediate state when // docking a task/launching recents at the same time, but home doesn't actually get // visible after the state settles in. if (isWithinDisplay(task) && mDisplayContent.getDockedStackVisibleForUserLocked() != null) { setMinimizedDockedStack(visible, false /* animate */); } } void notifyAppTransitionStarting(ArraySet openingApps, ArraySet closingApps) { if (containsHomeTaskWithinDisplay(openingApps)) { setMinimizedDockedStack(true /* minimized */, true /* animate */); } else if (containsHomeTaskWithinDisplay(closingApps)) { setMinimizedDockedStack(false /* minimized */, true /* animate */); } } private boolean containsHomeTaskWithinDisplay(ArraySet apps) { for (int i = apps.size() - 1; i >= 0; i--) { final Task task = apps.valueAt(i).mTask; if (task != null && task.isHomeTask()) { return isWithinDisplay(task); } } return false; } private boolean isWithinDisplay(Task task) { task.mStack.getBounds(mTmpRect); mDisplayContent.getLogicalDisplayRect(mTmpRect2); return mTmpRect.intersect(mTmpRect2); } /** * Sets whether the docked stack is currently in a minimized state, i.e. all the tasks in the * docked stack are heavily clipped so you can only see a minimal peek state. * * @param minimizedDock Whether the docked stack is currently minimized. * @param animate Whether to animate the change. */ private void setMinimizedDockedStack(boolean minimizedDock, boolean animate) { final boolean wasMinimized = mMinimizedDock; mMinimizedDock = minimizedDock; if (minimizedDock == wasMinimized || mDisplayContent.getDockedStackVisibleForUserLocked() == null) { return; } clearImeAdjustAnimation(); if (minimizedDock) { if (animate) { startAdjustAnimation(0f, 1f); } else { setMinimizedDockedStack(true); } } else { if (animate) { startAdjustAnimation(1f, 0f); } else { setMinimizedDockedStack(false); } } } private void clearImeAdjustAnimation() { final ArrayList stacks = mDisplayContent.getStacks(); for (int i = stacks.size() - 1; i >= 0; --i) { final TaskStack stack = stacks.get(i); if (stack != null && stack.isAdjustedForIme()) { stack.resetAdjustedForIme(true /* adjustBoundsNow */); } } mAnimatingForIme = false; } private void startAdjustAnimation(float from, float to) { mAnimatingForMinimizedDockedStack = true; mAnimationStarted = false; mAnimationStart = from; mAnimationTarget = to; } private void startImeAdjustAnimation(float from, float to) { mAnimatingForIme = true; mAnimationStarted = false; mAnimationStart = from; mAnimationTarget = to; } private void setMinimizedDockedStack(boolean minimized) { final TaskStack stack = mDisplayContent.getDockedStackVisibleForUserLocked(); notifyDockedStackMinimizedChanged(minimized, 0); if (stack == null) { return; } if (stack.setAdjustedForMinimizedDock(minimized ? 1f : 0f)) { mService.mWindowPlacerLocked.performSurfacePlacement(); } } private boolean isAnimationMaximizing() { return mAnimationTarget == 0f; } public boolean animate(long now) { if (mAnimatingForMinimizedDockedStack) { return animateForMinimizedDockedStack(now); } else if (mAnimatingForIme) { return animateForIme(now); } else { return false; } } private boolean animateForIme(long now) { if (!mAnimationStarted) { mAnimationStarted = true; mAnimationStartTime = now; mAnimationDuration = (long) (IME_ADJUST_DURATION * mService.getWindowAnimationScaleLocked()); } float t = Math.min(1f, (float) (now - mAnimationStartTime) / mAnimationDuration); t = (mAnimationTarget == 1f ? IME_ADJUST_ENTRY_INTERPOLATOR : TOUCH_RESPONSE_INTERPOLATOR) .getInterpolation(t); final ArrayList stacks = mDisplayContent.getStacks(); boolean updated = false; for (int i = stacks.size() - 1; i >= 0; --i) { final TaskStack stack = stacks.get(i); if (stack != null && stack.isAdjustedForIme()) { if (t >= 1f && mAnimationTarget == 0f) { stack.resetAdjustedForIme(true /* adjustBoundsNow */); updated = true; } else { updated |= stack.updateAdjustForIme(getInterpolatedAnimationValue(t)); } } } if (updated) { mService.mWindowPlacerLocked.performSurfacePlacement(); } if (t >= 1.0f) { mAnimatingForIme = false; return false; } else { return true; } } private boolean animateForMinimizedDockedStack(long now) { final TaskStack stack = mDisplayContent.getDockedStackVisibleForUserLocked(); if (!mAnimationStarted) { mAnimationStarted = true; mAnimationStartTime = now; final long transitionDuration = isAnimationMaximizing() ? mService.mAppTransition.getLastClipRevealTransitionDuration() : DEFAULT_APP_TRANSITION_DURATION; mAnimationDuration = (long) (transitionDuration * mService.getTransitionAnimationScaleLocked()); mMaximizeMeetFraction = getClipRevealMeetFraction(stack); notifyDockedStackMinimizedChanged(mMinimizedDock, (long) (mAnimationDuration * mMaximizeMeetFraction)); } float t = Math.min(1f, (float) (now - mAnimationStartTime) / mAnimationDuration); t = (isAnimationMaximizing() ? TOUCH_RESPONSE_INTERPOLATOR : mMinimizedDockInterpolator) .getInterpolation(t); if (stack != null) { if (stack.setAdjustedForMinimizedDock(getMinimizeAmount(stack, t))) { mService.mWindowPlacerLocked.performSurfacePlacement(); } } if (t >= 1.0f) { mAnimatingForMinimizedDockedStack = false; return false; } else { return true; } } private float getInterpolatedAnimationValue(float t) { return t * mAnimationTarget + (1 - t) * mAnimationStart; } /** * Gets the amount how much to minimize a stack depending on the interpolated fraction t. */ private float getMinimizeAmount(TaskStack stack, float t) { final float naturalAmount = getInterpolatedAnimationValue(t); if (isAnimationMaximizing()) { return adjustMaximizeAmount(stack, t, naturalAmount); } else { return naturalAmount; } } /** * When maximizing the stack during a clip reveal transition, this adjusts the minimize amount * during the transition such that the edge of the clip reveal rect is met earlier in the * transition so we don't create a visible "hole", but only if both the clip reveal and the * docked stack divider start from about the same portion on the screen. */ private float adjustMaximizeAmount(TaskStack stack, float t, float naturalAmount) { if (mMaximizeMeetFraction == 1f) { return naturalAmount; } final int minimizeDistance = stack.getMinimizeDistance(); float startPrime = mService.mAppTransition.getLastClipRevealMaxTranslation() / (float) minimizeDistance; final float amountPrime = t * mAnimationTarget + (1 - t) * startPrime; final float t2 = Math.min(t / mMaximizeMeetFraction, 1); return amountPrime * t2 + naturalAmount * (1 - t2); } /** * Retrieves the animation fraction at which the docked stack has to meet the clip reveal * edge. See {@link #adjustMaximizeAmount}. */ private float getClipRevealMeetFraction(TaskStack stack) { if (!isAnimationMaximizing() || stack == null || !mService.mAppTransition.hadClipRevealAnimation()) { return 1f; } final int minimizeDistance = stack.getMinimizeDistance(); final float fraction = Math.abs(mService.mAppTransition.getLastClipRevealMaxTranslation()) / (float) minimizeDistance; final float t = Math.max(0, Math.min(1, (fraction - CLIP_REVEAL_MEET_FRACTION_MIN) / (CLIP_REVEAL_MEET_FRACTION_MAX - CLIP_REVEAL_MEET_FRACTION_MIN))); return CLIP_REVEAL_MEET_EARLIEST + (1 - t) * (CLIP_REVEAL_MEET_LAST - CLIP_REVEAL_MEET_EARLIEST); } @Override public boolean isFullscreen() { return false; } @Override public DisplayInfo getDisplayInfo() { return mDisplayContent.getDisplayInfo(); } @Override public void getDimBounds(Rect outBounds) { // This dim layer user doesn't need this. } @Override public String toShortString() { return TAG; } }