PipMenuActivityController.java revision 81d406104a1661658eba8755de59bf1df575e4c7
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.Handler;
30import android.os.Message;
31import android.os.Messenger;
32import android.os.RemoteException;
33import android.os.UserHandle;
34import android.util.Log;
35import android.util.Pair;
36import android.view.IWindowManager;
37
38import com.android.systemui.pip.phone.PipMediaController.ActionListener;
39
40import java.io.PrintWriter;
41import java.util.ArrayList;
42import java.util.List;
43
44/**
45 * Manages the PiP menu activity which can show menu options or a scrim.
46 *
47 * The current media session provides actions whenever there are no valid actions provided by the
48 * current PiP activity. Otherwise, those actions always take precedence.
49 */
50public class PipMenuActivityController {
51
52    private static final String TAG = "PipMenuActController";
53
54    public static final String EXTRA_CONTROLLER_MESSENGER = "messenger";
55    public static final String EXTRA_ACTIONS = "actions";
56    public static final String EXTRA_STACK_BOUNDS = "stack_bounds";
57    public static final String EXTRA_MOVEMENT_BOUNDS = "movement_bounds";
58    public static final String EXTRA_SHOW_MENU = "show_menu";
59
60    public static final int MESSAGE_MENU_VISIBILITY_CHANGED = 100;
61    public static final int MESSAGE_EXPAND_PIP = 101;
62    public static final int MESSAGE_MINIMIZE_PIP = 102;
63    public static final int MESSAGE_DISMISS_PIP = 103;
64    public static final int MESSAGE_UPDATE_ACTIVITY_CALLBACK = 104;
65    public static final int MESSAGE_REGISTER_INPUT_CONSUMER = 105;
66
67    /**
68     * A listener interface to receive notification on changes in PIP.
69     */
70    public interface Listener {
71        /**
72         * Called when the PIP menu visibility changes.
73         *
74         * @param menuVisible whether or not the menu is visible
75         * @param resize whether or not to resize the PiP with the visibility change
76         */
77        void onPipMenuVisibilityChanged(boolean menuVisible, boolean resize);
78
79        /**
80         * Called when the PIP requested to be expanded.
81         */
82        void onPipExpand();
83
84        /**
85         * Called when the PIP requested to be minimized.
86         */
87        void onPipMinimize();
88
89        /**
90         * Called when the PIP requested to be dismissed.
91         */
92        void onPipDismiss();
93    }
94
95    private Context mContext;
96    private IActivityManager mActivityManager;
97    private PipMediaController mMediaController;
98    private InputConsumerController mInputConsumerController;
99
100    private ArrayList<Listener> mListeners = new ArrayList<>();
101    private ParceledListSlice mAppActions;
102    private ParceledListSlice mMediaActions;
103    private boolean mMenuVisible;
104
105    private boolean mStartActivityRequested;
106    private Messenger mToActivityMessenger;
107    private Messenger mMessenger = new Messenger(new Handler() {
108        @Override
109        public void handleMessage(Message msg) {
110            switch (msg.what) {
111                case MESSAGE_MENU_VISIBILITY_CHANGED: {
112                    boolean visible = msg.arg1 > 0;
113                    onMenuVisibilityChanged(visible, true /* resize */);
114                    break;
115                }
116                case MESSAGE_EXPAND_PIP: {
117                    mListeners.forEach(l -> l.onPipExpand());
118                    // Preemptively mark the menu as invisible once we expand the PiP, but don't
119                    // resize as we will be animating the stack
120                    onMenuVisibilityChanged(false, false /* resize */);
121                    break;
122                }
123                case MESSAGE_MINIMIZE_PIP: {
124                    mListeners.forEach(l -> l.onPipMinimize());
125                    break;
126                }
127                case MESSAGE_DISMISS_PIP: {
128                    mListeners.forEach(l -> l.onPipDismiss());
129                    // Preemptively mark the menu as invisible once we dismiss the PiP, but don't
130                    // resize as we'll be removing the stack in place
131                    onMenuVisibilityChanged(false, false /* resize */);
132                    break;
133                }
134                case MESSAGE_REGISTER_INPUT_CONSUMER: {
135                    mInputConsumerController.registerInputConsumer();
136                    break;
137                }
138                case MESSAGE_UPDATE_ACTIVITY_CALLBACK: {
139                    mToActivityMessenger = msg.replyTo;
140                    mStartActivityRequested = false;
141                    // Mark the menu as invisible once the activity finishes as well
142                    if (mToActivityMessenger == null) {
143                        onMenuVisibilityChanged(false, true /* resize */);
144                    }
145                    break;
146                }
147            }
148        }
149    });
150
151    private ActionListener mMediaActionListener = new ActionListener() {
152        @Override
153        public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
154            mMediaActions = new ParceledListSlice<>(mediaActions);
155            updateMenuActions();
156        }
157    };
158
159    public PipMenuActivityController(Context context, IActivityManager activityManager,
160            PipMediaController mediaController, InputConsumerController inputConsumerController) {
161        mContext = context;
162        mActivityManager = activityManager;
163        mMediaController = mediaController;
164        mInputConsumerController = inputConsumerController;
165    }
166
167    public void onActivityPinned() {
168        if (!mMenuVisible) {
169            // If the menu is not visible, then re-register the input consumer if it is not already
170            // registered
171            mInputConsumerController.registerInputConsumer();
172        }
173    }
174
175    /**
176     * Adds a new menu activity listener.
177     */
178    public void addListener(Listener listener) {
179        if (!mListeners.contains(listener)) {
180            mListeners.add(listener);
181        }
182    }
183
184    /**
185     * Updates the appearance of the menu and scrim on top of the PiP while dismissing.
186     */
187    public void setDismissFraction(float fraction) {
188        if (mToActivityMessenger != null) {
189            Message m = Message.obtain();
190            m.what = PipMenuActivity.MESSAGE_UPDATE_DISMISS_FRACTION;
191            m.obj = fraction;
192            try {
193                mToActivityMessenger.send(m);
194            } catch (RemoteException e) {
195                Log.e(TAG, "Could not notify menu to show", e);
196            }
197        } else if (!mStartActivityRequested) {
198            startMenuActivity(null /* stackBounds */, null /* movementBounds */,
199                    false /* showMenu */);
200        }
201    }
202
203    /**
204     * Shows the menu activity.
205     */
206    public void showMenu(Rect stackBounds, Rect movementBounds) {
207        if (mToActivityMessenger != null) {
208            Message m = Message.obtain();
209            m.what = PipMenuActivity.MESSAGE_SHOW_MENU;
210            m.obj = new Pair<>(stackBounds, movementBounds);
211            try {
212                mToActivityMessenger.send(m);
213            } catch (RemoteException e) {
214                Log.e(TAG, "Could not notify menu to show", e);
215            }
216        } else if (!mStartActivityRequested) {
217            startMenuActivity(stackBounds, movementBounds, true /* showMenu */);
218        }
219    }
220
221    /**
222     * Pokes the menu, indicating that the user is interacting with it.
223     */
224    public void pokeMenu() {
225        if (mToActivityMessenger != null) {
226            Message m = Message.obtain();
227            m.what = PipMenuActivity.MESSAGE_POKE_MENU;
228            try {
229                mToActivityMessenger.send(m);
230            } catch (RemoteException e) {
231                Log.e(TAG, "Could not notify poke menu", e);
232            }
233        }
234    }
235
236    /**
237     * Hides the menu activity.
238     */
239    public void hideMenu() {
240        if (mToActivityMessenger != null) {
241            Message m = Message.obtain();
242            m.what = PipMenuActivity.MESSAGE_HIDE_MENU;
243            try {
244                mToActivityMessenger.send(m);
245            } catch (RemoteException e) {
246                Log.e(TAG, "Could not notify menu to hide", e);
247            }
248        }
249    }
250
251    /**
252     * @return whether the menu is currently visible.
253     */
254    public boolean isMenuVisible() {
255        return mMenuVisible;
256    }
257
258    /**
259     * Sets the menu actions to the actions provided by the current PiP activity.
260     */
261    public void setAppActions(ParceledListSlice appActions) {
262        mAppActions = appActions;
263        updateMenuActions();
264    }
265
266    /**
267     * @return the best set of actions to show in the PiP menu.
268     */
269    private ParceledListSlice resolveMenuActions() {
270        if (isValidActions(mAppActions)) {
271            return mAppActions;
272        }
273        return mMediaActions;
274    }
275
276    /**
277     * Starts the menu activity on the top task of the pinned stack.
278     */
279    private void startMenuActivity(Rect stackBounds, Rect movementBounds, boolean showMenu) {
280        try {
281            StackInfo pinnedStackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
282            if (pinnedStackInfo != null && pinnedStackInfo.taskIds != null &&
283                    pinnedStackInfo.taskIds.length > 0) {
284                Intent intent = new Intent(mContext, PipMenuActivity.class);
285                intent.putExtra(EXTRA_CONTROLLER_MESSENGER, mMessenger);
286                intent.putExtra(EXTRA_ACTIONS, resolveMenuActions());
287                if (stackBounds != null) {
288                    intent.putExtra(EXTRA_STACK_BOUNDS, stackBounds.flattenToString());
289                }
290                if (movementBounds != null) {
291                    intent.putExtra(EXTRA_MOVEMENT_BOUNDS, movementBounds.flattenToString());
292                }
293                intent.putExtra(EXTRA_SHOW_MENU, showMenu);
294                ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
295                options.setLaunchTaskId(
296                        pinnedStackInfo.taskIds[pinnedStackInfo.taskIds.length - 1]);
297                options.setTaskOverlay(true, true /* canResume */);
298                mContext.startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT);
299                mStartActivityRequested = true;
300            } else {
301                Log.e(TAG, "No PIP tasks found");
302            }
303        } catch (RemoteException e) {
304            mStartActivityRequested = false;
305            Log.e(TAG, "Error showing PIP menu activity", e);
306        }
307    }
308
309    /**
310     * Updates the PiP menu activity with the best set of actions provided.
311     */
312    private void updateMenuActions() {
313        if (mToActivityMessenger != null) {
314            // Fetch the pinned stack bounds
315            Rect stackBounds = null;
316            try {
317                StackInfo pinnedStackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
318                if (pinnedStackInfo != null) {
319                    stackBounds = pinnedStackInfo.bounds;
320                }
321            } catch (RemoteException e) {
322                Log.e(TAG, "Error showing PIP menu activity", e);
323            }
324
325            Message m = Message.obtain();
326            m.what = PipMenuActivity.MESSAGE_UPDATE_ACTIONS;
327            m.obj = new Pair<>(stackBounds, resolveMenuActions());
328            try {
329                mToActivityMessenger.send(m);
330            } catch (RemoteException e) {
331                Log.e(TAG, "Could not notify menu activity to update actions", e);
332            }
333        }
334    }
335
336    /**
337     * Returns whether the set of actions are valid.
338     */
339    private boolean isValidActions(ParceledListSlice actions) {
340        return actions != null && actions.getList().size() > 0;
341    }
342
343    /**
344     * Handles changes in menu visibility.
345     */
346    private void onMenuVisibilityChanged(boolean visible, boolean resize) {
347        if (visible) {
348            mInputConsumerController.unregisterInputConsumer();
349        } else {
350            mInputConsumerController.registerInputConsumer();
351        }
352        if (visible != mMenuVisible) {
353            mListeners.forEach(l -> l.onPipMenuVisibilityChanged(visible, resize));
354            if (visible) {
355                // Once visible, start listening for media action changes. This call will trigger
356                // the menu actions to be updated again.
357                mMediaController.addListener(mMediaActionListener);
358            } else {
359                // Once hidden, stop listening for media action changes. This call will trigger
360                // the menu actions to be updated again.
361                mMediaController.removeListener(mMediaActionListener);
362            }
363        }
364        mMenuVisible = visible;
365    }
366
367    public void dump(PrintWriter pw, String prefix) {
368        final String innerPrefix = prefix + "  ";
369        pw.println(prefix + TAG);
370        pw.println(innerPrefix + "mMenuVisible=" + mMenuVisible);
371        pw.println(innerPrefix + "mListeners=" + mListeners.size());
372    }
373}
374