blob: 753dfa7396f8453955a5ac471a8d32b59f05bf0c [file] [log] [blame]
/*
* Copyright (C) 2021 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.wm.shell.common.split;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static com.android.wm.shell.common.split.SplitScreenConstants.FADE_DURATION;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Binder;
import android.view.IWindow;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.SurfaceSession;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.common.SurfaceUtils;
import java.util.function.Consumer;
/**
* Handles split decor like showing resizing hint for a specific split.
*/
public class SplitDecorManager extends WindowlessWindowManager {
private static final String TAG = SplitDecorManager.class.getSimpleName();
private static final String RESIZING_BACKGROUND_SURFACE_NAME = "ResizingBackground";
private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground";
private final IconProvider mIconProvider;
private final SurfaceSession mSurfaceSession;
private Drawable mIcon;
private ImageView mResizingIconView;
private SurfaceControlViewHost mViewHost;
private SurfaceControl mHostLeash;
private SurfaceControl mIconLeash;
private SurfaceControl mBackgroundLeash;
private SurfaceControl mGapBackgroundLeash;
private SurfaceControl mScreenshot;
private boolean mShown;
private boolean mIsResizing;
private final Rect mOldBounds = new Rect();
private final Rect mResizingBounds = new Rect();
private final Rect mTempRect = new Rect();
private ValueAnimator mFadeAnimator;
private ValueAnimator mScreenshotAnimator;
private int mIconSize;
private int mOffsetX;
private int mOffsetY;
private int mRunningAnimationCount = 0;
public SplitDecorManager(Configuration configuration, IconProvider iconProvider,
SurfaceSession surfaceSession) {
super(configuration, null /* rootSurface */, null /* hostInputToken */);
mIconProvider = iconProvider;
mSurfaceSession = surfaceSession;
}
@Override
protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
// Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
.setContainerLayer()
.setName(TAG)
.setHidden(true)
.setParent(mHostLeash)
.setCallsite("SplitDecorManager#attachToParentSurface");
mIconLeash = builder.build();
return mIconLeash;
}
/** Inflates split decor surface on the root surface. */
public void inflate(Context context, SurfaceControl rootLeash, Rect rootBounds) {
if (mIconLeash != null && mViewHost != null) {
return;
}
context = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY,
null /* options */);
mHostLeash = rootLeash;
mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), this,
"SplitDecorManager");
mIconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size);
final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(context)
.inflate(R.layout.split_decor, null);
mResizingIconView = rootLayout.findViewById(R.id.split_resizing_icon);
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY,
FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
lp.width = rootBounds.width();
lp.height = rootBounds.height();
lp.token = new Binder();
lp.setTitle(TAG);
lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
// TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports
// TRUSTED_OVERLAY for windowless window without input channel.
mViewHost.setView(rootLayout, lp);
}
/** Releases the surfaces for split decor. */
public void release(SurfaceControl.Transaction t) {
if (mFadeAnimator != null) {
if (mFadeAnimator.isRunning()) {
mFadeAnimator.cancel();
}
mFadeAnimator = null;
}
if (mScreenshotAnimator != null) {
if (mScreenshotAnimator.isRunning()) {
mScreenshotAnimator.cancel();
}
mScreenshotAnimator = null;
}
if (mViewHost != null) {
mViewHost.release();
mViewHost = null;
}
if (mIconLeash != null) {
t.remove(mIconLeash);
mIconLeash = null;
}
if (mBackgroundLeash != null) {
t.remove(mBackgroundLeash);
mBackgroundLeash = null;
}
if (mGapBackgroundLeash != null) {
t.remove(mGapBackgroundLeash);
mGapBackgroundLeash = null;
}
if (mScreenshot != null) {
t.remove(mScreenshot);
mScreenshot = null;
}
mHostLeash = null;
mIcon = null;
mResizingIconView = null;
mIsResizing = false;
mShown = false;
mOldBounds.setEmpty();
mResizingBounds.setEmpty();
}
/** Showing resizing hint. */
public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds,
Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY,
boolean immediately) {
if (mResizingIconView == null) {
return;
}
if (!mIsResizing) {
mIsResizing = true;
mOldBounds.set(newBounds);
}
mResizingBounds.set(newBounds);
mOffsetX = offsetX;
mOffsetY = offsetY;
final boolean show =
newBounds.width() > mOldBounds.width() || newBounds.height() > mOldBounds.height();
final boolean update = show != mShown;
if (update && mFadeAnimator != null && mFadeAnimator.isRunning()) {
// If we need to animate and animator still running, cancel it before we ensure both
// background and icon surfaces are non null for next animation.
mFadeAnimator.cancel();
}
if (mBackgroundLeash == null) {
mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession);
t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask))
.setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
}
if (mGapBackgroundLeash == null && !immediately) {
final boolean isLandscape = newBounds.height() == sideBounds.height();
final int left = isLandscape ? mOldBounds.width() : 0;
final int top = isLandscape ? 0 : mOldBounds.height();
mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession);
// Fill up another side bounds area.
t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask))
.setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2)
.setPosition(mGapBackgroundLeash, left, top)
.setWindowCrop(mGapBackgroundLeash, sideBounds.width(), sideBounds.height());
}
if (mIcon == null && resizingTask.topActivityInfo != null) {
mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo);
mResizingIconView.setImageDrawable(mIcon);
mResizingIconView.setVisibility(View.VISIBLE);
WindowManager.LayoutParams lp =
(WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
lp.width = mIconSize;
lp.height = mIconSize;
mViewHost.relayout(lp);
t.setLayer(mIconLeash, Integer.MAX_VALUE);
}
t.setPosition(mIconLeash,
newBounds.width() / 2 - mIconSize / 2,
newBounds.height() / 2 - mIconSize / 2);
if (update) {
if (immediately) {
t.setVisibility(mBackgroundLeash, show);
t.setVisibility(mIconLeash, show);
} else {
startFadeAnimation(show, false, null);
}
mShown = show;
}
}
/** Stops showing resizing hint. */
public void onResized(SurfaceControl.Transaction t, Consumer<Boolean> animFinishedCallback) {
if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
mScreenshotAnimator.cancel();
}
if (mScreenshot != null) {
t.setPosition(mScreenshot, mOffsetX, mOffsetY);
final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
mScreenshotAnimator = ValueAnimator.ofFloat(1, 0);
mScreenshotAnimator.setDuration(FADE_DURATION);
mScreenshotAnimator.addUpdateListener(valueAnimator -> {
final float progress = (float) valueAnimator.getAnimatedValue();
animT.setAlpha(mScreenshot, progress);
animT.apply();
});
mScreenshotAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mRunningAnimationCount++;
}
@Override
public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) {
mRunningAnimationCount--;
animT.remove(mScreenshot);
animT.apply();
animT.close();
mScreenshot = null;
if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
animFinishedCallback.accept(true);
}
}
});
mScreenshotAnimator.start();
}
if (mResizingIconView == null) {
return;
}
mIsResizing = false;
mOffsetX = 0;
mOffsetY = 0;
mOldBounds.setEmpty();
mResizingBounds.setEmpty();
if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
if (!mShown) {
// If fade-out animation is running, just add release callback to it.
SurfaceControl.Transaction finishT = new SurfaceControl.Transaction();
mFadeAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
releaseDecor(finishT);
finishT.apply();
finishT.close();
}
});
return;
}
}
if (mShown) {
fadeOutDecor(()-> animFinishedCallback.accept(true));
} else {
// Decor surface is hidden so release it directly.
releaseDecor(t);
if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
animFinishedCallback.accept(false);
}
}
}
/** Screenshot host leash and attach on it if meet some conditions */
public void screenshotIfNeeded(SurfaceControl.Transaction t) {
if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) {
if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
mScreenshotAnimator.cancel();
} else if (mScreenshot != null) {
t.remove(mScreenshot);
}
mTempRect.set(mOldBounds);
mTempRect.offsetTo(0, 0);
mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect,
Integer.MAX_VALUE - 1);
}
}
/** Set screenshot and attach on host leash it if meet some conditions */
public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) {
if (screenshot == null || !screenshot.isValid()) return;
if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) {
if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
mScreenshotAnimator.cancel();
} else if (mScreenshot != null) {
t.remove(mScreenshot);
}
mScreenshot = screenshot;
t.reparent(screenshot, mHostLeash);
t.setLayer(screenshot, Integer.MAX_VALUE - 1);
}
}
/** Fade-out decor surface with animation end callback, if decor is hidden, run the callback
* directly. */
public void fadeOutDecor(Runnable finishedCallback) {
if (mShown) {
// If previous animation is running, just cancel it.
if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
mFadeAnimator.cancel();
}
startFadeAnimation(false /* show */, true, finishedCallback);
mShown = false;
} else {
if (finishedCallback != null) finishedCallback.run();
}
}
private void startFadeAnimation(boolean show, boolean releaseSurface,
Runnable finishedCallback) {
final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
mFadeAnimator = ValueAnimator.ofFloat(0f, 1f);
mFadeAnimator.setDuration(FADE_DURATION);
mFadeAnimator.addUpdateListener(valueAnimator-> {
final float progress = (float) valueAnimator.getAnimatedValue();
if (mBackgroundLeash != null) {
animT.setAlpha(mBackgroundLeash, show ? progress : 1 - progress);
}
if (mIconLeash != null) {
animT.setAlpha(mIconLeash, show ? progress : 1 - progress);
}
animT.apply();
});
mFadeAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(@NonNull Animator animation) {
mRunningAnimationCount++;
if (show) {
animT.show(mBackgroundLeash).show(mIconLeash);
}
if (mGapBackgroundLeash != null) {
animT.setVisibility(mGapBackgroundLeash, show);
}
animT.apply();
}
@Override
public void onAnimationEnd(@NonNull Animator animation) {
mRunningAnimationCount--;
if (!show) {
if (mBackgroundLeash != null) {
animT.hide(mBackgroundLeash);
}
if (mIconLeash != null) {
animT.hide(mIconLeash);
}
}
if (releaseSurface) {
releaseDecor(animT);
}
animT.apply();
animT.close();
if (mRunningAnimationCount == 0 && finishedCallback != null) {
finishedCallback.run();
}
}
});
mFadeAnimator.start();
}
/** Release or hide decor hint. */
private void releaseDecor(SurfaceControl.Transaction t) {
if (mBackgroundLeash != null) {
t.remove(mBackgroundLeash);
mBackgroundLeash = null;
}
if (mGapBackgroundLeash != null) {
t.remove(mGapBackgroundLeash);
mGapBackgroundLeash = null;
}
if (mIcon != null) {
mResizingIconView.setVisibility(View.GONE);
mResizingIconView.setImageDrawable(null);
t.hide(mIconLeash);
mIcon = null;
}
}
private static float[] getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) {
final int taskBgColor = taskInfo.taskDescription.getBackgroundColor();
return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).getComponents();
}
}