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