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