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
17package com.android.systemui.pip.phone;
18
19import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
20import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
21
22import android.app.ActivityManager.StackInfo;
23import android.app.ActivityOptions;
24import android.app.IActivityManager;
25import android.app.RemoteAction;
26import android.content.Context;
27import android.content.Intent;
28import android.content.pm.ParceledListSlice;
29import android.graphics.Rect;
30import android.os.Bundle;
31import android.os.Debug;
32import android.os.Handler;
33import android.os.Message;
34import android.os.Messenger;
35import android.os.RemoteException;
36import android.os.SystemClock;
37import android.os.UserHandle;
38import android.util.Log;
39import android.view.IWindowManager;
40
41import com.android.systemui.pip.phone.PipMediaController.ActionListener;
42import com.android.systemui.recents.events.EventBus;
43import com.android.systemui.recents.events.component.HidePipMenuEvent;
44import com.android.systemui.recents.misc.ReferenceCountedTrigger;
45import com.android.systemui.shared.system.InputConsumerController;
46
47import java.io.PrintWriter;
48import java.util.ArrayList;
49import 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 */
57public 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         */
97        void onPipMenuStateChanged(int menuState, boolean resize);
98
99        /**
100         * Called when the PIP requested to be expanded.
101         */
102        void onPipExpand();
103
104        /**
105         * Called when the PIP requested to be minimized.
106         */
107        void onPipMinimize();
108
109        /**
110         * Called when the PIP requested to be dismissed.
111         */
112        void onPipDismiss();
113
114        /**
115         * Called when the PIP requested to show the menu.
116         */
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
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
215    public boolean isMenuActivityVisible() {
216        return mToActivityMessenger != null;
217    }
218
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
227    public void onActivityUnpinned() {
228        hideMenu();
229        setStartActivityRequested(false);
230    }
231
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
492    private void setStartActivityRequested(boolean requested) {
493        mHandler.removeCallbacks(mStartActivityRequestedTimeoutRunnable);
494        mStartActivityRequested = requested;
495        mStartActivityRequestedTime = requested ? SystemClock.uptimeMillis() : 0;
496    }
497
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
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