Home | History | Annotate | Download | only in phone
      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          */
     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