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