blob: 6f7e64f28e7481eae3e20b6e48b376b720d385d5 [file] [log] [blame]
/*
* 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 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 final WindowManagerService mService;
private final DisplayContent mDisplayContent;
private final int mDividerWindowWidth;
private final 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<IDockedStackListener> 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;
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);
mDimLayer = new DimLayer(displayContent.mService, this, displayContent.getDisplayId(),
"DockedStackDim");
mMinimizedDockInterpolator = AnimationUtils.loadInterpolator(
context, android.R.interpolator.fast_out_slow_in);
}
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) {
mAnimatingForIme = animate;
mAdjustedForIme = adjusted;
}
}
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 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<AppWindowToken> openingApps,
ArraySet<AppWindowToken> closingApps) {
if (containsHomeTaskWithinDisplay(openingApps)) {
setMinimizedDockedStack(true /* minimized */, true /* animate */);
} else if (containsHomeTaskWithinDisplay(closingApps)) {
setMinimizedDockedStack(false /* minimized */, true /* animate */);
}
}
private boolean containsHomeTaskWithinDisplay(ArraySet<AppWindowToken> 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;
}
mAnimatingForIme = false;
if (minimizedDock) {
if (animate) {
startAdjustAnimation(0f, 1f);
} else {
setMinimizedDockedStack(true);
}
} else {
if (animate) {
startAdjustAnimation(1f, 0f);
} else {
setMinimizedDockedStack(false);
}
}
}
private void startAdjustAnimation(float from, float to) {
mAnimatingForMinimizedDockedStack = 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();
} else {
return false;
}
}
private boolean animateForIme() {
boolean updated = false;
boolean animating = false;
final ArrayList<TaskStack> stacks = mDisplayContent.getStacks();
for (int i = stacks.size() - 1; i >= 0; --i) {
final TaskStack stack = stacks.get(i);
if (stack != null && stack.isAdjustedForIme()) {
updated |= stack.updateAdjustForIme();
animating |= stack.isAnimatingForIme();
}
}
if (updated) {
mService.mWindowPlacerLocked.performSurfacePlacement();
}
if (!animating) {
mAnimatingForIme = false;
for (int i = stacks.size() - 1; i >= 0; --i) {
final TaskStack stack = stacks.get(i);
if (stack != null) {
stack.clearImeGoingAway();
}
}
}
return animating;
}
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;
}
}
/**
* 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 = t * mAnimationTarget + (1 - t) * mAnimationStart;
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;
}
}