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