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 com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_ACTIONS;
     20 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_ALLOW_TIMEOUT;
     21 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_CONTROLLER_MESSENGER;
     22 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_WILL_RESIZE_MENU;
     23 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_DISMISS_FRACTION;
     24 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_MOVEMENT_BOUNDS;
     25 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_MENU_STATE;
     26 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_STACK_BOUNDS;
     27 
     28 import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE;
     29 import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE;
     30 import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL;
     31 
     32 import android.animation.Animator;
     33 import android.animation.AnimatorListenerAdapter;
     34 import android.animation.AnimatorSet;
     35 import android.animation.ObjectAnimator;
     36 import android.animation.ValueAnimator;
     37 import android.annotation.Nullable;
     38 import android.app.Activity;
     39 import android.app.ActivityManager;
     40 import android.app.PendingIntent.CanceledException;
     41 import android.app.RemoteAction;
     42 import android.content.Intent;
     43 import android.content.pm.ParceledListSlice;
     44 import android.graphics.Color;
     45 import android.graphics.PointF;
     46 import android.graphics.Rect;
     47 import android.graphics.drawable.ColorDrawable;
     48 import android.graphics.drawable.Drawable;
     49 import android.os.Bundle;
     50 import android.os.Handler;
     51 import android.os.Message;
     52 import android.os.Messenger;
     53 import android.os.RemoteException;
     54 import android.util.Log;
     55 import android.view.LayoutInflater;
     56 import android.view.MotionEvent;
     57 import android.view.View;
     58 import android.view.View.OnTouchListener;
     59 import android.view.ViewConfiguration;
     60 import android.view.ViewGroup;
     61 import android.view.WindowManager.LayoutParams;
     62 import android.widget.FrameLayout;
     63 import android.widget.ImageView;
     64 import android.widget.LinearLayout;
     65 
     66 import com.android.systemui.Interpolators;
     67 import com.android.systemui.R;
     68 import com.android.systemui.recents.events.EventBus;
     69 import com.android.systemui.recents.events.component.HidePipMenuEvent;
     70 
     71 import java.util.ArrayList;
     72 import java.util.Collections;
     73 import java.util.List;
     74 
     75 /**
     76  * Translucent activity that gets started on top of a task in PIP to allow the user to control it.
     77  */
     78 public class PipMenuActivity extends Activity {
     79 
     80     private static final String TAG = "PipMenuActivity";
     81 
     82     public static final int MESSAGE_SHOW_MENU = 1;
     83     public static final int MESSAGE_POKE_MENU = 2;
     84     public static final int MESSAGE_HIDE_MENU = 3;
     85     public static final int MESSAGE_UPDATE_ACTIONS = 4;
     86     public static final int MESSAGE_UPDATE_DISMISS_FRACTION = 5;
     87     public static final int MESSAGE_ANIMATION_ENDED = 6;
     88 
     89     private static final long INITIAL_DISMISS_DELAY = 3500;
     90     private static final long POST_INTERACTION_DISMISS_DELAY = 2000;
     91     private static final long MENU_FADE_DURATION = 125;
     92 
     93     private static final float MENU_BACKGROUND_ALPHA = 0.3f;
     94     private static final float DISMISS_BACKGROUND_ALPHA = 0.6f;
     95 
     96     private static final float DISABLED_ACTION_ALPHA = 0.54f;
     97 
     98     private int mMenuState;
     99     private boolean mAllowMenuTimeout = true;
    100     private boolean mAllowTouches = true;
    101 
    102     private final List<RemoteAction> mActions = new ArrayList<>();
    103 
    104     private View mViewRoot;
    105     private Drawable mBackgroundDrawable;
    106     private View mMenuContainer;
    107     private LinearLayout mActionsGroup;
    108     private View mDismissButton;
    109     private ImageView mExpandButton;
    110     private int mBetweenActionPaddingLand;
    111 
    112     private AnimatorSet mMenuContainerAnimator;
    113 
    114     private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener =
    115             new ValueAnimator.AnimatorUpdateListener() {
    116                 @Override
    117                 public void onAnimationUpdate(ValueAnimator animation) {
    118                     final float alpha = (float) animation.getAnimatedValue();
    119                     mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA*alpha*255));
    120                 }
    121             };
    122 
    123     private PipTouchState mTouchState;
    124     private PointF mDownPosition = new PointF();
    125     private PointF mDownDelta = new PointF();
    126     private ViewConfiguration mViewConfig;
    127     private Handler mHandler = new Handler();
    128     private Messenger mToControllerMessenger;
    129     private Messenger mMessenger = new Messenger(new Handler() {
    130         @Override
    131         public void handleMessage(Message msg) {
    132             switch (msg.what) {
    133                 case MESSAGE_SHOW_MENU: {
    134                     final Bundle data = (Bundle) msg.obj;
    135                     showMenu(data.getInt(EXTRA_MENU_STATE),
    136                             data.getParcelable(EXTRA_STACK_BOUNDS),
    137                             data.getParcelable(EXTRA_MOVEMENT_BOUNDS),
    138                             data.getBoolean(EXTRA_ALLOW_TIMEOUT),
    139                             data.getBoolean(EXTRA_WILL_RESIZE_MENU));
    140                     break;
    141                 }
    142                 case MESSAGE_POKE_MENU:
    143                     cancelDelayedFinish();
    144                     break;
    145                 case MESSAGE_HIDE_MENU:
    146                     hideMenu();
    147                     break;
    148                 case MESSAGE_UPDATE_ACTIONS: {
    149                     final Bundle data = (Bundle) msg.obj;
    150                     final ParceledListSlice actions = data.getParcelable(EXTRA_ACTIONS);
    151                     setActions(data.getParcelable(EXTRA_STACK_BOUNDS), actions != null
    152                             ? actions.getList() : Collections.EMPTY_LIST);
    153                     break;
    154                 }
    155                 case MESSAGE_UPDATE_DISMISS_FRACTION: {
    156                     final Bundle data = (Bundle) msg.obj;
    157                     updateDismissFraction(data.getFloat(EXTRA_DISMISS_FRACTION));
    158                     break;
    159                 }
    160                 case MESSAGE_ANIMATION_ENDED: {
    161                     mAllowTouches = true;
    162                     break;
    163                 }
    164             }
    165         }
    166     });
    167 
    168     private final Runnable mFinishRunnable = new Runnable() {
    169         @Override
    170         public void run() {
    171             hideMenu();
    172         }
    173     };
    174 
    175     @Override
    176     protected void onCreate(@Nullable Bundle savedInstanceState) {
    177         // Set the flags to allow us to watch for outside touches and also hide the menu and start
    178         // manipulating the PIP in the same touch gesture
    179         mViewConfig = ViewConfiguration.get(this);
    180         mTouchState = new PipTouchState(mViewConfig, mHandler, () -> {
    181             if (mMenuState == MENU_STATE_CLOSE) {
    182                 showPipMenu();
    183             } else {
    184                 expandPip();
    185             }
    186         });
    187         getWindow().addFlags(LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | LayoutParams.FLAG_SLIPPERY);
    188 
    189         super.onCreate(savedInstanceState);
    190         setContentView(R.layout.pip_menu_activity);
    191 
    192         mBackgroundDrawable = new ColorDrawable(Color.BLACK);
    193         mBackgroundDrawable.setAlpha(0);
    194         mViewRoot = findViewById(R.id.background);
    195         mViewRoot.setBackground(mBackgroundDrawable);
    196         mMenuContainer = findViewById(R.id.menu_container);
    197         mMenuContainer.setAlpha(0);
    198         mMenuContainer.setOnTouchListener((v, event) -> {
    199             mTouchState.onTouchEvent(event);
    200             switch (event.getAction()) {
    201                 case MotionEvent.ACTION_UP:
    202                     if (mTouchState.isDoubleTap() || mMenuState == MENU_STATE_FULL) {
    203                         // Expand to fullscreen if this is a double tap or we are already expanded
    204                         expandPip();
    205                     } else if (!mTouchState.isWaitingForDoubleTap()) {
    206                         // User has stalled long enough for this not to be a drag or a double tap,
    207                         // just expand the menu if necessary
    208                         if (mMenuState == MENU_STATE_CLOSE) {
    209                             showPipMenu();
    210                         }
    211                     } else {
    212                         // Next touch event _may_ be the second tap for the double-tap, schedule a
    213                         // fallback runnable to trigger the menu if no touch event occurs before the
    214                         // next tap
    215                         mTouchState.scheduleDoubleTapTimeoutCallback();
    216                     }
    217                     break;
    218             }
    219             return true;
    220         });
    221         mDismissButton = findViewById(R.id.dismiss);
    222         mDismissButton.setAlpha(0);
    223         mDismissButton.setOnClickListener((v) -> {
    224             dismissPip();
    225         });
    226         mActionsGroup = findViewById(R.id.actions_group);
    227         mBetweenActionPaddingLand = getResources().getDimensionPixelSize(
    228                 R.dimen.pip_between_action_padding_land);
    229         mExpandButton = findViewById(R.id.expand_button);
    230 
    231         updateFromIntent(getIntent());
    232         setTitle(R.string.pip_menu_title);
    233         setDisablePreviewScreenshots(true);
    234     }
    235 
    236     @Override
    237     protected void onNewIntent(Intent intent) {
    238         super.onNewIntent(intent);
    239         updateFromIntent(intent);
    240     }
    241 
    242     @Override
    243     public void onUserInteraction() {
    244         if (mAllowMenuTimeout) {
    245             repostDelayedFinish(POST_INTERACTION_DISMISS_DELAY);
    246         }
    247     }
    248 
    249     @Override
    250     protected void onUserLeaveHint() {
    251         super.onUserLeaveHint();
    252 
    253         // If another task is starting on top of the menu, then hide and finish it so that it can be
    254         // recreated on the top next time it starts
    255         hideMenu();
    256     }
    257 
    258     @Override
    259     protected void onStop() {
    260         super.onStop();
    261 
    262         cancelDelayedFinish();
    263         EventBus.getDefault().unregister(this);
    264     }
    265 
    266     @Override
    267     protected void onDestroy() {
    268         super.onDestroy();
    269 
    270         // Fallback, if we are destroyed for any other reason (like when the task is being reset),
    271         // also reset the callback.
    272         notifyActivityCallback(null);
    273     }
    274 
    275     @Override
    276     public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
    277         if (!isInPictureInPictureMode) {
    278             finish();
    279         }
    280     }
    281 
    282     @Override
    283     public boolean dispatchTouchEvent(MotionEvent ev) {
    284         if (!mAllowTouches) {
    285             return super.dispatchTouchEvent(ev);
    286         }
    287 
    288         // On the first action outside the window, hide the menu
    289         switch (ev.getAction()) {
    290             case MotionEvent.ACTION_OUTSIDE:
    291                 hideMenu();
    292                 break;
    293             case MotionEvent.ACTION_DOWN:
    294                 mDownPosition.set(ev.getX(), ev.getY());
    295                 mDownDelta.set(0f, 0f);
    296                 break;
    297             case MotionEvent.ACTION_MOVE:
    298                 mDownDelta.set(ev.getX() - mDownPosition.x, ev.getY() - mDownPosition.y);
    299                 if (mDownDelta.length() > mViewConfig.getScaledTouchSlop()
    300                         && mMenuState != MENU_STATE_NONE) {
    301                     // Restore the input consumer and let that drive the movement of this menu
    302                     notifyRegisterInputConsumer();
    303                     cancelDelayedFinish();
    304                 }
    305                 break;
    306         }
    307         return super.dispatchTouchEvent(ev);
    308     }
    309 
    310     @Override
    311     public void finish() {
    312         notifyActivityCallback(null);
    313         super.finish();
    314         // Hide without an animation (the menu should already be invisible at this point)
    315         overridePendingTransition(0, 0);
    316     }
    317 
    318     @Override
    319     public void setTaskDescription(ActivityManager.TaskDescription taskDescription) {
    320         // Do nothing
    321     }
    322 
    323     public final void onBusEvent(HidePipMenuEvent event) {
    324         if (mMenuState != MENU_STATE_NONE) {
    325             // If the menu is visible in either the closed or full state, then hide the menu and
    326             // trigger the animation trigger afterwards
    327             event.getAnimationTrigger().increment();
    328             hideMenu(() -> {
    329                 mHandler.post(() -> {
    330                     event.getAnimationTrigger().decrement();
    331                 });
    332             }, true /* notifyMenuVisibility */);
    333         }
    334     }
    335 
    336     private void showMenu(int menuState, Rect stackBounds, Rect movementBounds,
    337             boolean allowMenuTimeout, boolean resizeMenuOnShow) {
    338         mAllowMenuTimeout = allowMenuTimeout;
    339         if (mMenuState != menuState) {
    340             // Disallow touches if the menu needs to resize while showing, and we are transitioning
    341             // to/from a full menu state.
    342             boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow &&
    343                     (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL);
    344             mAllowTouches = !disallowTouchesUntilAnimationEnd;
    345             cancelDelayedFinish();
    346             updateActionViews(stackBounds);
    347             if (mMenuContainerAnimator != null) {
    348                 mMenuContainerAnimator.cancel();
    349             }
    350             notifyMenuStateChange(menuState);
    351             mMenuContainerAnimator = new AnimatorSet();
    352             ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
    353                     mMenuContainer.getAlpha(), 1f);
    354             menuAnim.addUpdateListener(mMenuBgUpdateListener);
    355             ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
    356                     mDismissButton.getAlpha(), 1f);
    357             if (menuState == MENU_STATE_FULL) {
    358                 mMenuContainerAnimator.playTogether(menuAnim, dismissAnim);
    359             } else {
    360                 mMenuContainerAnimator.play(dismissAnim);
    361             }
    362             mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN);
    363             mMenuContainerAnimator.setDuration(MENU_FADE_DURATION);
    364             if (allowMenuTimeout) {
    365                 mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
    366                     @Override
    367                     public void onAnimationEnd(Animator animation) {
    368                         repostDelayedFinish(INITIAL_DISMISS_DELAY);
    369                     }
    370                 });
    371             }
    372             mMenuContainerAnimator.start();
    373         } else {
    374             // If we are already visible, then just start the delayed dismiss and unregister any
    375             // existing input consumers from the previous drag
    376             if (allowMenuTimeout) {
    377                 repostDelayedFinish(POST_INTERACTION_DISMISS_DELAY);
    378             }
    379             notifyUnregisterInputConsumer();
    380         }
    381     }
    382 
    383     private void hideMenu() {
    384         hideMenu(null /* animationFinishedRunnable */, true /* notifyMenuVisibility */);
    385     }
    386 
    387     private void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility) {
    388         if (mMenuState != MENU_STATE_NONE) {
    389             cancelDelayedFinish();
    390             if (notifyMenuVisibility) {
    391                 notifyMenuStateChange(MENU_STATE_NONE);
    392             }
    393             mMenuContainerAnimator = new AnimatorSet();
    394             ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
    395                     mMenuContainer.getAlpha(), 0f);
    396             menuAnim.addUpdateListener(mMenuBgUpdateListener);
    397             ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
    398                     mDismissButton.getAlpha(), 0f);
    399             mMenuContainerAnimator.playTogether(menuAnim, dismissAnim);
    400             mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT);
    401             mMenuContainerAnimator.setDuration(MENU_FADE_DURATION);
    402             mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
    403                 @Override
    404                 public void onAnimationEnd(Animator animation) {
    405                     if (animationFinishedRunnable != null) {
    406                         animationFinishedRunnable.run();
    407                     }
    408                     finish();
    409                 }
    410             });
    411             mMenuContainerAnimator.start();
    412         } else {
    413             // If the menu is not visible, just finish now
    414             finish();
    415         }
    416     }
    417 
    418     private void updateFromIntent(Intent intent) {
    419         mToControllerMessenger = intent.getParcelableExtra(EXTRA_CONTROLLER_MESSENGER);
    420         if (mToControllerMessenger == null) {
    421             Log.w(TAG, "Controller messenger is null. Stopping.");
    422             finish();
    423             return;
    424         }
    425         notifyActivityCallback(mMessenger);
    426 
    427         // Register for HidePipMenuEvents once we notify the controller of this activity
    428         EventBus.getDefault().register(this);
    429 
    430         ParceledListSlice actions = intent.getParcelableExtra(EXTRA_ACTIONS);
    431         if (actions != null) {
    432             mActions.clear();
    433             mActions.addAll(actions.getList());
    434         }
    435 
    436         final int menuState = intent.getIntExtra(EXTRA_MENU_STATE, MENU_STATE_NONE);
    437         if (menuState != MENU_STATE_NONE) {
    438             Rect stackBounds = intent.getParcelableExtra(EXTRA_STACK_BOUNDS);
    439             Rect movementBounds = intent.getParcelableExtra(EXTRA_MOVEMENT_BOUNDS);
    440             boolean allowMenuTimeout = intent.getBooleanExtra(EXTRA_ALLOW_TIMEOUT, true);
    441             boolean willResizeMenu = intent.getBooleanExtra(EXTRA_WILL_RESIZE_MENU, false);
    442             showMenu(menuState, stackBounds, movementBounds, allowMenuTimeout, willResizeMenu);
    443         }
    444     }
    445 
    446     private void setActions(Rect stackBounds, List<RemoteAction> actions) {
    447         mActions.clear();
    448         mActions.addAll(actions);
    449         updateActionViews(stackBounds);
    450     }
    451 
    452     private void updateActionViews(Rect stackBounds) {
    453         ViewGroup expandContainer = findViewById(R.id.expand_container);
    454         ViewGroup actionsContainer = findViewById(R.id.actions_container);
    455         actionsContainer.setOnTouchListener((v, ev) -> {
    456             // Do nothing, prevent click through to parent
    457             return true;
    458         });
    459 
    460         if (mActions.isEmpty() || mMenuState == MENU_STATE_CLOSE) {
    461             actionsContainer.setVisibility(View.INVISIBLE);
    462         } else {
    463             actionsContainer.setVisibility(View.VISIBLE);
    464             if (mActionsGroup != null) {
    465                 // Ensure we have as many buttons as actions
    466                 final LayoutInflater inflater = LayoutInflater.from(this);
    467                 while (mActionsGroup.getChildCount() < mActions.size()) {
    468                     final ImageView actionView = (ImageView) inflater.inflate(
    469                             R.layout.pip_menu_action, mActionsGroup, false);
    470                     mActionsGroup.addView(actionView);
    471                 }
    472 
    473                 // Update the visibility of all views
    474                 for (int i = 0; i < mActionsGroup.getChildCount(); i++) {
    475                     mActionsGroup.getChildAt(i).setVisibility(i < mActions.size()
    476                             ? View.VISIBLE
    477                             : View.GONE);
    478                 }
    479 
    480                 // Recreate the layout
    481                 final boolean isLandscapePip = stackBounds != null &&
    482                         (stackBounds.width() > stackBounds.height());
    483                 for (int i = 0; i < mActions.size(); i++) {
    484                     final RemoteAction action = mActions.get(i);
    485                     final ImageView actionView = (ImageView) mActionsGroup.getChildAt(i);
    486 
    487                     // TODO: Check if the action drawable has changed before we reload it
    488                     action.getIcon().loadDrawableAsync(this, d -> {
    489                         d.setTint(Color.WHITE);
    490                         actionView.setImageDrawable(d);
    491                     }, mHandler);
    492                     actionView.setContentDescription(action.getContentDescription());
    493                     if (action.isEnabled()) {
    494                         actionView.setOnClickListener(v -> {
    495                             try {
    496                                 action.getActionIntent().send();
    497                             } catch (CanceledException e) {
    498                                 Log.w(TAG, "Failed to send action", e);
    499                             }
    500                         });
    501                     }
    502                     actionView.setEnabled(action.isEnabled());
    503                     actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA);
    504 
    505                     // Update the margin between actions
    506                     LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
    507                             actionView.getLayoutParams();
    508                     lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0;
    509                 }
    510             }
    511 
    512             // Update the expand container margin to adjust the center of the expand button to
    513             // account for the existence of the action container
    514             FrameLayout.LayoutParams expandedLp =
    515                     (FrameLayout.LayoutParams) expandContainer.getLayoutParams();
    516             expandedLp.topMargin = getResources().getDimensionPixelSize(
    517                     R.dimen.pip_action_padding);
    518             expandedLp.bottomMargin = getResources().getDimensionPixelSize(
    519                     R.dimen.pip_expand_container_edge_margin);
    520             expandContainer.requestLayout();
    521         }
    522     }
    523 
    524     private void updateDismissFraction(float fraction) {
    525         int alpha;
    526         final float menuAlpha = 1 - fraction;
    527         if (mMenuState == MENU_STATE_FULL) {
    528             mMenuContainer.setAlpha(menuAlpha);
    529             mDismissButton.setAlpha(menuAlpha);
    530             final float interpolatedAlpha =
    531                     MENU_BACKGROUND_ALPHA * menuAlpha + DISMISS_BACKGROUND_ALPHA * fraction;
    532             alpha = (int) (interpolatedAlpha * 255);
    533         } else {
    534             if (mMenuState == MENU_STATE_CLOSE) {
    535                 mDismissButton.setAlpha(menuAlpha);
    536             }
    537             alpha = (int) (fraction * DISMISS_BACKGROUND_ALPHA * 255);
    538         }
    539         mBackgroundDrawable.setAlpha(alpha);
    540     }
    541 
    542     private void notifyRegisterInputConsumer() {
    543         Message m = Message.obtain();
    544         m.what = PipMenuActivityController.MESSAGE_REGISTER_INPUT_CONSUMER;
    545         sendMessage(m, "Could not notify controller to register input consumer");
    546     }
    547 
    548     private void notifyUnregisterInputConsumer() {
    549         Message m = Message.obtain();
    550         m.what = PipMenuActivityController.MESSAGE_UNREGISTER_INPUT_CONSUMER;
    551         sendMessage(m, "Could not notify controller to unregister input consumer");
    552     }
    553 
    554     private void notifyMenuStateChange(int menuState) {
    555         mMenuState = menuState;
    556         Message m = Message.obtain();
    557         m.what = PipMenuActivityController.MESSAGE_MENU_STATE_CHANGED;
    558         m.arg1 = menuState;
    559         sendMessage(m, "Could not notify controller of PIP menu visibility");
    560     }
    561 
    562     private void expandPip() {
    563         // Do not notify menu visibility when hiding the menu, the controller will do this when it
    564         // handles the message
    565         hideMenu(() -> {
    566             sendEmptyMessage(PipMenuActivityController.MESSAGE_EXPAND_PIP,
    567                     "Could not notify controller to expand PIP");
    568         }, false /* notifyMenuVisibility */);
    569     }
    570 
    571     private void minimizePip() {
    572         sendEmptyMessage(PipMenuActivityController.MESSAGE_MINIMIZE_PIP,
    573                 "Could not notify controller to minimize PIP");
    574     }
    575 
    576     private void dismissPip() {
    577         // Do not notify menu visibility when hiding the menu, the controller will do this when it
    578         // handles the message
    579         hideMenu(() -> {
    580             sendEmptyMessage(PipMenuActivityController.MESSAGE_DISMISS_PIP,
    581                     "Could not notify controller to dismiss PIP");
    582         }, false /* notifyMenuVisibility */);
    583     }
    584 
    585     private void showPipMenu() {
    586         Message m = Message.obtain();
    587         m.what = PipMenuActivityController.MESSAGE_SHOW_MENU;
    588         sendMessage(m, "Could not notify controller to show PIP menu");
    589     }
    590 
    591     private void notifyActivityCallback(Messenger callback) {
    592         Message m = Message.obtain();
    593         m.what = PipMenuActivityController.MESSAGE_UPDATE_ACTIVITY_CALLBACK;
    594         m.replyTo = callback;
    595         sendMessage(m, "Could not notify controller of activity finished");
    596     }
    597 
    598     private void sendEmptyMessage(int what, String errorMsg) {
    599         Message m = Message.obtain();
    600         m.what = what;
    601         sendMessage(m, errorMsg);
    602     }
    603 
    604     private void sendMessage(Message m, String errorMsg) {
    605         if (mToControllerMessenger == null) {
    606             return;
    607         }
    608         try {
    609             mToControllerMessenger.send(m);
    610         } catch (RemoteException e) {
    611             Log.e(TAG, errorMsg, e);
    612         }
    613     }
    614 
    615     private void cancelDelayedFinish() {
    616         mHandler.removeCallbacks(mFinishRunnable);
    617     }
    618 
    619     private void repostDelayedFinish(long delay) {
    620         mHandler.removeCallbacks(mFinishRunnable);
    621         mHandler.postDelayed(mFinishRunnable, delay);
    622     }
    623 }
    624