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