diff options
| author | 2024-01-29 23:25:07 +0000 | |
|---|---|---|
| committer | 2024-01-29 23:25:07 +0000 | |
| commit | 4c8a8d46e8186dcfa8029ef76ddcd6548c2a4662 (patch) | |
| tree | 5f94af99e6543ab9cd23da2f02df47237469894a | |
| parent | 936b87d4d5f224e58387457d5843d6227090e03f (diff) | |
| parent | 6071c2d09dc01c304561c631efbf20a886c71bac (diff) | |
Merge "Refactor PiP2 to include PiP menu related classes" into main
15 files changed, 1678 insertions, 13 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java index 0775f5279e31..2f1189a8a984 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.common.pip; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; @@ -33,12 +33,13 @@ import android.view.SurfaceControl; import android.view.WindowManager; import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; import java.util.List; /** - * Interface to allow {@link com.android.wm.shell.pip.PipTaskOrganizer} to call into - * PiP menu when certain events happen (task appear/vanish, PiP move, etc.) + * Interface to interact with PiP menu when certain events happen + * (task appear/vanish, PiP move, etc.). */ public interface PipMenuController { @@ -52,15 +53,15 @@ public interface PipMenuController { float ALPHA_NO_CHANGE = -1f; /** - * Called when - * {@link PipTaskOrganizer#onTaskAppeared(RunningTaskInfo, SurfaceControl)} + * Called when out implementation of + * {@link ShellTaskOrganizer.TaskListener#onTaskAppeared(RunningTaskInfo, SurfaceControl)} * is called. */ void attach(SurfaceControl leash); /** - * Called when - * {@link PipTaskOrganizer#onTaskVanished(RunningTaskInfo)} is called. + * Called when our implementation of + * {@link ShellTaskOrganizer.TaskListener#onTaskVanished(RunningTaskInfo)} is called. */ void detach(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 7b98fa6523cb..8eecf1c58db0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -18,18 +18,23 @@ package com.android.wm.shell.dagger.pip; import android.annotation.NonNull; import android.content.Context; +import android.os.Handler; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.common.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; +import com.android.wm.shell.pip2.phone.PhonePipMenuController; import com.android.wm.shell.pip2.phone.PipController; import com.android.wm.shell.pip2.phone.PipScheduler; import com.android.wm.shell.pip2.phone.PipTransition; @@ -86,4 +91,16 @@ public abstract class Pip2Module { @ShellMainThread ShellExecutor mainExecutor) { return new PipScheduler(context, pipBoundsState, mainExecutor); } + + @WMSingleton + @Provides + static PhonePipMenuController providePipPhoneMenuController(Context context, + PipBoundsState pipBoundsState, PipMediaController pipMediaController, + SystemWindows systemWindows, + PipUiEventLogger pipUiEventLogger, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler) { + return new PhonePipMenuController(context, pipBoundsState, pipMediaController, + systemWindows, pipUiEventLogger, mainExecutor, mainHandler); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 07b8f11458be..52a06e0bbe7f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -86,6 +86,7 @@ import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.phone.PipMotionHelper; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 8e375a9ef5ee..e7392662bdf1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -68,6 +68,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 0e7073688ec4..d1fd207c4a66 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -40,6 +40,7 @@ import androidx.annotation.NonNull; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index 760652625f9e..d8e8b587004a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -41,8 +41,8 @@ import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipMediaController; import com.android.wm.shell.common.pip.PipMediaController.ActionListener; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUiEventLogger; -import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index c6803f7beebd..843c84a06f8c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -40,7 +40,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.SystemWindows; -import com.android.wm.shell.pip.PipMenuController; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.List; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java index 21223c9ac362..cac63eb2a2ad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java @@ -28,10 +28,10 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipTaskOrganizer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java index 571c839adf11..d16a692dbd0a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java @@ -25,10 +25,10 @@ import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.transitTypeToString; +import static com.android.wm.shell.common.pip.PipMenuController.ALPHA_NO_CHANGE; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; -import static com.android.wm.shell.pip.PipMenuController.ALPHA_NO_CHANGE; import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP; import static com.android.wm.shell.pip.PipTransitionState.ENTERING_PIP; import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java new file mode 100644 index 000000000000..24077a35d41c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2020 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.pip2; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.Choreographer; +import android.view.SurfaceControl; + +import com.android.wm.shell.R; +import com.android.wm.shell.transition.Transitions; + +/** + * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition. + */ +public class PipSurfaceTransactionHelper { + /** for {@link #scale(SurfaceControl.Transaction, SurfaceControl, Rect, Rect)} operation */ + private final Matrix mTmpTransform = new Matrix(); + private final float[] mTmpFloat9 = new float[9]; + private final RectF mTmpSourceRectF = new RectF(); + private final RectF mTmpDestinationRectF = new RectF(); + private final Rect mTmpDestinationRect = new Rect(); + + private int mCornerRadius; + private int mShadowRadius; + + public PipSurfaceTransactionHelper(Context context) { + onDensityOrFontScaleChanged(context); + } + + /** + * Called when display size or font size of settings changed + * + * @param context the current context + */ + public void onDensityOrFontScaleChanged(Context context) { + mCornerRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius); + mShadowRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius); + } + + /** + * Operates the alpha on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash, + float alpha) { + tx.setAlpha(leash, alpha); + return this; + } + + /** + * Operates the crop (and position) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect destinationBounds) { + tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()) + .setPosition(leash, destinationBounds.left, destinationBounds.top); + return this; + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, Rect destinationBounds) { + mTmpDestinationRectF.set(destinationBounds); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, RectF destinationBounds) { + return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, Rect destinationBounds, float degrees) { + mTmpDestinationRectF.set(destinationBounds); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation. + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, RectF destinationBounds, float degrees) { + mTmpSourceRectF.set(sourceBounds); + // We want the matrix to position the surface relative to the screen coordinates so offset + // the source to 0,0 + mTmpSourceRectF.offsetTo(0, 0); + mTmpDestinationRectF.set(destinationBounds); + mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); + mTmpTransform.postRotate(degrees, + mTmpDestinationRectF.centerX(), mTmpDestinationRectF.centerY()); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9); + return this; + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, + SurfaceControl leash, Rect sourceRectHint, + Rect sourceBounds, Rect destinationBounds, Rect insets, + boolean isInPipDirection, float fraction) { + mTmpDestinationRect.set(sourceBounds); + // Similar to {@link #scale}, we want to position the surface relative to the screen + // coordinates so offset the bounds to 0,0 + mTmpDestinationRect.offsetTo(0, 0); + mTmpDestinationRect.inset(insets); + // Scale to the bounds no smaller than the destination and offset such that the top/left + // of the scaled inset source rect aligns with the top/left of the destination bounds + final float scale; + if (isInPipDirection + && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) { + // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only. + final float endScale = sourceBounds.width() <= sourceBounds.height() + ? (float) destinationBounds.width() / sourceRectHint.width() + : (float) destinationBounds.height() / sourceRectHint.height(); + final float startScale = sourceBounds.width() <= sourceBounds.height() + ? (float) destinationBounds.width() / sourceBounds.width() + : (float) destinationBounds.height() / sourceBounds.height(); + scale = (1 - fraction) * startScale + fraction * endScale; + } else { + scale = Math.max((float) destinationBounds.width() / sourceBounds.width(), + (float) destinationBounds.height() / sourceBounds.height()); + } + final float left = destinationBounds.left - insets.left * scale; + final float top = destinationBounds.top - insets.top * scale; + mTmpTransform.setScale(scale, scale); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9) + .setCrop(leash, mTmpDestinationRect) + .setPosition(leash, left, top); + return this; + } + + /** + * Operates the rotation according to the given degrees and scale (setMatrix) according to the + * source bounds and rotated destination bounds. The crop will be the unscaled source bounds. + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper rotateAndScaleWithCrop(SurfaceControl.Transaction tx, + SurfaceControl leash, Rect sourceBounds, Rect destinationBounds, Rect insets, + float degrees, float positionX, float positionY, boolean isExpanding, + boolean clockwise) { + mTmpDestinationRect.set(sourceBounds); + mTmpDestinationRect.inset(insets); + final int srcW = mTmpDestinationRect.width(); + final int srcH = mTmpDestinationRect.height(); + final int destW = destinationBounds.width(); + final int destH = destinationBounds.height(); + // Scale by the short side so there won't be empty area if the aspect ratio of source and + // destination are different. + final float scale = srcW <= srcH ? (float) destW / srcW : (float) destH / srcH; + final Rect crop = mTmpDestinationRect; + crop.set(0, 0, Transitions.SHELL_TRANSITIONS_ROTATION ? destH + : destW, Transitions.SHELL_TRANSITIONS_ROTATION ? destW : destH); + // Inverse scale for crop to fit in screen coordinates. + crop.scale(1 / scale); + crop.offset(insets.left, insets.top); + if (isExpanding) { + // Expand bounds (shrink insets) in source orientation. + positionX -= insets.left * scale; + positionY -= insets.top * scale; + } else { + // Shrink bounds (expand insets) in destination orientation. + if (clockwise) { + positionX -= insets.top * scale; + positionY += insets.left * scale; + } else { + positionX += insets.top * scale; + positionY -= insets.left * scale; + } + } + mTmpTransform.setScale(scale, scale); + mTmpTransform.postRotate(degrees); + mTmpTransform.postTranslate(positionX, positionY); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9).setCrop(leash, crop); + return this; + } + + /** + * Resets the scale (setMatrix) on a given transaction and leash if there's any + * + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper resetScale(SurfaceControl.Transaction tx, + SurfaceControl leash, + Rect destinationBounds) { + tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, mTmpFloat9) + .setPosition(leash, destinationBounds.left, destinationBounds.top); + return this; + } + + /** + * Operates the round corner radius on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash, + boolean applyCornerRadius) { + tx.setCornerRadius(leash, applyCornerRadius ? mCornerRadius : 0); + return this; + } + + /** + * Operates the round corner radius on a given transaction and leash, scaled by bounds + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect fromBounds, Rect toBounds) { + final float scale = (float) (Math.hypot(fromBounds.width(), fromBounds.height()) + / Math.hypot(toBounds.width(), toBounds.height())); + tx.setCornerRadius(leash, mCornerRadius * scale); + return this; + } + + /** + * Operates the shadow radius on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper shadow(SurfaceControl.Transaction tx, SurfaceControl leash, + boolean applyShadowRadius) { + tx.setShadowRadius(leash, applyShadowRadius ? mShadowRadius : 0); + return this; + } + + /** + * Interface to standardize {@link SurfaceControl.Transaction} generation across PiP. + */ + public interface SurfaceControlTransactionFactory { + /** + * @return a new transaction to operate on. + */ + SurfaceControl.Transaction getTransaction(); + } + + /** + * Implementation of {@link SurfaceControlTransactionFactory} that returns + * {@link SurfaceControl.Transaction} with VsyncId being set. + */ + public static class VsyncSurfaceControlTransactionFactory + implements SurfaceControlTransactionFactory { + @Override + public SurfaceControl.Transaction getTransaction() { + final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + tx.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + return tx; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java new file mode 100644 index 000000000000..2478252213a7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java @@ -0,0 +1,608 @@ +/* + * Copyright (C) 2020 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.pip2.phone; + +import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.RemoteAction; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Debug; +import android.os.Handler; +import android.os.RemoteException; +import android.util.Size; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.WindowManagerGlobal; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipMediaController.ActionListener; +import com.android.wm.shell.common.pip.PipMenuController; +import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Manages the PiP menu view which can show menu options or a scrim. + * + * The current media session provides actions whenever there are no valid actions provided by the + * current PiP activity. Otherwise, those actions always take precedence. + */ +public class PhonePipMenuController implements PipMenuController { + + private static final String TAG = "PhonePipMenuController"; + private static final boolean DEBUG = false; + + public static final int MENU_STATE_NONE = 0; + public static final int MENU_STATE_FULL = 1; + + /** + * A listener interface to receive notification on changes in PIP. + */ + public interface Listener { + /** + * Called when the PIP menu visibility change has started. + * + * @param menuState the new, about-to-change state of the menu + * @param resize whether or not to resize the PiP with the state change + */ + void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback); + + /** + * Called when the PIP menu state has finished changing/animating. + * + * @param menuState the new state of the menu. + */ + void onPipMenuStateChangeFinish(int menuState); + + /** + * Called when the PIP requested to be expanded. + */ + void onPipExpand(); + + /** + * Called when the PIP requested to be dismissed. + */ + void onPipDismiss(); + + /** + * Called when the PIP requested to show the menu. + */ + void onPipShowMenu(); + + /** + * Called when the PIP requested to enter Split. + */ + void onEnterSplit(); + } + + private final Matrix mMoveTransform = new Matrix(); + private final Rect mTmpSourceBounds = new Rect(); + private final RectF mTmpSourceRectF = new RectF(); + private final RectF mTmpDestinationRectF = new RectF(); + private final Context mContext; + private final PipBoundsState mPipBoundsState; + private final PipMediaController mMediaController; + private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; + + private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + private final float[] mTmpTransform = new float[9]; + + private final ArrayList<Listener> mListeners = new ArrayList<>(); + private final SystemWindows mSystemWindows; + private final PipUiEventLogger mPipUiEventLogger; + + private List<RemoteAction> mAppActions; + private RemoteAction mCloseAction; + private List<RemoteAction> mMediaActions; + + private int mMenuState; + + private PipMenuView mPipMenuView; + + private SurfaceControl mLeash; + + private ActionListener mMediaActionListener = new ActionListener() { + @Override + public void onMediaActionsChanged(List<RemoteAction> mediaActions) { + mMediaActions = new ArrayList<>(mediaActions); + updateMenuActions(); + } + }; + + public PhonePipMenuController(Context context, PipBoundsState pipBoundsState, + PipMediaController mediaController, SystemWindows systemWindows, + PipUiEventLogger pipUiEventLogger, + ShellExecutor mainExecutor, Handler mainHandler) { + mContext = context; + mPipBoundsState = pipBoundsState; + mMediaController = mediaController; + mSystemWindows = systemWindows; + mMainExecutor = mainExecutor; + mMainHandler = mainHandler; + mPipUiEventLogger = pipUiEventLogger; + + mSurfaceControlTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); + } + + public boolean isMenuVisible() { + return mPipMenuView != null && mMenuState != MENU_STATE_NONE; + } + + /** + * Attach the menu when the PiP task first appears. + */ + @Override + public void attach(SurfaceControl leash) { + mLeash = leash; + attachPipMenuView(); + } + + /** + * Detach the menu when the PiP task is gone. + */ + @Override + public void detach() { + hideMenu(); + detachPipMenuView(); + mLeash = null; + } + + void attachPipMenuView() { + // In case detach was not called (e.g. PIP unexpectedly closed) + if (mPipMenuView != null) { + detachPipMenuView(); + } + mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler, + mPipUiEventLogger); + mPipMenuView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + v.getViewRootImpl().addSurfaceChangedCallback( + new ViewRootImpl.SurfaceChangedCallback() { + @Override + public void surfaceCreated(SurfaceControl.Transaction t) { + final SurfaceControl sc = getSurfaceControl(); + if (sc != null) { + t.reparent(sc, mLeash); + // make menu on top of the surface + t.setLayer(sc, Integer.MAX_VALUE); + } + } + + @Override + public void surfaceReplaced(SurfaceControl.Transaction t) { + } + + @Override + public void surfaceDestroyed() { + } + }); + } + + @Override + public void onViewDetachedFromWindow(View v) { + } + }); + + mSystemWindows.addView(mPipMenuView, + getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), + 0, SHELL_ROOT_LAYER_PIP); + setShellRootAccessibilityWindow(); + + // Make sure the initial actions are set + updateMenuActions(); + } + + private void detachPipMenuView() { + if (mPipMenuView == null) { + return; + } + + mSystemWindows.removeView(mPipMenuView); + mPipMenuView = null; + } + + /** + * Updates the layout parameters of the menu. + * @param destinationBounds New Menu bounds. + */ + @Override + public void updateMenuBounds(Rect destinationBounds) { + mSystemWindows.updateViewLayout(mPipMenuView, + getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, destinationBounds.width(), + destinationBounds.height())); + updateMenuLayout(destinationBounds); + } + + @Override + public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (mPipMenuView != null) { + mPipMenuView.onFocusTaskChanged(taskInfo); + } + } + + /** + * Tries to grab a surface control from {@link PipMenuView}. If this isn't available for some + * reason (ie. the window isn't ready yet, thus {@link ViewRootImpl} is + * {@code null}), it will get the leash that the WindowlessWM has assigned to it. + */ + public SurfaceControl getSurfaceControl() { + return mSystemWindows.getViewSurface(mPipMenuView); + } + + /** + * Adds a new menu activity listener. + */ + public void addListener(Listener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + + @Nullable + Size getEstimatedMinMenuSize() { + return mPipMenuView == null ? null : mPipMenuView.getEstimatedMinMenuSize(); + } + + /** + * When other components requests the menu controller directly to show the menu, we must + * first fire off the request to the other listeners who will then propagate the call + * back to the controller with the right parameters. + */ + @Override + public void showMenu() { + mListeners.forEach(Listener::onPipShowMenu); + } + + /** + * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu + * upon PiP window transition is finished. + */ + public void showMenuWithPossibleDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean willResizeMenu, boolean showResizeHandle) { + if (willResizeMenu) { + // hide all visible controls including close button and etc. first, this is to ensure + // menu is totally invisible during the transition to eliminate unpleasant artifacts + fadeOutMenu(); + } + showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, + willResizeMenu /* withDelay=willResizeMenu here */, showResizeHandle); + } + + /** + * Shows the menu activity immediately. + */ + public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean willResizeMenu, boolean showResizeHandle) { + showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, + false /* withDelay */, showResizeHandle); + } + + private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showMenu() state=%s" + + " isMenuVisible=%s" + + " allowMenuTimeout=%s" + + " willResizeMenu=%s" + + " withDelay=%s" + + " showResizeHandle=%s" + + " callers=\n%s", TAG, menuState, isMenuVisible(), allowMenuTimeout, + willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, " ")); + } + + if (!checkPipMenuState()) { + return; + } + + // Sync the menu bounds before showing it in case it is out of sync. + movePipMenu(null /* pipLeash */, null /* transaction */, stackBounds, + PipMenuController.ALPHA_NO_CHANGE); + updateMenuBounds(stackBounds); + + mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay, + showResizeHandle); + } + + /** + * Move the PiP menu, which does a translation and possibly a scale transformation. + */ + @Override + public void movePipMenu(@Nullable SurfaceControl pipLeash, + @Nullable SurfaceControl.Transaction t, + Rect destinationBounds, float alpha) { + if (destinationBounds.isEmpty()) { + return; + } + + if (!checkPipMenuState()) { + return; + } + + // TODO(b/286307861) transaction should be applied outside of PiP menu controller + if (pipLeash != null && t != null) { + t.apply(); + } + } + + /** + * Does an immediate window crop of the PiP menu. + */ + @Override + public void resizePipMenu(@Nullable SurfaceControl pipLeash, + @Nullable SurfaceControl.Transaction t, + Rect destinationBounds) { + if (destinationBounds.isEmpty()) { + return; + } + + if (!checkPipMenuState()) { + return; + } + + // TODO(b/286307861) transaction should be applied outside of PiP menu controller + if (pipLeash != null && t != null) { + t.apply(); + } + } + + private boolean checkPipMenuState() { + if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Not going to move PiP, either menu or its parent is not created.", TAG); + return false; + } + + return true; + } + + /** + * Pokes the menu, indicating that the user is interacting with it. + */ + public void pokeMenu() { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: pokeMenu() isMenuVisible=%b", TAG, isMenuVisible); + } + if (isMenuVisible) { + mPipMenuView.pokeMenu(); + } + } + + private void fadeOutMenu() { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: fadeOutMenu() isMenuVisible=%b", TAG, isMenuVisible); + } + if (isMenuVisible) { + mPipMenuView.fadeOutMenu(); + } + } + + /** + * Hides the menu view. + */ + public void hideMenu() { + final boolean isMenuVisible = isMenuVisible(); + if (isMenuVisible) { + mPipMenuView.hideMenu(); + } + } + + /** + * Hides the menu view. + * + * @param animationType the animation type to use upon hiding the menu + * @param resize whether or not to resize the PiP with the state change + */ + public void hideMenu(@PipMenuView.AnimationType int animationType, boolean resize) { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: hideMenu() state=%s" + + " isMenuVisible=%s" + + " animationType=%s" + + " resize=%s" + + " callers=\n%s", TAG, mMenuState, isMenuVisible, + animationType, resize, + Debug.getCallers(5, " ")); + } + if (isMenuVisible) { + mPipMenuView.hideMenu(resize, animationType); + } + } + + /** + * Hides the menu activity. + */ + public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) { + if (isMenuVisible()) { + // If the menu is visible in either the closed or full state, then hide the menu and + // trigger the animation trigger afterwards + if (onStartCallback != null) { + onStartCallback.run(); + } + mPipMenuView.hideMenu(onEndCallback); + } + } + + /** + * Sets the menu actions to the actions provided by the current PiP menu. + */ + @Override + public void setAppActions(List<RemoteAction> appActions, + RemoteAction closeAction) { + mAppActions = appActions; + mCloseAction = closeAction; + updateMenuActions(); + } + + void onPipExpand() { + mListeners.forEach(Listener::onPipExpand); + } + + void onPipDismiss() { + mListeners.forEach(Listener::onPipDismiss); + } + + void onEnterSplit() { + mListeners.forEach(Listener::onEnterSplit); + } + + /** + * @return the best set of actions to show in the PiP menu. + */ + private List<RemoteAction> resolveMenuActions() { + if (isValidActions(mAppActions)) { + return mAppActions; + } + return mMediaActions; + } + + /** + * Updates the PiP menu with the best set of actions provided. + */ + private void updateMenuActions() { + if (mPipMenuView != null) { + mPipMenuView.setActions(mPipBoundsState.getBounds(), + resolveMenuActions(), mCloseAction); + } + } + + /** + * Returns whether the set of actions are valid. + */ + private static boolean isValidActions(List<?> actions) { + return actions != null && actions.size() > 0; + } + + /** + * Handles changes in menu visibility. + */ + void onMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onMenuStateChangeStart() mMenuState=%s" + + " menuState=%s resize=%s" + + " callers=\n%s", TAG, mMenuState, menuState, resize, + Debug.getCallers(5, " ")); + } + + if (menuState != mMenuState) { + mListeners.forEach(l -> l.onPipMenuStateChangeStart(menuState, resize, callback)); + if (menuState == MENU_STATE_FULL) { + // Once visible, start listening for media action changes. This call will trigger + // the menu actions to be updated again. + mMediaController.addActionListener(mMediaActionListener); + } else { + // Once hidden, stop listening for media action changes. This call will trigger + // the menu actions to be updated again. + mMediaController.removeActionListener(mMediaActionListener); + } + + try { + WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, + mSystemWindows.getFocusGrantToken(mPipMenuView), + menuState != MENU_STATE_NONE /* grantFocus */); + } catch (RemoteException e) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Unable to update focus as menu appears/disappears, %s", TAG, e); + } + } + } + + void onMenuStateChangeFinish(int menuState) { + if (menuState != mMenuState) { + mListeners.forEach(l -> l.onPipMenuStateChangeFinish(menuState)); + } + mMenuState = menuState; + setShellRootAccessibilityWindow(); + } + + private void setShellRootAccessibilityWindow() { + switch (mMenuState) { + case MENU_STATE_NONE: + mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, null); + break; + default: + mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, + mPipMenuView); + break; + } + } + + /** + * Handles a pointer event sent from pip input consumer. + */ + void handlePointerEvent(MotionEvent ev) { + if (mPipMenuView == null) { + return; + } + + if (ev.isTouchEvent()) { + mPipMenuView.dispatchTouchEvent(ev); + } else { + mPipMenuView.dispatchGenericMotionEvent(ev); + } + } + + /** + * Tell the PIP Menu to recalculate its layout given its current position on the display. + */ + public void updateMenuLayout(Rect bounds) { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateMenuLayout() state=%s" + + " isMenuVisible=%s" + + " callers=\n%s", TAG, mMenuState, isMenuVisible, + Debug.getCallers(5, " ")); + } + if (isMenuVisible) { + mPipMenuView.updateMenuLayout(bounds); + } + } + + void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mMenuState=" + mMenuState); + pw.println(innerPrefix + "mPipMenuView=" + mPipMenuView); + pw.println(innerPrefix + "mListeners=" + mListeners.size()); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java new file mode 100644 index 000000000000..7252675dc52d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java @@ -0,0 +1,56 @@ +/* + * 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.pip2.phone; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.android.wm.shell.R; + +/** + * Container layout wraps single action image view drawn in PiP menu and can restrict the size of + * action image view (see pip_menu_action.xml). + */ +public class PipMenuActionView extends FrameLayout { + private ImageView mImageView; + private View mCustomCloseBackground; + + public PipMenuActionView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mImageView = findViewById(R.id.image); + mCustomCloseBackground = findViewById(R.id.custom_close_bg); + } + + /** pass through to internal {@link #mImageView} */ + public void setImageDrawable(Drawable drawable) { + mImageView.setImageDrawable(drawable); + } + + /** pass through to internal {@link #mCustomCloseBackground} */ + public void setCustomCloseBackgroundVisibility(@Visibility int visibility) { + mCustomCloseBackground.setVisibility(visibility); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java new file mode 100644 index 000000000000..b5e575ba33f2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 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.pip2.phone; + +import android.content.Context; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +/** + * Helper class to calculate and place the menu icons on the PIP Menu. + */ +public class PipMenuIconsAlgorithm { + + private static final String TAG = "PipMenuIconsAlgorithm"; + + protected ViewGroup mViewRoot; + protected ViewGroup mTopEndContainer; + protected View mDragHandle; + protected View mEnterSplitButton; + protected View mSettingsButton; + protected View mDismissButton; + + protected PipMenuIconsAlgorithm(Context context) { + } + + /** + * Bind the necessary views. + */ + public void bindViews(ViewGroup viewRoot, ViewGroup topEndContainer, View dragHandle, + View enterSplitButton, View settingsButton, View dismissButton) { + mViewRoot = viewRoot; + mTopEndContainer = topEndContainer; + mDragHandle = dragHandle; + mEnterSplitButton = enterSplitButton; + mSettingsButton = settingsButton; + mDismissButton = dismissButton; + } + + /** + * Updates the position of the drag handle based on where the PIP window is on the screen. + */ + public void onBoundsChanged(Rect bounds) { + // On phones, the menu icons are always static and will never move based on the PIP window + // position. No need to do anything here. + } + + /** + * Set the gravity on the given view. + */ + protected static void setLayoutGravity(View v, int gravity) { + if (v.getLayoutParams() instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v.getLayoutParams(); + params.gravity = gravity; + v.setLayoutParams(params); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java new file mode 100644 index 000000000000..a5b76c7df20b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java @@ -0,0 +1,630 @@ +/* + * Copyright (C) 2020 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.pip2.phone; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS; +import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS; +import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS; +import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; + +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.UserHandle; +import android.util.Pair; +import android.util.Size; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Translucent window that gets started on top of a task in PIP to allow the user to control it. + */ +public class PipMenuView extends FrameLayout { + + private static final String TAG = "PipMenuView"; + + private static final int ANIMATION_NONE_DURATION_MS = 0; + private static final int ANIMATION_HIDE_DURATION_MS = 125; + + /** No animation performed during menu hide. */ + public static final int ANIM_TYPE_NONE = 0; + /** Fade out the menu until it's invisible. Used when the PIP window remains visible. */ + public static final int ANIM_TYPE_HIDE = 1; + /** Fade out the menu in sync with the PIP window. */ + public static final int ANIM_TYPE_DISMISS = 2; + + @IntDef(prefix = { "ANIM_TYPE_" }, value = { + ANIM_TYPE_NONE, + ANIM_TYPE_HIDE, + ANIM_TYPE_DISMISS + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AnimationType {} + + private static final int INITIAL_DISMISS_DELAY = 3500; + private static final int POST_INTERACTION_DISMISS_DELAY = 2000; + private static final long MENU_SHOW_ON_EXPAND_START_DELAY = 30; + + private static final float MENU_BACKGROUND_ALPHA = 0.54f; + private static final float DISABLED_ACTION_ALPHA = 0.54f; + + private int mMenuState; + private boolean mAllowMenuTimeout = true; + private boolean mAllowTouches = true; + private int mDismissFadeOutDurationMs; + private final List<RemoteAction> mActions = new ArrayList<>(); + private RemoteAction mCloseAction; + + private AccessibilityManager mAccessibilityManager; + private Drawable mBackgroundDrawable; + private View mMenuContainer; + private LinearLayout mActionsGroup; + private int mBetweenActionPaddingLand; + + private AnimatorSet mMenuContainerAnimator; + private final PhonePipMenuController mController; + private final PipUiEventLogger mPipUiEventLogger; + + private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener = + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final float alpha = (float) animation.getAnimatedValue(); + mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA * alpha * 255)); + } + }; + + private ShellExecutor mMainExecutor; + private Handler mMainHandler; + + /** + * Whether the most recent showing of the menu caused a PIP resize, such as when PIP is too + * small and it is resized on menu show to fit the actions. + */ + private boolean mDidLastShowMenuResize; + private final Runnable mHideMenuRunnable = this::hideMenu; + + protected View mViewRoot; + protected View mSettingsButton; + protected View mDismissButton; + protected View mEnterSplitButton; + protected View mTopEndContainer; + protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm; + + // How long the shell will wait for the app to close the PiP if a custom action is set. + private final int mPipForceCloseDelay; + + public PipMenuView(Context context, PhonePipMenuController controller, + ShellExecutor mainExecutor, Handler mainHandler, PipUiEventLogger pipUiEventLogger) { + super(context, null, 0); + mContext = context; + mController = controller; + mMainExecutor = mainExecutor; + mMainHandler = mainHandler; + mPipUiEventLogger = pipUiEventLogger; + + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + inflate(context, R.layout.pip_menu, this); + + mPipForceCloseDelay = context.getResources().getInteger( + R.integer.config_pipForceCloseDelay); + + mBackgroundDrawable = mContext.getDrawable(R.drawable.pip_menu_background); + mBackgroundDrawable.setAlpha(0); + mViewRoot = findViewById(R.id.background); + mViewRoot.setBackground(mBackgroundDrawable); + mMenuContainer = findViewById(R.id.menu_container); + mMenuContainer.setAlpha(0); + mTopEndContainer = findViewById(R.id.top_end_container); + mSettingsButton = findViewById(R.id.settings); + mSettingsButton.setAlpha(0); + mSettingsButton.setOnClickListener((v) -> { + if (v.getAlpha() != 0) { + showSettings(); + } + }); + mDismissButton = findViewById(R.id.dismiss); + mDismissButton.setAlpha(0); + mDismissButton.setOnClickListener(v -> dismissPip()); + findViewById(R.id.expand_button).setOnClickListener(v -> { + if (mMenuContainer.getAlpha() != 0) { + expandPip(); + } + }); + + mEnterSplitButton = findViewById(R.id.enter_split); + mEnterSplitButton.setAlpha(0); + mEnterSplitButton.setOnClickListener(v -> { + if (mEnterSplitButton.getAlpha() != 0) { + enterSplit(); + } + }); + + // this disables the ripples + mEnterSplitButton.setEnabled(false); + + findViewById(R.id.resize_handle).setAlpha(0); + + mActionsGroup = findViewById(R.id.actions_group); + mBetweenActionPaddingLand = getResources().getDimensionPixelSize( + R.dimen.pip_between_action_padding_land); + mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext); + mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer, + findViewById(R.id.resize_handle), mEnterSplitButton, mSettingsButton, + mDismissButton); + mDismissFadeOutDurationMs = context.getResources() + .getInteger(R.integer.config_pipExitAnimationDuration); + + initAccessibility(); + } + + private void initAccessibility() { + this.setAccessibilityDelegate(new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + String label = getResources().getString(R.string.pip_menu_title); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, label)); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action == ACTION_CLICK && mMenuState != MENU_STATE_FULL) { + mController.showMenu(); + } + return super.performAccessibilityAction(host, action, args); + } + }); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ESCAPE) { + hideMenu(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public boolean shouldDelayChildPressedState() { + return true; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (!mAllowTouches) { + return false; + } + + if (mAllowMenuTimeout) { + repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); + } + + return super.dispatchTouchEvent(ev); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (mAllowMenuTimeout) { + repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); + } + + return super.dispatchGenericMotionEvent(event); + } + + void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {} + + void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) { + mAllowMenuTimeout = allowMenuTimeout; + mDidLastShowMenuResize = resizeMenuOnShow; + final boolean enableEnterSplit = + mContext.getResources().getBoolean(R.bool.config_pipEnableEnterSplitButton); + if (mMenuState != menuState) { + // Disallow touches if the menu needs to resize while showing, and we are transitioning + // to/from a full menu state. + boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow + && (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL); + mAllowTouches = !disallowTouchesUntilAnimationEnd; + cancelDelayedHide(); + if (mMenuContainerAnimator != null) { + mMenuContainerAnimator.cancel(); + } + mMenuContainerAnimator = new AnimatorSet(); + ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA, + mMenuContainer.getAlpha(), 1f); + menuAnim.addUpdateListener(mMenuBgUpdateListener); + ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, + mSettingsButton.getAlpha(), 1f); + ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, + mDismissButton.getAlpha(), 1f); + if (menuState == MENU_STATE_FULL) { + mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim); + } + mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN); + mMenuContainerAnimator.setDuration(ANIMATION_HIDE_DURATION_MS); + mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAllowTouches = true; + notifyMenuStateChangeFinish(menuState); + if (allowMenuTimeout) { + repostDelayedHide(INITIAL_DISMISS_DELAY); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mAllowTouches = true; + } + }); + if (withDelay) { + // starts the menu container animation after window expansion is completed + notifyMenuStateChangeStart(menuState, resizeMenuOnShow, () -> { + if (mMenuContainerAnimator == null) { + return; + } + mMenuContainerAnimator.setStartDelay(MENU_SHOW_ON_EXPAND_START_DELAY); + setVisibility(VISIBLE); + mMenuContainerAnimator.start(); + }); + } else { + notifyMenuStateChangeStart(menuState, resizeMenuOnShow, null); + setVisibility(VISIBLE); + mMenuContainerAnimator.start(); + } + updateActionViews(menuState, stackBounds); + } else { + // If we are already visible, then just start the delayed dismiss and unregister any + // existing input consumers from the previous drag + if (allowMenuTimeout) { + repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); + } + } + } + + /** + * Different from {@link #hideMenu()}, this function does not try to finish this menu activity + * and instead, it fades out the controls by setting the alpha to 0 directly without menu + * visibility callbacks invoked. + */ + void fadeOutMenu() { + mMenuContainer.setAlpha(0f); + mSettingsButton.setAlpha(0f); + mDismissButton.setAlpha(0f); + mEnterSplitButton.setAlpha(0f); + } + + void pokeMenu() { + cancelDelayedHide(); + } + + void updateMenuLayout(Rect bounds) { + mPipMenuIconsAlgorithm.onBoundsChanged(bounds); + } + + void hideMenu() { + hideMenu(null); + } + + void hideMenu(Runnable animationEndCallback) { + hideMenu(animationEndCallback, true /* notifyMenuVisibility */, mDidLastShowMenuResize, + ANIM_TYPE_HIDE); + } + + void hideMenu(boolean resize, @AnimationType int animationType) { + hideMenu(null /* animationFinishedRunnable */, true /* notifyMenuVisibility */, resize, + animationType); + } + + void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility, + boolean resize, @AnimationType int animationType) { + if (mMenuState != MENU_STATE_NONE) { + cancelDelayedHide(); + if (notifyMenuVisibility) { + notifyMenuStateChangeStart(MENU_STATE_NONE, resize, null); + } + mMenuContainerAnimator = new AnimatorSet(); + ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA, + mMenuContainer.getAlpha(), 0f); + menuAnim.addUpdateListener(mMenuBgUpdateListener); + ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, + mSettingsButton.getAlpha(), 0f); + ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, + mDismissButton.getAlpha(), 0f); + ObjectAnimator enterSplitAnim = ObjectAnimator.ofFloat(mEnterSplitButton, View.ALPHA, + mEnterSplitButton.getAlpha(), 0f); + mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, + enterSplitAnim); + mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT); + mMenuContainerAnimator.setDuration(getFadeOutDuration(animationType)); + mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setVisibility(GONE); + if (notifyMenuVisibility) { + notifyMenuStateChangeFinish(MENU_STATE_NONE); + } + if (animationFinishedRunnable != null) { + animationFinishedRunnable.run(); + } + } + }); + mMenuContainerAnimator.start(); + } + } + + /** + * @return Estimated minimum {@link Size} to hold the actions. + * See also {@link #updateActionViews(Rect)} + */ + Size getEstimatedMinMenuSize() { + final int pipActionSize = getResources().getDimensionPixelSize(R.dimen.pip_action_size); + // the minimum width would be (2 * pipActionSize) since we have settings and dismiss button + // on the top action container. + final int width = Math.max(2, mActions.size()) * pipActionSize; + final int height = getResources().getDimensionPixelSize(R.dimen.pip_expand_action_size) + + getResources().getDimensionPixelSize(R.dimen.pip_action_padding) + + getResources().getDimensionPixelSize(R.dimen.pip_expand_container_edge_margin); + return new Size(width, height); + } + + void setActions(Rect stackBounds, @Nullable List<RemoteAction> actions, + @Nullable RemoteAction closeAction) { + mActions.clear(); + if (actions != null && !actions.isEmpty()) { + mActions.addAll(actions); + } + mCloseAction = closeAction; + if (mMenuState == MENU_STATE_FULL) { + updateActionViews(mMenuState, stackBounds); + } + } + + private void updateActionViews(int menuState, Rect stackBounds) { + ViewGroup expandContainer = findViewById(R.id.expand_container); + ViewGroup actionsContainer = findViewById(R.id.actions_container); + actionsContainer.setOnTouchListener((v, ev) -> { + // Do nothing, prevent click through to parent + return true; + }); + + // Update the expand button only if it should show with the menu + expandContainer.setVisibility(menuState == MENU_STATE_FULL + ? View.VISIBLE + : View.INVISIBLE); + + LayoutParams expandedLp = + (LayoutParams) expandContainer.getLayoutParams(); + if (mActions.isEmpty() || menuState == MENU_STATE_NONE) { + actionsContainer.setVisibility(View.INVISIBLE); + + // Update the expand container margin to adjust the center of the expand button to + // account for the existence of the action container + expandedLp.topMargin = 0; + expandedLp.bottomMargin = 0; + } else { + actionsContainer.setVisibility(View.VISIBLE); + if (mActionsGroup != null) { + // Ensure we have as many buttons as actions + final LayoutInflater inflater = LayoutInflater.from(mContext); + while (mActionsGroup.getChildCount() < mActions.size()) { + final PipMenuActionView actionView = (PipMenuActionView) inflater.inflate( + R.layout.pip_menu_action, mActionsGroup, false); + mActionsGroup.addView(actionView); + } + + // Update the visibility of all views + for (int i = 0; i < mActionsGroup.getChildCount(); i++) { + mActionsGroup.getChildAt(i).setVisibility(i < mActions.size() + ? View.VISIBLE + : View.GONE); + } + + // Recreate the layout + final boolean isLandscapePip = stackBounds != null + && (stackBounds.width() > stackBounds.height()); + for (int i = 0; i < mActions.size(); i++) { + final RemoteAction action = mActions.get(i); + final PipMenuActionView actionView = + (PipMenuActionView) mActionsGroup.getChildAt(i); + final boolean isCloseAction = mCloseAction != null && Objects.equals( + mCloseAction.getActionIntent(), action.getActionIntent()); + + final int iconType = action.getIcon().getType(); + if (iconType == Icon.TYPE_URI || iconType == Icon.TYPE_URI_ADAPTIVE_BITMAP) { + // Disallow loading icon from content URI + actionView.setImageDrawable(null); + } else { + // TODO: Check if the action drawable has changed before we reload it + action.getIcon().loadDrawableAsync(mContext, d -> { + if (d != null) { + d.setTint(Color.WHITE); + actionView.setImageDrawable(d); + } + }, mMainHandler); + } + actionView.setCustomCloseBackgroundVisibility( + isCloseAction ? View.VISIBLE : View.GONE); + actionView.setContentDescription(action.getContentDescription()); + if (action.isEnabled()) { + actionView.setOnClickListener( + v -> onActionViewClicked(action.getActionIntent(), isCloseAction)); + } + actionView.setEnabled(action.isEnabled()); + actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); + + // Update the margin between actions + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) + actionView.getLayoutParams(); + lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0; + } + } + + // Update the expand container margin to adjust the center of the expand button to + // account for the existence of the action container + expandedLp.topMargin = getResources().getDimensionPixelSize( + R.dimen.pip_action_padding); + expandedLp.bottomMargin = getResources().getDimensionPixelSize( + R.dimen.pip_expand_container_edge_margin); + } + expandContainer.requestLayout(); + } + + private void notifyMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { + mController.onMenuStateChangeStart(menuState, resize, callback); + } + + private void notifyMenuStateChangeFinish(int menuState) { + mMenuState = menuState; + mController.onMenuStateChangeFinish(menuState); + } + + private void expandPip() { + // Do not notify menu visibility when hiding the menu, the controller will do this when it + // handles the message + hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* resize */, + ANIM_TYPE_HIDE); + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); + } + + private void dismissPip() { + if (mMenuState != MENU_STATE_NONE) { + // Do not call hideMenu() directly. Instead, let the menu controller handle it just as + // any other dismissal that will update the touch state and fade out the PIP task + // and the menu view at the same time. + mController.onPipDismiss(); + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE); + } + } + + /** + * Execute the {@link PendingIntent} attached to the {@link PipMenuActionView}. + * If the given {@link PendingIntent} matches {@link #mCloseAction}, we need to make sure + * the PiP is removed after a certain timeout in case the app does not respond in a + * timely manner. + */ + private void onActionViewClicked(@NonNull PendingIntent intent, boolean isCloseAction) { + try { + intent.send(); + } catch (PendingIntent.CanceledException e) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to send action, %s", TAG, e); + } + if (isCloseAction) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_CUSTOM_CLOSE); + mAllowTouches = false; + mMainExecutor.executeDelayed(() -> { + hideMenu(); + // TODO: it's unsafe to call onPipDismiss with a delay here since + // we may have a different PiP by the time this runnable is executed. + mController.onPipDismiss(); + mAllowTouches = true; + }, mPipForceCloseDelay); + } + } + + private void enterSplit() { + // Do not notify menu visibility when hiding the menu, the controller will do this when it + // handles the message + hideMenu(mController::onEnterSplit, false /* notifyMenuVisibility */, true /* resize */, + ANIM_TYPE_HIDE); + } + + + private void showSettings() { + final Pair<ComponentName, Integer> topPipActivityInfo = + PipUtils.getTopPipActivity(mContext); + if (topPipActivityInfo.first != null) { + final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS, + Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null)); + settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); + mContext.startActivityAsUser(settingsIntent, UserHandle.of(topPipActivityInfo.second)); + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_SETTINGS); + } + } + + private void cancelDelayedHide() { + mMainExecutor.removeCallbacks(mHideMenuRunnable); + } + + private void repostDelayedHide(int delay) { + int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay, + FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS); + mMainExecutor.removeCallbacks(mHideMenuRunnable); + mMainExecutor.executeDelayed(mHideMenuRunnable, recommendedTimeout); + } + + private long getFadeOutDuration(@AnimationType int animationType) { + switch (animationType) { + case ANIM_TYPE_NONE: + return ANIMATION_NONE_DURATION_MS; + case ANIM_TYPE_HIDE: + return ANIMATION_HIDE_DURATION_MS; + case ANIM_TYPE_DISMISS: + return mDismissFadeOutDurationMs; + default: + throw new IllegalStateException("Invalid animation type " + animationType); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index f3d178aef4ea..fbf4d13a0c19 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -42,8 +42,8 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; -import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; |