1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.pip.phone;
18 
19 import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP;
20 
21 import android.annotation.Nullable;
22 import android.app.ActivityManager;
23 import android.app.RemoteAction;
24 import android.content.Context;
25 import android.graphics.Matrix;
26 import android.graphics.Rect;
27 import android.graphics.RectF;
28 import android.os.Debug;
29 import android.os.Handler;
30 import android.os.RemoteException;
31 import android.util.Size;
32 import android.view.MotionEvent;
33 import android.view.SurfaceControl;
34 import android.view.View;
35 import android.view.ViewRootImpl;
36 import android.view.WindowManagerGlobal;
37 
38 import com.android.internal.protolog.common.ProtoLog;
39 import com.android.wm.shell.common.ShellExecutor;
40 import com.android.wm.shell.common.SystemWindows;
41 import com.android.wm.shell.common.pip.PipBoundsState;
42 import com.android.wm.shell.common.pip.PipMediaController;
43 import com.android.wm.shell.common.pip.PipMediaController.ActionListener;
44 import com.android.wm.shell.common.pip.PipUiEventLogger;
45 import com.android.wm.shell.pip.PipMenuController;
46 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
47 import com.android.wm.shell.protolog.ShellProtoLogGroup;
48 import com.android.wm.shell.splitscreen.SplitScreenController;
49 
50 import java.io.PrintWriter;
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.Optional;
54 
55 /**
56  * Manages the PiP menu view which can show menu options or a scrim.
57  *
58  * The current media session provides actions whenever there are no valid actions provided by the
59  * current PiP activity. Otherwise, those actions always take precedence.
60  */
61 public class PhonePipMenuController implements PipMenuController {
62 
63     private static final String TAG = "PhonePipMenuController";
64     private static final boolean DEBUG = false;
65 
66     public static final int MENU_STATE_NONE = 0;
67     public static final int MENU_STATE_FULL = 1;
68 
69     /**
70      * A listener interface to receive notification on changes in PIP.
71      */
72     public interface Listener {
73         /**
74          * Called when the PIP menu visibility change has started.
75          *
76          * @param menuState the new, about-to-change state of the menu
77          * @param resize whether or not to resize the PiP with the state change
78          */
onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback)79         void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback);
80 
81         /**
82          * Called when the PIP menu state has finished changing/animating.
83          *
84          * @param menuState the new state of the menu.
85          */
onPipMenuStateChangeFinish(int menuState)86         void onPipMenuStateChangeFinish(int menuState);
87 
88         /**
89          * Called when the PIP requested to be expanded.
90          */
onPipExpand()91         void onPipExpand();
92 
93         /**
94          * Called when the PIP requested to be dismissed.
95          */
onPipDismiss()96         void onPipDismiss();
97 
98         /**
99          * Called when the PIP requested to show the menu.
100          */
onPipShowMenu()101         void onPipShowMenu();
102 
103         /**
104          * Called when the PIP requested to enter Split.
105          */
onEnterSplit()106         void onEnterSplit();
107     }
108 
109     private final Matrix mMoveTransform = new Matrix();
110     private final Rect mTmpSourceBounds = new Rect();
111     private final RectF mTmpSourceRectF = new RectF();
112     private final RectF mTmpDestinationRectF = new RectF();
113     private final Context mContext;
114     private final PipBoundsState mPipBoundsState;
115     private final PipMediaController mMediaController;
116     private final ShellExecutor mMainExecutor;
117     private final Handler mMainHandler;
118 
119     private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
120             mSurfaceControlTransactionFactory;
121     private final float[] mTmpTransform = new float[9];
122 
123     private final ArrayList<Listener> mListeners = new ArrayList<>();
124     private final SystemWindows mSystemWindows;
125     private final Optional<SplitScreenController> mSplitScreenController;
126     private final PipUiEventLogger mPipUiEventLogger;
127 
128     private List<RemoteAction> mAppActions;
129     private RemoteAction mCloseAction;
130     private List<RemoteAction> mMediaActions;
131 
132     private int mMenuState;
133 
134     private PipMenuView mPipMenuView;
135 
136     private SurfaceControl mLeash;
137 
138     private ActionListener mMediaActionListener = new ActionListener() {
139         @Override
140         public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
141             mMediaActions = new ArrayList<>(mediaActions);
142             updateMenuActions();
143         }
144     };
145 
PhonePipMenuController(Context context, PipBoundsState pipBoundsState, PipMediaController mediaController, SystemWindows systemWindows, Optional<SplitScreenController> splitScreenOptional, PipUiEventLogger pipUiEventLogger, ShellExecutor mainExecutor, Handler mainHandler)146     public PhonePipMenuController(Context context, PipBoundsState pipBoundsState,
147             PipMediaController mediaController, SystemWindows systemWindows,
148             Optional<SplitScreenController> splitScreenOptional,
149             PipUiEventLogger pipUiEventLogger,
150             ShellExecutor mainExecutor, Handler mainHandler) {
151         mContext = context;
152         mPipBoundsState = pipBoundsState;
153         mMediaController = mediaController;
154         mSystemWindows = systemWindows;
155         mMainExecutor = mainExecutor;
156         mMainHandler = mainHandler;
157         mSplitScreenController = splitScreenOptional;
158         mPipUiEventLogger = pipUiEventLogger;
159 
160         mSurfaceControlTransactionFactory =
161                 new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
162     }
163 
isMenuVisible()164     public boolean isMenuVisible() {
165         return mPipMenuView != null && mMenuState != MENU_STATE_NONE;
166     }
167 
168     /**
169      * Attach the menu when the PiP task first appears.
170      */
171     @Override
attach(SurfaceControl leash)172     public void attach(SurfaceControl leash) {
173         mLeash = leash;
174         attachPipMenuView();
175     }
176 
177     /**
178      * Detach the menu when the PiP task is gone.
179      */
180     @Override
detach()181     public void detach() {
182         hideMenu();
183         detachPipMenuView();
184         mLeash = null;
185     }
186 
attachPipMenuView()187     void attachPipMenuView() {
188         // In case detach was not called (e.g. PIP unexpectedly closed)
189         if (mPipMenuView != null) {
190             detachPipMenuView();
191         }
192         mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler,
193                 mSplitScreenController, mPipUiEventLogger);
194         mPipMenuView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
195             @Override
196             public void onViewAttachedToWindow(View v) {
197                 v.getViewRootImpl().addSurfaceChangedCallback(
198                         new ViewRootImpl.SurfaceChangedCallback() {
199                             @Override
200                             public void surfaceCreated(SurfaceControl.Transaction t) {
201                                 final SurfaceControl sc = getSurfaceControl();
202                                 if (sc != null) {
203                                     t.reparent(sc, mLeash);
204                                     // make menu on top of the surface
205                                     t.setLayer(sc, Integer.MAX_VALUE);
206                                 }
207                             }
208 
209                             @Override
210                             public void surfaceReplaced(SurfaceControl.Transaction t) {
211                             }
212 
213                             @Override
214                             public void surfaceDestroyed() {
215                             }
216                         });
217             }
218 
219             @Override
220             public void onViewDetachedFromWindow(View v) {
221             }
222         });
223 
224         mSystemWindows.addView(mPipMenuView,
225                 getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
226                 0, SHELL_ROOT_LAYER_PIP);
227         setShellRootAccessibilityWindow();
228 
229         // Make sure the initial actions are set
230         updateMenuActions();
231     }
232 
detachPipMenuView()233     private void detachPipMenuView() {
234         if (mPipMenuView == null) {
235             return;
236         }
237 
238         mSystemWindows.removeView(mPipMenuView);
239         mPipMenuView = null;
240     }
241 
242     /**
243      * Updates the layout parameters of the menu.
244      * @param destinationBounds New Menu bounds.
245      */
246     @Override
updateMenuBounds(Rect destinationBounds)247     public void updateMenuBounds(Rect destinationBounds) {
248         mSystemWindows.updateViewLayout(mPipMenuView,
249                 getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, destinationBounds.width(),
250                         destinationBounds.height()));
251         updateMenuLayout(destinationBounds);
252     }
253 
254     @Override
onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo)255     public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
256         if (mPipMenuView != null) {
257             mPipMenuView.onFocusTaskChanged(taskInfo);
258         }
259     }
260 
261     /**
262      * Tries to grab a surface control from {@link PipMenuView}. If this isn't available for some
263      * reason (ie. the window isn't ready yet, thus {@link android.view.ViewRootImpl} is
264      * {@code null}), it will get the leash that the WindowlessWM has assigned to it.
265      */
getSurfaceControl()266     public SurfaceControl getSurfaceControl() {
267         return mSystemWindows.getViewSurface(mPipMenuView);
268     }
269 
270     /**
271      * Adds a new menu activity listener.
272      */
addListener(Listener listener)273     public void addListener(Listener listener) {
274         if (!mListeners.contains(listener)) {
275             mListeners.add(listener);
276         }
277     }
278 
279     @Nullable
getEstimatedMinMenuSize()280     Size getEstimatedMinMenuSize() {
281         return mPipMenuView == null ? null : mPipMenuView.getEstimatedMinMenuSize();
282     }
283 
284     /**
285      * When other components requests the menu controller directly to show the menu, we must
286      * first fire off the request to the other listeners who will then propagate the call
287      * back to the controller with the right parameters.
288      */
289     @Override
showMenu()290     public void showMenu() {
291         mListeners.forEach(Listener::onPipShowMenu);
292     }
293 
294     /**
295      * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu
296      * upon PiP window transition is finished.
297      */
showMenuWithPossibleDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean willResizeMenu, boolean showResizeHandle)298     public void showMenuWithPossibleDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout,
299             boolean willResizeMenu, boolean showResizeHandle) {
300         if (willResizeMenu) {
301             // hide all visible controls including close button and etc. first, this is to ensure
302             // menu is totally invisible during the transition to eliminate unpleasant artifacts
303             fadeOutMenu();
304         }
305         showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
306                 willResizeMenu /* withDelay=willResizeMenu here */, showResizeHandle);
307     }
308 
309     /**
310      * Shows the menu activity immediately.
311      */
showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean willResizeMenu, boolean showResizeHandle)312     public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
313             boolean willResizeMenu, boolean showResizeHandle) {
314         showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
315                 false /* withDelay */, showResizeHandle);
316     }
317 
showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean willResizeMenu, boolean withDelay, boolean showResizeHandle)318     private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout,
319             boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) {
320         if (DEBUG) {
321             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
322                     "%s: showMenu() state=%s"
323                             + " isMenuVisible=%s"
324                             + " allowMenuTimeout=%s"
325                             + " willResizeMenu=%s"
326                             + " withDelay=%s"
327                             + " showResizeHandle=%s"
328                             + " callers=\n%s", TAG, menuState, isMenuVisible(), allowMenuTimeout,
329                     willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, "    "));
330         }
331 
332         if (!checkPipMenuState()) {
333             return;
334         }
335 
336         // Sync the menu bounds before showing it in case it is out of sync.
337         movePipMenu(null /* pipLeash */, null /* transaction */, stackBounds,
338                 PipMenuController.ALPHA_NO_CHANGE);
339         updateMenuBounds(stackBounds);
340 
341         mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay,
342                 showResizeHandle);
343     }
344 
345     /**
346      * Move the PiP menu, which does a translation and possibly a scale transformation.
347      */
348     @Override
movePipMenu(@ullable SurfaceControl pipLeash, @Nullable SurfaceControl.Transaction t, Rect destinationBounds, float alpha)349     public void movePipMenu(@Nullable SurfaceControl pipLeash,
350             @Nullable SurfaceControl.Transaction t,
351             Rect destinationBounds, float alpha) {
352         if (destinationBounds.isEmpty()) {
353             return;
354         }
355 
356         if (!checkPipMenuState()) {
357             return;
358         }
359 
360         // TODO(b/286307861) transaction should be applied outside of PiP menu controller
361         if (pipLeash != null && t != null) {
362             t.apply();
363         }
364     }
365 
366     /**
367      * Does an immediate window crop of the PiP menu.
368      */
369     @Override
resizePipMenu(@ullable SurfaceControl pipLeash, @Nullable SurfaceControl.Transaction t, Rect destinationBounds)370     public void resizePipMenu(@Nullable SurfaceControl pipLeash,
371             @Nullable SurfaceControl.Transaction t,
372             Rect destinationBounds) {
373         if (destinationBounds.isEmpty()) {
374             return;
375         }
376 
377         if (!checkPipMenuState()) {
378             return;
379         }
380 
381         // TODO(b/286307861) transaction should be applied outside of PiP menu controller
382         if (pipLeash != null && t != null) {
383             t.apply();
384         }
385     }
386 
checkPipMenuState()387     private boolean checkPipMenuState() {
388         if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
389             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
390                     "%s: Not going to move PiP, either menu or its parent is not created.", TAG);
391             return false;
392         }
393 
394         return true;
395     }
396 
397     /**
398      * Pokes the menu, indicating that the user is interacting with it.
399      */
pokeMenu()400     public void pokeMenu() {
401         final boolean isMenuVisible = isMenuVisible();
402         if (DEBUG) {
403             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
404                     "%s: pokeMenu() isMenuVisible=%b", TAG, isMenuVisible);
405         }
406         if (isMenuVisible) {
407             mPipMenuView.pokeMenu();
408         }
409     }
410 
fadeOutMenu()411     private void fadeOutMenu() {
412         final boolean isMenuVisible = isMenuVisible();
413         if (DEBUG) {
414             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
415                     "%s: fadeOutMenu() isMenuVisible=%b", TAG, isMenuVisible);
416         }
417         if (isMenuVisible) {
418             mPipMenuView.fadeOutMenu();
419         }
420     }
421 
422     /**
423      * Hides the menu view.
424      */
hideMenu()425     public void hideMenu() {
426         final boolean isMenuVisible = isMenuVisible();
427         if (isMenuVisible) {
428             mPipMenuView.hideMenu();
429         }
430     }
431 
432     /**
433      * Hides the menu view.
434      *
435      * @param animationType the animation type to use upon hiding the menu
436      * @param resize whether or not to resize the PiP with the state change
437      */
hideMenu(@ipMenuView.AnimationType int animationType, boolean resize)438     public void hideMenu(@PipMenuView.AnimationType int animationType, boolean resize) {
439         final boolean isMenuVisible = isMenuVisible();
440         if (DEBUG) {
441             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
442                     "%s: hideMenu() state=%s"
443                             + " isMenuVisible=%s"
444                             + " animationType=%s"
445                             + " resize=%s"
446                             + " callers=\n%s", TAG, mMenuState, isMenuVisible,
447                     animationType, resize,
448                     Debug.getCallers(5, "    "));
449         }
450         if (isMenuVisible) {
451             mPipMenuView.hideMenu(resize, animationType);
452         }
453     }
454 
455     /**
456      * Hides the menu activity.
457      */
hideMenu(Runnable onStartCallback, Runnable onEndCallback)458     public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) {
459         if (isMenuVisible()) {
460             // If the menu is visible in either the closed or full state, then hide the menu and
461             // trigger the animation trigger afterwards
462             if (onStartCallback != null) {
463                 onStartCallback.run();
464             }
465             mPipMenuView.hideMenu(onEndCallback);
466         }
467     }
468 
469     /**
470      * Sets the menu actions to the actions provided by the current PiP menu.
471      */
472     @Override
setAppActions(List<RemoteAction> appActions, RemoteAction closeAction)473     public void setAppActions(List<RemoteAction> appActions,
474             RemoteAction closeAction) {
475         mAppActions = appActions;
476         mCloseAction = closeAction;
477         updateMenuActions();
478     }
479 
onPipExpand()480     void onPipExpand() {
481         mListeners.forEach(Listener::onPipExpand);
482     }
483 
onPipDismiss()484     void onPipDismiss() {
485         mListeners.forEach(Listener::onPipDismiss);
486     }
487 
onEnterSplit()488     void onEnterSplit() {
489         mListeners.forEach(Listener::onEnterSplit);
490     }
491 
492     /**
493      * @return the best set of actions to show in the PiP menu.
494      */
resolveMenuActions()495     private List<RemoteAction> resolveMenuActions() {
496         if (isValidActions(mAppActions)) {
497             return mAppActions;
498         }
499         return mMediaActions;
500     }
501 
502     /**
503      * Updates the PiP menu with the best set of actions provided.
504      */
updateMenuActions()505     private void updateMenuActions() {
506         if (mPipMenuView != null) {
507             mPipMenuView.setActions(mPipBoundsState.getBounds(),
508                     resolveMenuActions(), mCloseAction);
509         }
510     }
511 
512     /**
513      * Returns whether the set of actions are valid.
514      */
isValidActions(List<?> actions)515     private static boolean isValidActions(List<?> actions) {
516         return actions != null && actions.size() > 0;
517     }
518 
519     /**
520      * Handles changes in menu visibility.
521      */
onMenuStateChangeStart(int menuState, boolean resize, Runnable callback)522     void onMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
523         if (DEBUG) {
524             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
525                     "%s: onMenuStateChangeStart() mMenuState=%s"
526                             + " menuState=%s resize=%s"
527                             + " callers=\n%s", TAG, mMenuState, menuState, resize,
528                     Debug.getCallers(5, "    "));
529         }
530 
531         if (menuState != mMenuState) {
532             mListeners.forEach(l -> l.onPipMenuStateChangeStart(menuState, resize, callback));
533             if (menuState == MENU_STATE_FULL) {
534                 // Once visible, start listening for media action changes. This call will trigger
535                 // the menu actions to be updated again.
536                 mMediaController.addActionListener(mMediaActionListener);
537             } else {
538                 // Once hidden, stop listening for media action changes. This call will trigger
539                 // the menu actions to be updated again.
540                 mMediaController.removeActionListener(mMediaActionListener);
541             }
542 
543             try {
544                 WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */,
545                         mSystemWindows.getFocusGrantToken(mPipMenuView),
546                         menuState != MENU_STATE_NONE /* grantFocus */);
547             } catch (RemoteException e) {
548                 ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
549                         "%s: Unable to update focus as menu appears/disappears, %s", TAG, e);
550             }
551         }
552     }
553 
onMenuStateChangeFinish(int menuState)554     void onMenuStateChangeFinish(int menuState) {
555         if (menuState != mMenuState) {
556             mListeners.forEach(l -> l.onPipMenuStateChangeFinish(menuState));
557         }
558         mMenuState = menuState;
559         setShellRootAccessibilityWindow();
560     }
561 
setShellRootAccessibilityWindow()562     private void setShellRootAccessibilityWindow() {
563         switch (mMenuState) {
564             case MENU_STATE_NONE:
565                 mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, null);
566                 break;
567             default:
568                 mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP,
569                         mPipMenuView);
570                 break;
571         }
572     }
573 
574     /**
575      * Handles a pointer event sent from pip input consumer.
576      */
handlePointerEvent(MotionEvent ev)577     void handlePointerEvent(MotionEvent ev) {
578         if (mPipMenuView == null) {
579             return;
580         }
581 
582         if (ev.isTouchEvent()) {
583             mPipMenuView.dispatchTouchEvent(ev);
584         } else {
585             mPipMenuView.dispatchGenericMotionEvent(ev);
586         }
587     }
588 
589     /**
590      * Tell the PIP Menu to recalculate its layout given its current position on the display.
591      */
updateMenuLayout(Rect bounds)592     public void updateMenuLayout(Rect bounds) {
593         final boolean isMenuVisible = isMenuVisible();
594         if (DEBUG) {
595             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
596                     "%s: updateMenuLayout() state=%s"
597                             + " isMenuVisible=%s"
598                             + " callers=\n%s", TAG, mMenuState, isMenuVisible,
599                     Debug.getCallers(5, "    "));
600         }
601         if (isMenuVisible) {
602             mPipMenuView.updateMenuLayout(bounds);
603         }
604     }
605 
dump(PrintWriter pw, String prefix)606     void dump(PrintWriter pw, String prefix) {
607         final String innerPrefix = prefix + "  ";
608         pw.println(prefix + TAG);
609         pw.println(innerPrefix + "mMenuState=" + mMenuState);
610         pw.println(innerPrefix + "mPipMenuView=" + mPipMenuView);
611         pw.println(innerPrefix + "mListeners=" + mListeners.size());
612     }
613 }
614