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