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