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.ActivityManager.StackId.PINNED_STACK_ID;
     20 
     21 import android.app.ActivityManager.StackInfo;
     22 import android.app.ActivityOptions;
     23 import android.app.IActivityManager;
     24 import android.app.RemoteAction;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.pm.ParceledListSlice;
     28 import android.graphics.Rect;
     29 import android.os.Bundle;
     30 import android.os.Debug;
     31 import android.os.Handler;
     32 import android.os.Message;
     33 import android.os.Messenger;
     34 import android.os.RemoteException;
     35 import android.os.UserHandle;
     36 import android.util.Log;
     37 import android.view.IWindowManager;
     38 
     39 import com.android.systemui.pip.phone.PipMediaController.ActionListener;
     40 import com.android.systemui.recents.events.EventBus;
     41 import com.android.systemui.recents.events.component.HidePipMenuEvent;
     42 import com.android.systemui.recents.misc.ReferenceCountedTrigger;
     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_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