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