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