• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.systemui.pip.phone;
18 
19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
21 
22 import android.annotation.Nullable;
23 import android.app.ActivityManager.StackInfo;
24 import android.app.ActivityOptions;
25 import android.app.ActivityTaskManager;
26 import android.app.RemoteAction;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.ParceledListSlice;
30 import android.graphics.Rect;
31 import android.os.Bundle;
32 import android.os.Debug;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.os.Message;
36 import android.os.Messenger;
37 import android.os.RemoteException;
38 import android.os.SystemClock;
39 import android.os.UserHandle;
40 import android.util.Log;
41 import android.view.MotionEvent;
42 
43 import com.android.systemui.pip.phone.PipMediaController.ActionListener;
44 import com.android.systemui.shared.system.InputConsumerController;
45 
46 import java.io.PrintWriter;
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * Manages the PiP menu activity which can show menu options or a scrim.
52  *
53  * The current media session provides actions whenever there are no valid actions provided by the
54  * current PiP activity. Otherwise, those actions always take precedence.
55  */
56 public class PipMenuActivityController {
57 
58     private static final String TAG = "PipMenuActController";
59     private static final boolean DEBUG = false;
60 
61     public static final String EXTRA_CONTROLLER_MESSENGER = "messenger";
62     public static final String EXTRA_ACTIONS = "actions";
63     public static final String EXTRA_STACK_BOUNDS = "stack_bounds";
64     public static final String EXTRA_ALLOW_TIMEOUT = "allow_timeout";
65     public static final String EXTRA_WILL_RESIZE_MENU = "resize_menu_on_show";
66     public static final String EXTRA_DISMISS_FRACTION = "dismiss_fraction";
67     public static final String EXTRA_MENU_STATE = "menu_state";
68     public static final String EXTRA_SHOW_MENU_WITH_DELAY = "show_menu_with_delay";
69     public static final String EXTRA_SHOW_RESIZE_HANDLE = "show_resize_handle";
70     public static final String EXTRA_MESSAGE_CALLBACK_WHAT = "message_callback_what";
71 
72     public static final int MESSAGE_MENU_STATE_CHANGED = 100;
73     public static final int MESSAGE_EXPAND_PIP = 101;
74     public static final int MESSAGE_DISMISS_PIP = 103;
75     public static final int MESSAGE_UPDATE_ACTIVITY_CALLBACK = 104;
76     public static final int MESSAGE_SHOW_MENU = 107;
77 
78     public static final int MENU_STATE_NONE = 0;
79     public static final int MENU_STATE_CLOSE = 1;
80     public static final int MENU_STATE_FULL = 2;
81 
82     // The duration to wait before we consider the start activity as having timed out
83     private static final long START_ACTIVITY_REQUEST_TIMEOUT_MS = 300;
84 
85     /**
86      * A listener interface to receive notification on changes in PIP.
87      */
88     public interface Listener {
89         /**
90          * Called when the PIP menu visibility changes.
91          *
92          * @param menuState the current state of the menu
93          * @param resize whether or not to resize the PiP with the state change
94          */
onPipMenuStateChanged(int menuState, boolean resize, Runnable callback)95         void onPipMenuStateChanged(int menuState, boolean resize, Runnable callback);
96 
97         /**
98          * Called when the PIP requested to be expanded.
99          */
onPipExpand()100         void onPipExpand();
101 
102         /**
103          * Called when the PIP requested to be dismissed.
104          */
onPipDismiss()105         void onPipDismiss();
106 
107         /**
108          * Called when the PIP requested to show the menu.
109          */
onPipShowMenu()110         void onPipShowMenu();
111     }
112 
113     private Context mContext;
114     private PipMediaController mMediaController;
115     private InputConsumerController mInputConsumerController;
116 
117     private ArrayList<Listener> mListeners = new ArrayList<>();
118     private ParceledListSlice mAppActions;
119     private ParceledListSlice mMediaActions;
120     private int mMenuState;
121 
122     // The dismiss fraction update is sent frequently, so use a temporary bundle for the message
123     private Bundle mTmpDismissFractionData = new Bundle();
124 
125     private Runnable mOnAnimationEndRunnable;
126     private boolean mStartActivityRequested;
127     private long mStartActivityRequestedTime;
128     private Messenger mToActivityMessenger;
129     private Handler mHandler = new Handler(Looper.getMainLooper()) {
130         @Override
131         public void handleMessage(Message msg) {
132             switch (msg.what) {
133                 case MESSAGE_MENU_STATE_CHANGED: {
134                     final int menuState = msg.arg1;
135                     final boolean resize = msg.arg2 != 0;
136                     onMenuStateChanged(menuState, resize,
137                             getMenuStateChangeFinishedCallback(msg.replyTo, (Bundle) msg.obj));
138                     break;
139                 }
140                 case MESSAGE_EXPAND_PIP: {
141                     mListeners.forEach(Listener::onPipExpand);
142                     break;
143                 }
144                 case MESSAGE_DISMISS_PIP: {
145                     mListeners.forEach(Listener::onPipDismiss);
146                     break;
147                 }
148                 case MESSAGE_SHOW_MENU: {
149                     mListeners.forEach(Listener::onPipShowMenu);
150                     break;
151                 }
152                 case MESSAGE_UPDATE_ACTIVITY_CALLBACK: {
153                     mToActivityMessenger = msg.replyTo;
154                     setStartActivityRequested(false);
155                     if (mOnAnimationEndRunnable != null) {
156                         mOnAnimationEndRunnable.run();
157                         mOnAnimationEndRunnable = null;
158                     }
159                     // Mark the menu as invisible once the activity finishes as well
160                     if (mToActivityMessenger == null) {
161                         final boolean resize = msg.arg1 != 0;
162                         onMenuStateChanged(MENU_STATE_NONE, resize, null /* callback */);
163                     }
164                     break;
165                 }
166             }
167         }
168     };
169     private Messenger mMessenger = new Messenger(mHandler);
170 
171     private Runnable mStartActivityRequestedTimeoutRunnable = () -> {
172         setStartActivityRequested(false);
173         if (mOnAnimationEndRunnable != null) {
174             mOnAnimationEndRunnable.run();
175             mOnAnimationEndRunnable = null;
176         }
177         Log.e(TAG, "Expected start menu activity request timed out");
178     };
179 
180     private ActionListener mMediaActionListener = new ActionListener() {
181         @Override
182         public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
183             mMediaActions = new ParceledListSlice<>(mediaActions);
184             updateMenuActions();
185         }
186     };
187 
PipMenuActivityController(Context context, PipMediaController mediaController, InputConsumerController inputConsumerController)188     public PipMenuActivityController(Context context,
189             PipMediaController mediaController, InputConsumerController inputConsumerController) {
190         mContext = context;
191         mMediaController = mediaController;
192         mInputConsumerController = inputConsumerController;
193     }
194 
isMenuActivityVisible()195     public boolean isMenuActivityVisible() {
196         return mToActivityMessenger != null;
197     }
198 
onActivityPinned()199     public void onActivityPinned() {
200         mInputConsumerController.registerInputConsumer(true /* withSfVsync */);
201     }
202 
onActivityUnpinned()203     public void onActivityUnpinned() {
204         hideMenu();
205         mInputConsumerController.unregisterInputConsumer();
206         setStartActivityRequested(false);
207     }
208 
onPinnedStackAnimationEnded()209     public void onPinnedStackAnimationEnded() {
210         // Note: Only active menu activities care about this event
211         if (mToActivityMessenger != null) {
212             Message m = Message.obtain();
213             m.what = PipMenuActivity.MESSAGE_ANIMATION_ENDED;
214             try {
215                 mToActivityMessenger.send(m);
216             } catch (RemoteException e) {
217                 Log.e(TAG, "Could not notify menu pinned animation ended", e);
218             }
219         }
220     }
221 
222     /**
223      * Adds a new menu activity listener.
224      */
addListener(Listener listener)225     public void addListener(Listener listener) {
226         if (!mListeners.contains(listener)) {
227             mListeners.add(listener);
228         }
229     }
230 
231     /**
232      * Updates the appearance of the menu and scrim on top of the PiP while dismissing.
233      */
setDismissFraction(float fraction)234     public void setDismissFraction(float fraction) {
235         if (DEBUG) {
236             Log.d(TAG, "setDismissFraction() hasActivity=" + (mToActivityMessenger != null)
237                     + " fraction=" + fraction);
238         }
239         if (mToActivityMessenger != null) {
240             mTmpDismissFractionData.clear();
241             mTmpDismissFractionData.putFloat(EXTRA_DISMISS_FRACTION, fraction);
242             Message m = Message.obtain();
243             m.what = PipMenuActivity.MESSAGE_UPDATE_DISMISS_FRACTION;
244             m.obj = mTmpDismissFractionData;
245             try {
246                 mToActivityMessenger.send(m);
247             } catch (RemoteException e) {
248                 Log.e(TAG, "Could not notify menu to update dismiss fraction", e);
249             }
250         } else if (!mStartActivityRequested || isStartActivityRequestedElapsed()) {
251             // If we haven't requested the start activity, or if it previously took too long to
252             // start, then start it
253             startMenuActivity(MENU_STATE_NONE, null /* stackBounds */,
254                     false /* allowMenuTimeout */, false /* resizeMenuOnShow */,
255                     false /* withDelay */, false /* showResizeHandle */);
256         }
257     }
258 
259     /**
260      * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu
261      * upon PiP window transition is finished.
262      */
showMenuWithDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean willResizeMenu, boolean showResizeHandle)263     public void showMenuWithDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout,
264             boolean willResizeMenu, boolean showResizeHandle) {
265         // hide all visible controls including close button and etc. first, this is to ensure
266         // menu is totally invisible during the transition to eliminate unpleasant artifacts
267         fadeOutMenu();
268         showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
269                 true /* withDelay */, showResizeHandle);
270     }
271 
272     /**
273      * Shows the menu activity immediately.
274      */
showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean willResizeMenu, boolean showResizeHandle)275     public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
276             boolean willResizeMenu, boolean showResizeHandle) {
277         showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
278                 false /* withDelay */, showResizeHandle);
279     }
280 
getMenuStateChangeFinishedCallback(@ullable Messenger replyTo, @Nullable Bundle data)281     private Runnable getMenuStateChangeFinishedCallback(@Nullable Messenger replyTo,
282             @Nullable Bundle data) {
283         if (replyTo == null || data == null) {
284             return null;
285         }
286         return () -> {
287             try {
288                 final Message m = Message.obtain();
289                 m.what = data.getInt(EXTRA_MESSAGE_CALLBACK_WHAT);
290                 replyTo.send(m);
291             } catch (RemoteException e) {
292                 // ignored
293             }
294         };
295     }
296 
297     private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout,
298             boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) {
299         if (DEBUG) {
300             Log.d(TAG, "showMenu() state=" + menuState
301                     + " hasActivity=" + (mToActivityMessenger != null)
302                     + " allowMenuTimeout=" + allowMenuTimeout
303                     + " willResizeMenu=" + willResizeMenu
304                     + " withDelay=" + withDelay
305                     + " showResizeHandle=" + showResizeHandle
306                     + " callers=\n" + Debug.getCallers(5, "    "));
307         }
308 
309         if (mToActivityMessenger != null) {
310             Bundle data = new Bundle();
311             data.putInt(EXTRA_MENU_STATE, menuState);
312             if (stackBounds != null) {
313                 data.putParcelable(EXTRA_STACK_BOUNDS, stackBounds);
314             }
315             data.putBoolean(EXTRA_ALLOW_TIMEOUT, allowMenuTimeout);
316             data.putBoolean(EXTRA_WILL_RESIZE_MENU, willResizeMenu);
317             data.putBoolean(EXTRA_SHOW_MENU_WITH_DELAY, withDelay);
318             data.putBoolean(EXTRA_SHOW_RESIZE_HANDLE, showResizeHandle);
319             Message m = Message.obtain();
320             m.what = PipMenuActivity.MESSAGE_SHOW_MENU;
321             m.obj = data;
322             try {
323                 mToActivityMessenger.send(m);
324             } catch (RemoteException e) {
325                 Log.e(TAG, "Could not notify menu to show", e);
326             }
327         } else if (!mStartActivityRequested || isStartActivityRequestedElapsed()) {
328             // If we haven't requested the start activity, or if it previously took too long to
329             // start, then start it
330             startMenuActivity(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay,
331                     showResizeHandle);
332         }
333     }
334 
335     /**
336      * Pokes the menu, indicating that the user is interacting with it.
337      */
338     public void pokeMenu() {
339         if (DEBUG) {
340             Log.d(TAG, "pokeMenu() hasActivity=" + (mToActivityMessenger != null));
341         }
342         if (mToActivityMessenger != null) {
343             Message m = Message.obtain();
344             m.what = PipMenuActivity.MESSAGE_POKE_MENU;
345             try {
346                 mToActivityMessenger.send(m);
347             } catch (RemoteException e) {
348                 Log.e(TAG, "Could not notify poke menu", e);
349             }
350         }
351     }
352 
353     private void fadeOutMenu() {
354         if (DEBUG) {
355             Log.d(TAG, "fadeOutMenu() state=" + mMenuState
356                     + " hasActivity=" + (mToActivityMessenger != null)
357                     + " callers=\n" + Debug.getCallers(5, "    "));
358         }
359         if (mToActivityMessenger != null) {
360             Message m = Message.obtain();
361             m.what = PipMenuActivity.MESSAGE_FADE_OUT_MENU;
362             try {
363                 mToActivityMessenger.send(m);
364             } catch (RemoteException e) {
365                 Log.e(TAG, "Could not notify menu to fade out", e);
366             }
367         }
368     }
369 
370     /**
371      * Hides the menu activity.
372      */
373     public void hideMenu() {
374         if (DEBUG) {
375             Log.d(TAG, "hideMenu() state=" + mMenuState
376                     + " hasActivity=" + (mToActivityMessenger != null)
377                     + " callers=\n" + Debug.getCallers(5, "    "));
378         }
379         if (mToActivityMessenger != null) {
380             Message m = Message.obtain();
381             m.what = PipMenuActivity.MESSAGE_HIDE_MENU;
382             try {
383                 mToActivityMessenger.send(m);
384             } catch (RemoteException e) {
385                 Log.e(TAG, "Could not notify menu to hide", e);
386             }
387         }
388     }
389 
390     /**
391      * Hides the menu activity.
392      */
393     public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) {
394         if (mStartActivityRequested) {
395             // If the menu has been start-requested, but not actually started, then we defer the
396             // trigger callback until the menu has started and called back to the controller.
397             mOnAnimationEndRunnable = onEndCallback;
398             onStartCallback.run();
399 
400             // Fallback for b/63752800, we have started the PipMenuActivity but it has not made any
401             // callbacks. Don't continue to wait for the menu to show past some timeout.
402             mHandler.removeCallbacks(mStartActivityRequestedTimeoutRunnable);
403             mHandler.postDelayed(mStartActivityRequestedTimeoutRunnable,
404                     START_ACTIVITY_REQUEST_TIMEOUT_MS);
405         } else if (mMenuState != MENU_STATE_NONE && mToActivityMessenger != null) {
406             // If the menu is visible in either the closed or full state, then hide the menu and
407             // trigger the animation trigger afterwards
408             onStartCallback.run();
409             Message m = Message.obtain();
410             m.what = PipMenuActivity.MESSAGE_HIDE_MENU;
411             m.obj = onEndCallback;
412             try {
413                 mToActivityMessenger.send(m);
414             } catch (RemoteException e) {
415                 Log.e(TAG, "Could not notify hide menu", e);
416             }
417         }
418     }
419 
420     /**
421      * Preemptively mark the menu as invisible, used when we are directly manipulating the pinned
422      * stack and don't want to trigger a resize which can animate the stack in a conflicting way
423      * (ie. when manually expanding or dismissing).
424      */
425     public void hideMenuWithoutResize() {
426         onMenuStateChanged(MENU_STATE_NONE, false /* resize */, null /* callback */);
427     }
428 
429     /**
430      * Sets the menu actions to the actions provided by the current PiP activity.
431      */
432     public void setAppActions(ParceledListSlice appActions) {
433         mAppActions = appActions;
434         updateMenuActions();
435     }
436 
437     /**
438      * @return the best set of actions to show in the PiP menu.
439      */
440     private ParceledListSlice resolveMenuActions() {
441         if (isValidActions(mAppActions)) {
442             return mAppActions;
443         }
444         return mMediaActions;
445     }
446 
447     /**
448      * Starts the menu activity on the top task of the pinned stack.
449      */
450     private void startMenuActivity(int menuState, Rect stackBounds, boolean allowMenuTimeout,
451             boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) {
452         try {
453             StackInfo pinnedStackInfo = ActivityTaskManager.getService().getStackInfo(
454                     WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
455             if (pinnedStackInfo != null && pinnedStackInfo.taskIds != null &&
456                     pinnedStackInfo.taskIds.length > 0) {
457                 Intent intent = new Intent(mContext, PipMenuActivity.class);
458                 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
459                 intent.putExtra(EXTRA_CONTROLLER_MESSENGER, mMessenger);
460                 intent.putExtra(EXTRA_ACTIONS, resolveMenuActions());
461                 if (stackBounds != null) {
462                     intent.putExtra(EXTRA_STACK_BOUNDS, stackBounds);
463                 }
464                 intent.putExtra(EXTRA_MENU_STATE, menuState);
465                 intent.putExtra(EXTRA_ALLOW_TIMEOUT, allowMenuTimeout);
466                 intent.putExtra(EXTRA_WILL_RESIZE_MENU, willResizeMenu);
467                 intent.putExtra(EXTRA_SHOW_MENU_WITH_DELAY, withDelay);
468                 intent.putExtra(EXTRA_SHOW_RESIZE_HANDLE, showResizeHandle);
469                 ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
470                 options.setLaunchTaskId(
471                         pinnedStackInfo.taskIds[pinnedStackInfo.taskIds.length - 1]);
472                 options.setTaskOverlay(true, true /* canResume */);
473                 mContext.startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT);
474                 setStartActivityRequested(true);
475             } else {
476                 Log.e(TAG, "No PIP tasks found");
477             }
478         } catch (RemoteException e) {
479             setStartActivityRequested(false);
480             Log.e(TAG, "Error showing PIP menu activity", e);
481         }
482     }
483 
484     /**
485      * Updates the PiP menu activity with the best set of actions provided.
486      */
487     private void updateMenuActions() {
488         if (mToActivityMessenger != null) {
489             // Fetch the pinned stack bounds
490             Rect stackBounds = null;
491             try {
492                 StackInfo pinnedStackInfo = ActivityTaskManager.getService().getStackInfo(
493                         WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
494                 if (pinnedStackInfo != null) {
495                     stackBounds = pinnedStackInfo.bounds;
496                 }
497             } catch (RemoteException e) {
498                 Log.e(TAG, "Error showing PIP menu activity", e);
499             }
500 
501             Bundle data = new Bundle();
502             data.putParcelable(EXTRA_STACK_BOUNDS, stackBounds);
503             data.putParcelable(EXTRA_ACTIONS, resolveMenuActions());
504             Message m = Message.obtain();
505             m.what = PipMenuActivity.MESSAGE_UPDATE_ACTIONS;
506             m.obj = data;
507             try {
508                 mToActivityMessenger.send(m);
509             } catch (RemoteException e) {
510                 Log.e(TAG, "Could not notify menu activity to update actions", e);
511             }
512         }
513     }
514 
515     /**
516      * Returns whether the set of actions are valid.
517      */
518     private boolean isValidActions(ParceledListSlice actions) {
519         return actions != null && actions.getList().size() > 0;
520     }
521 
522     /**
523      * @return whether the time of the activity request has exceeded the timeout.
524      */
525     private boolean isStartActivityRequestedElapsed() {
526         return (SystemClock.uptimeMillis() - mStartActivityRequestedTime)
527                 >= START_ACTIVITY_REQUEST_TIMEOUT_MS;
528     }
529 
530     /**
531      * Handles changes in menu visibility.
532      */
533     private void onMenuStateChanged(int menuState, boolean resize, Runnable callback) {
534         if (DEBUG) {
535             Log.d(TAG, "onMenuStateChanged() mMenuState=" + mMenuState
536                     + " menuState=" + menuState + " resize=" + resize
537                     + " callers=\n" + Debug.getCallers(5, "    "));
538         }
539 
540         if (menuState != mMenuState) {
541             mListeners.forEach(l -> l.onPipMenuStateChanged(menuState, resize, callback));
542             if (menuState == MENU_STATE_FULL) {
543                 // Once visible, start listening for media action changes. This call will trigger
544                 // the menu actions to be updated again.
545                 mMediaController.addListener(mMediaActionListener);
546             } else {
547                 // Once hidden, stop listening for media action changes. This call will trigger
548                 // the menu actions to be updated again.
549                 mMediaController.removeListener(mMediaActionListener);
550             }
551         }
552         mMenuState = menuState;
553     }
554 
555     private void setStartActivityRequested(boolean requested) {
556         mHandler.removeCallbacks(mStartActivityRequestedTimeoutRunnable);
557         mStartActivityRequested = requested;
558         mStartActivityRequestedTime = requested ? SystemClock.uptimeMillis() : 0;
559     }
560 
561     /**
562      * Handles a pointer event sent from pip input consumer.
563      */
564     void handlePointerEvent(MotionEvent ev) {
565         if (mToActivityMessenger != null) {
566             Message m = Message.obtain();
567             m.what = PipMenuActivity.MESSAGE_POINTER_EVENT;
568             m.obj = ev;
569             try {
570                 mToActivityMessenger.send(m);
571             } catch (RemoteException e) {
572                 Log.e(TAG, "Could not dispatch touch event", e);
573             }
574         }
575     }
576 
577     public void dump(PrintWriter pw, String prefix) {
578         final String innerPrefix = prefix + "  ";
579         pw.println(prefix + TAG);
580         pw.println(innerPrefix + "mMenuState=" + mMenuState);
581         pw.println(innerPrefix + "mToActivityMessenger=" + mToActivityMessenger);
582         pw.println(innerPrefix + "mListeners=" + mListeners.size());
583         pw.println(innerPrefix + "mStartActivityRequested=" + mStartActivityRequested);
584         pw.println(innerPrefix + "mStartActivityRequestedTime=" + mStartActivityRequestedTime);
585     }
586 }
587