| /* |
| * 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 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.SurfaceUtils; |
| |
| /** |
| * 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 long FADE_DURATION = 133; |
| |
| 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 boolean mShown; |
| private boolean mIsResizing; |
| private Rect mBounds = new Rect(); |
| private ValueAnimator mFadeAnimator; |
| |
| private int mIconSize; |
| |
| public SplitDecorManager(Configuration configuration, IconProvider iconProvider, |
| SurfaceSession surfaceSession) { |
| super(configuration, null /* rootSurface */, null /* hostInputToken */); |
| mIconProvider = iconProvider; |
| mSurfaceSession = surfaceSession; |
| } |
| |
| @Override |
| protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { |
| // 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(); |
| b.setParent(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); |
| |
| 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 && mFadeAnimator.isRunning()) { |
| mFadeAnimator.cancel(); |
| } |
| if (mViewHost != null) { |
| mViewHost.release(); |
| mViewHost = null; |
| } |
| if (mIconLeash != null) { |
| t.remove(mIconLeash); |
| mIconLeash = null; |
| } |
| if (mBackgroundLeash != null) { |
| t.remove(mBackgroundLeash); |
| mBackgroundLeash = null; |
| } |
| mHostLeash = null; |
| mIcon = null; |
| mResizingIconView = null; |
| mIsResizing = false; |
| mShown = false; |
| } |
| |
| /** Showing resizing hint. */ |
| public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, |
| SurfaceControl.Transaction t) { |
| if (mResizingIconView == null) { |
| return; |
| } |
| |
| if (!mIsResizing) { |
| mIsResizing = true; |
| mBounds.set(newBounds); |
| } |
| |
| final boolean show = |
| newBounds.width() > mBounds.width() || newBounds.height() > mBounds.height(); |
| final boolean animate = show != mShown; |
| if (animate && 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 (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 (animate) { |
| startFadeAnimation(show, false /* isResized */); |
| mShown = show; |
| } |
| } |
| |
| /** Stops showing resizing hint. */ |
| public void onResized(SurfaceControl.Transaction t) { |
| if (mResizingIconView == null) { |
| return; |
| } |
| |
| mIsResizing = false; |
| 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 fade-in animation is running, cancel it and re-run fade-out one. |
| mFadeAnimator.cancel(); |
| } |
| if (mShown) { |
| startFadeAnimation(false /* show */, true /* isResized */); |
| } else { |
| // Decor surface is hidden so release it directly. |
| releaseDecor(t); |
| } |
| } |
| |
| private void startFadeAnimation(boolean show, boolean isResized) { |
| 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) { |
| if (show) { |
| animT.show(mBackgroundLeash).show(mIconLeash).apply(); |
| } |
| } |
| |
| @Override |
| public void onAnimationEnd(@NonNull Animator animation) { |
| if (!show) { |
| if (mBackgroundLeash != null) { |
| animT.hide(mBackgroundLeash); |
| } |
| if (mIconLeash != null) { |
| animT.hide(mIconLeash); |
| } |
| } |
| if (isResized) { |
| releaseDecor(animT); |
| } |
| animT.apply(); |
| animT.close(); |
| } |
| }); |
| mFadeAnimator.start(); |
| } |
| |
| /** Release or hide decor hint. */ |
| private void releaseDecor(SurfaceControl.Transaction t) { |
| if (mBackgroundLeash != null) { |
| t.remove(mBackgroundLeash); |
| mBackgroundLeash = 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(); |
| } |
| } |