1 /* 2 * Copyright (C) 2020 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.wm.shell.pip.phone; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 20 import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; 21 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 22 import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS; 23 import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS; 24 import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS; 25 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; 26 27 import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; 28 import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; 29 30 import android.animation.Animator; 31 import android.animation.AnimatorListenerAdapter; 32 import android.animation.AnimatorSet; 33 import android.animation.ObjectAnimator; 34 import android.animation.ValueAnimator; 35 import android.annotation.IntDef; 36 import android.annotation.NonNull; 37 import android.annotation.Nullable; 38 import android.app.ActivityManager; 39 import android.app.PendingIntent; 40 import android.app.RemoteAction; 41 import android.app.WindowConfiguration; 42 import android.content.ComponentName; 43 import android.content.Context; 44 import android.content.Intent; 45 import android.graphics.Color; 46 import android.graphics.Rect; 47 import android.graphics.drawable.Drawable; 48 import android.graphics.drawable.Icon; 49 import android.net.Uri; 50 import android.os.Bundle; 51 import android.os.Handler; 52 import android.os.UserHandle; 53 import android.util.Pair; 54 import android.util.Size; 55 import android.view.KeyEvent; 56 import android.view.LayoutInflater; 57 import android.view.MotionEvent; 58 import android.view.View; 59 import android.view.ViewGroup; 60 import android.view.accessibility.AccessibilityManager; 61 import android.view.accessibility.AccessibilityNodeInfo; 62 import android.widget.FrameLayout; 63 import android.widget.LinearLayout; 64 65 import com.android.internal.protolog.common.ProtoLog; 66 import com.android.wm.shell.R; 67 import com.android.wm.shell.animation.Interpolators; 68 import com.android.wm.shell.common.ShellExecutor; 69 import com.android.wm.shell.common.pip.PipUiEventLogger; 70 import com.android.wm.shell.common.pip.PipUtils; 71 import com.android.wm.shell.protolog.ShellProtoLogGroup; 72 import com.android.wm.shell.splitscreen.SplitScreenController; 73 74 import java.lang.annotation.Retention; 75 import java.lang.annotation.RetentionPolicy; 76 import java.util.ArrayList; 77 import java.util.List; 78 import java.util.Objects; 79 import java.util.Optional; 80 81 /** 82 * Translucent window that gets started on top of a task in PIP to allow the user to control it. 83 */ 84 public class PipMenuView extends FrameLayout { 85 86 private static final String TAG = "PipMenuView"; 87 88 private static final int ANIMATION_NONE_DURATION_MS = 0; 89 private static final int ANIMATION_HIDE_DURATION_MS = 125; 90 91 /** No animation performed during menu hide. */ 92 public static final int ANIM_TYPE_NONE = 0; 93 /** Fade out the menu until it's invisible. Used when the PIP window remains visible. */ 94 public static final int ANIM_TYPE_HIDE = 1; 95 /** Fade out the menu in sync with the PIP window. */ 96 public static final int ANIM_TYPE_DISMISS = 2; 97 98 @IntDef(prefix = { "ANIM_TYPE_" }, value = { 99 ANIM_TYPE_NONE, 100 ANIM_TYPE_HIDE, 101 ANIM_TYPE_DISMISS 102 }) 103 @Retention(RetentionPolicy.SOURCE) 104 public @interface AnimationType {} 105 106 private static final int INITIAL_DISMISS_DELAY = 3500; 107 private static final int POST_INTERACTION_DISMISS_DELAY = 2000; 108 private static final long MENU_SHOW_ON_EXPAND_START_DELAY = 30; 109 110 private static final float MENU_BACKGROUND_ALPHA = 0.3f; 111 private static final float DISABLED_ACTION_ALPHA = 0.54f; 112 113 private int mMenuState; 114 private boolean mAllowMenuTimeout = true; 115 private boolean mAllowTouches = true; 116 private int mDismissFadeOutDurationMs; 117 private boolean mFocusedTaskAllowSplitScreen; 118 119 private final List<RemoteAction> mActions = new ArrayList<>(); 120 private RemoteAction mCloseAction; 121 122 private AccessibilityManager mAccessibilityManager; 123 private Drawable mBackgroundDrawable; 124 private View mMenuContainer; 125 private LinearLayout mActionsGroup; 126 private int mBetweenActionPaddingLand; 127 128 private AnimatorSet mMenuContainerAnimator; 129 private final PhonePipMenuController mController; 130 private final Optional<SplitScreenController> mSplitScreenControllerOptional; 131 private final PipUiEventLogger mPipUiEventLogger; 132 133 private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener = 134 new ValueAnimator.AnimatorUpdateListener() { 135 @Override 136 public void onAnimationUpdate(ValueAnimator animation) { 137 final float alpha = (float) animation.getAnimatedValue(); 138 mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA * alpha * 255)); 139 } 140 }; 141 142 private ShellExecutor mMainExecutor; 143 private Handler mMainHandler; 144 145 /** 146 * Whether the most recent showing of the menu caused a PIP resize, such as when PIP is too 147 * small and it is resized on menu show to fit the actions. 148 */ 149 private boolean mDidLastShowMenuResize; 150 private final Runnable mHideMenuRunnable = this::hideMenu; 151 152 protected View mViewRoot; 153 protected View mSettingsButton; 154 protected View mDismissButton; 155 protected View mEnterSplitButton; 156 protected View mTopEndContainer; 157 protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm; 158 159 // How long the shell will wait for the app to close the PiP if a custom action is set. 160 private final int mPipForceCloseDelay; 161 PipMenuView(Context context, PhonePipMenuController controller, ShellExecutor mainExecutor, Handler mainHandler, Optional<SplitScreenController> splitScreenController, PipUiEventLogger pipUiEventLogger)162 public PipMenuView(Context context, PhonePipMenuController controller, 163 ShellExecutor mainExecutor, Handler mainHandler, 164 Optional<SplitScreenController> splitScreenController, 165 PipUiEventLogger pipUiEventLogger) { 166 super(context, null, 0); 167 mContext = context; 168 mController = controller; 169 mMainExecutor = mainExecutor; 170 mMainHandler = mainHandler; 171 mSplitScreenControllerOptional = splitScreenController; 172 mPipUiEventLogger = pipUiEventLogger; 173 174 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 175 inflate(context, R.layout.pip_menu, this); 176 177 mPipForceCloseDelay = context.getResources().getInteger( 178 R.integer.config_pipForceCloseDelay); 179 180 mBackgroundDrawable = mContext.getDrawable(R.drawable.pip_menu_background); 181 mBackgroundDrawable.setAlpha(0); 182 mViewRoot = findViewById(R.id.background); 183 mViewRoot.setBackground(mBackgroundDrawable); 184 mMenuContainer = findViewById(R.id.menu_container); 185 mMenuContainer.setAlpha(0); 186 mTopEndContainer = findViewById(R.id.top_end_container); 187 mSettingsButton = findViewById(R.id.settings); 188 mSettingsButton.setAlpha(0); 189 mSettingsButton.setOnClickListener((v) -> { 190 if (v.getAlpha() != 0) { 191 showSettings(); 192 } 193 }); 194 mDismissButton = findViewById(R.id.dismiss); 195 mDismissButton.setAlpha(0); 196 mDismissButton.setOnClickListener(v -> dismissPip()); 197 findViewById(R.id.expand_button).setOnClickListener(v -> { 198 if (mMenuContainer.getAlpha() != 0) { 199 expandPip(); 200 } 201 }); 202 203 mEnterSplitButton = findViewById(R.id.enter_split); 204 mEnterSplitButton.setAlpha(0); 205 mEnterSplitButton.setOnClickListener(v -> { 206 if (mEnterSplitButton.getAlpha() != 0) { 207 enterSplit(); 208 } 209 }); 210 211 // this disables the ripples 212 mEnterSplitButton.setEnabled(false); 213 214 findViewById(R.id.resize_handle).setAlpha(0); 215 216 mActionsGroup = findViewById(R.id.actions_group); 217 mBetweenActionPaddingLand = getResources().getDimensionPixelSize( 218 R.dimen.pip_between_action_padding_land); 219 mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext); 220 mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer, 221 findViewById(R.id.resize_handle), mEnterSplitButton, mSettingsButton, 222 mDismissButton); 223 mDismissFadeOutDurationMs = context.getResources() 224 .getInteger(R.integer.config_pipExitAnimationDuration); 225 226 initAccessibility(); 227 } 228 initAccessibility()229 private void initAccessibility() { 230 this.setAccessibilityDelegate(new View.AccessibilityDelegate() { 231 @Override 232 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 233 super.onInitializeAccessibilityNodeInfo(host, info); 234 String label = getResources().getString(R.string.pip_menu_title); 235 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, label)); 236 } 237 238 @Override 239 public boolean performAccessibilityAction(View host, int action, Bundle args) { 240 if (action == ACTION_CLICK && mMenuState != MENU_STATE_FULL) { 241 mController.showMenu(); 242 } 243 return super.performAccessibilityAction(host, action, args); 244 } 245 }); 246 } 247 248 @Override onKeyUp(int keyCode, KeyEvent event)249 public boolean onKeyUp(int keyCode, KeyEvent event) { 250 if (keyCode == KeyEvent.KEYCODE_ESCAPE) { 251 hideMenu(); 252 return true; 253 } 254 return super.onKeyUp(keyCode, event); 255 } 256 257 @Override shouldDelayChildPressedState()258 public boolean shouldDelayChildPressedState() { 259 return true; 260 } 261 262 @Override dispatchTouchEvent(MotionEvent ev)263 public boolean dispatchTouchEvent(MotionEvent ev) { 264 if (!mAllowTouches) { 265 return false; 266 } 267 268 if (mAllowMenuTimeout) { 269 repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); 270 } 271 272 return super.dispatchTouchEvent(ev); 273 } 274 275 @Override dispatchGenericMotionEvent(MotionEvent event)276 public boolean dispatchGenericMotionEvent(MotionEvent event) { 277 if (mAllowMenuTimeout) { 278 repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); 279 } 280 281 return super.dispatchGenericMotionEvent(event); 282 } 283 onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo)284 public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { 285 final boolean isSplitScreen = mSplitScreenControllerOptional.isPresent() 286 && mSplitScreenControllerOptional.get().isTaskInSplitScreenForeground( 287 taskInfo.taskId); 288 mFocusedTaskAllowSplitScreen = isSplitScreen 289 || (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN 290 && taskInfo.supportsMultiWindow 291 && taskInfo.topActivityType != WindowConfiguration.ACTIVITY_TYPE_HOME); 292 } 293 showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle)294 void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, 295 boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) { 296 mAllowMenuTimeout = allowMenuTimeout; 297 mDidLastShowMenuResize = resizeMenuOnShow; 298 final boolean enableEnterSplit = 299 mContext.getResources().getBoolean(R.bool.config_pipEnableEnterSplitButton); 300 if (mMenuState != menuState) { 301 // Disallow touches if the menu needs to resize while showing, and we are transitioning 302 // to/from a full menu state. 303 boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow 304 && (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL); 305 mAllowTouches = !disallowTouchesUntilAnimationEnd; 306 cancelDelayedHide(); 307 if (mMenuContainerAnimator != null) { 308 mMenuContainerAnimator.cancel(); 309 } 310 mMenuContainerAnimator = new AnimatorSet(); 311 ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA, 312 mMenuContainer.getAlpha(), 1f); 313 menuAnim.addUpdateListener(mMenuBgUpdateListener); 314 ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, 315 mSettingsButton.getAlpha(), 1f); 316 ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, 317 mDismissButton.getAlpha(), 1f); 318 ObjectAnimator enterSplitAnim = ObjectAnimator.ofFloat(mEnterSplitButton, View.ALPHA, 319 mEnterSplitButton.getAlpha(), 320 enableEnterSplit && mFocusedTaskAllowSplitScreen ? 1f : 0f); 321 if (menuState == MENU_STATE_FULL) { 322 mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, 323 enterSplitAnim); 324 } else { 325 mMenuContainerAnimator.playTogether(enterSplitAnim); 326 } 327 mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN); 328 mMenuContainerAnimator.setDuration(ANIMATION_HIDE_DURATION_MS); 329 mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { 330 @Override 331 public void onAnimationEnd(Animator animation) { 332 mAllowTouches = true; 333 notifyMenuStateChangeFinish(menuState); 334 if (allowMenuTimeout) { 335 repostDelayedHide(INITIAL_DISMISS_DELAY); 336 } 337 } 338 339 @Override 340 public void onAnimationCancel(Animator animation) { 341 mAllowTouches = true; 342 } 343 }); 344 if (withDelay) { 345 // starts the menu container animation after window expansion is completed 346 notifyMenuStateChangeStart(menuState, resizeMenuOnShow, () -> { 347 if (mMenuContainerAnimator == null) { 348 return; 349 } 350 mMenuContainerAnimator.setStartDelay(MENU_SHOW_ON_EXPAND_START_DELAY); 351 setVisibility(VISIBLE); 352 mMenuContainerAnimator.start(); 353 }); 354 } else { 355 notifyMenuStateChangeStart(menuState, resizeMenuOnShow, null); 356 setVisibility(VISIBLE); 357 mMenuContainerAnimator.start(); 358 } 359 updateActionViews(menuState, stackBounds); 360 } else { 361 // If we are already visible, then just start the delayed dismiss and unregister any 362 // existing input consumers from the previous drag 363 if (allowMenuTimeout) { 364 repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); 365 } 366 } 367 } 368 369 /** 370 * Different from {@link #hideMenu()}, this function does not try to finish this menu activity 371 * and instead, it fades out the controls by setting the alpha to 0 directly without menu 372 * visibility callbacks invoked. 373 */ fadeOutMenu()374 void fadeOutMenu() { 375 mMenuContainer.setAlpha(0f); 376 mSettingsButton.setAlpha(0f); 377 mDismissButton.setAlpha(0f); 378 mEnterSplitButton.setAlpha(0f); 379 } 380 pokeMenu()381 void pokeMenu() { 382 cancelDelayedHide(); 383 } 384 updateMenuLayout(Rect bounds)385 void updateMenuLayout(Rect bounds) { 386 mPipMenuIconsAlgorithm.onBoundsChanged(bounds); 387 } 388 hideMenu()389 void hideMenu() { 390 hideMenu(null); 391 } 392 hideMenu(Runnable animationEndCallback)393 void hideMenu(Runnable animationEndCallback) { 394 hideMenu(animationEndCallback, true /* notifyMenuVisibility */, mDidLastShowMenuResize, 395 ANIM_TYPE_HIDE); 396 } 397 hideMenu(boolean resize, @AnimationType int animationType)398 void hideMenu(boolean resize, @AnimationType int animationType) { 399 hideMenu(null /* animationFinishedRunnable */, true /* notifyMenuVisibility */, resize, 400 animationType); 401 } 402 hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility, boolean resize, @AnimationType int animationType)403 void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility, 404 boolean resize, @AnimationType int animationType) { 405 if (mMenuState != MENU_STATE_NONE) { 406 cancelDelayedHide(); 407 if (notifyMenuVisibility) { 408 notifyMenuStateChangeStart(MENU_STATE_NONE, resize, null); 409 } 410 mMenuContainerAnimator = new AnimatorSet(); 411 ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA, 412 mMenuContainer.getAlpha(), 0f); 413 menuAnim.addUpdateListener(mMenuBgUpdateListener); 414 ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, 415 mSettingsButton.getAlpha(), 0f); 416 ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, 417 mDismissButton.getAlpha(), 0f); 418 ObjectAnimator enterSplitAnim = ObjectAnimator.ofFloat(mEnterSplitButton, View.ALPHA, 419 mEnterSplitButton.getAlpha(), 0f); 420 mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, 421 enterSplitAnim); 422 mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT); 423 mMenuContainerAnimator.setDuration(getFadeOutDuration(animationType)); 424 mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { 425 @Override 426 public void onAnimationEnd(Animator animation) { 427 setVisibility(GONE); 428 if (notifyMenuVisibility) { 429 notifyMenuStateChangeFinish(MENU_STATE_NONE); 430 } 431 if (animationFinishedRunnable != null) { 432 animationFinishedRunnable.run(); 433 } 434 } 435 }); 436 mMenuContainerAnimator.start(); 437 } 438 } 439 440 /** 441 * @return Estimated minimum {@link Size} to hold the actions. 442 * See also {@link #updateActionViews(Rect)} 443 */ getEstimatedMinMenuSize()444 Size getEstimatedMinMenuSize() { 445 final int pipActionSize = getResources().getDimensionPixelSize(R.dimen.pip_action_size); 446 // the minimum width would be (2 * pipActionSize) since we have settings and dismiss button 447 // on the top action container. 448 final int width = Math.max(2, mActions.size()) * pipActionSize; 449 final int height = getResources().getDimensionPixelSize(R.dimen.pip_expand_action_size) 450 + getResources().getDimensionPixelSize(R.dimen.pip_action_padding) 451 + getResources().getDimensionPixelSize(R.dimen.pip_expand_container_edge_margin); 452 return new Size(width, height); 453 } 454 setActions(Rect stackBounds, @Nullable List<RemoteAction> actions, @Nullable RemoteAction closeAction)455 void setActions(Rect stackBounds, @Nullable List<RemoteAction> actions, 456 @Nullable RemoteAction closeAction) { 457 mActions.clear(); 458 if (actions != null && !actions.isEmpty()) { 459 mActions.addAll(actions); 460 } 461 mCloseAction = closeAction; 462 if (mMenuState == MENU_STATE_FULL) { 463 updateActionViews(mMenuState, stackBounds); 464 } 465 } 466 updateActionViews(int menuState, Rect stackBounds)467 private void updateActionViews(int menuState, Rect stackBounds) { 468 ViewGroup expandContainer = findViewById(R.id.expand_container); 469 ViewGroup actionsContainer = findViewById(R.id.actions_container); 470 actionsContainer.setOnTouchListener((v, ev) -> { 471 // Do nothing, prevent click through to parent 472 return true; 473 }); 474 475 // Update the expand button only if it should show with the menu 476 expandContainer.setVisibility(menuState == MENU_STATE_FULL 477 ? View.VISIBLE 478 : View.INVISIBLE); 479 480 FrameLayout.LayoutParams expandedLp = 481 (FrameLayout.LayoutParams) expandContainer.getLayoutParams(); 482 if (mActions.isEmpty() || menuState == MENU_STATE_NONE) { 483 actionsContainer.setVisibility(View.INVISIBLE); 484 485 // Update the expand container margin to adjust the center of the expand button to 486 // account for the existence of the action container 487 expandedLp.topMargin = 0; 488 expandedLp.bottomMargin = 0; 489 } else { 490 actionsContainer.setVisibility(View.VISIBLE); 491 if (mActionsGroup != null) { 492 // Ensure we have as many buttons as actions 493 final LayoutInflater inflater = LayoutInflater.from(mContext); 494 while (mActionsGroup.getChildCount() < mActions.size()) { 495 final PipMenuActionView actionView = (PipMenuActionView) inflater.inflate( 496 R.layout.pip_menu_action, mActionsGroup, false); 497 mActionsGroup.addView(actionView); 498 } 499 500 // Update the visibility of all views 501 for (int i = 0; i < mActionsGroup.getChildCount(); i++) { 502 mActionsGroup.getChildAt(i).setVisibility(i < mActions.size() 503 ? View.VISIBLE 504 : View.GONE); 505 } 506 507 // Recreate the layout 508 final boolean isLandscapePip = stackBounds != null 509 && (stackBounds.width() > stackBounds.height()); 510 for (int i = 0; i < mActions.size(); i++) { 511 final RemoteAction action = mActions.get(i); 512 final PipMenuActionView actionView = 513 (PipMenuActionView) mActionsGroup.getChildAt(i); 514 final boolean isCloseAction = mCloseAction != null && Objects.equals( 515 mCloseAction.getActionIntent(), action.getActionIntent()); 516 517 final int iconType = action.getIcon().getType(); 518 if (iconType == Icon.TYPE_URI || iconType == Icon.TYPE_URI_ADAPTIVE_BITMAP) { 519 // Disallow loading icon from content URI 520 actionView.setImageDrawable(null); 521 } else { 522 // TODO: Check if the action drawable has changed before we reload it 523 action.getIcon().loadDrawableAsync(mContext, d -> { 524 if (d != null) { 525 d.setTint(Color.WHITE); 526 actionView.setImageDrawable(d); 527 } 528 }, mMainHandler); 529 } 530 actionView.setCustomCloseBackgroundVisibility( 531 isCloseAction ? View.VISIBLE : View.GONE); 532 actionView.setContentDescription(action.getContentDescription()); 533 if (action.isEnabled()) { 534 actionView.setOnClickListener( 535 v -> onActionViewClicked(action.getActionIntent(), isCloseAction)); 536 } 537 actionView.setEnabled(action.isEnabled()); 538 actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); 539 540 // Update the margin between actions 541 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) 542 actionView.getLayoutParams(); 543 lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0; 544 } 545 } 546 547 // Update the expand container margin to adjust the center of the expand button to 548 // account for the existence of the action container 549 expandedLp.topMargin = getResources().getDimensionPixelSize( 550 R.dimen.pip_action_padding); 551 expandedLp.bottomMargin = getResources().getDimensionPixelSize( 552 R.dimen.pip_expand_container_edge_margin); 553 } 554 expandContainer.requestLayout(); 555 } 556 notifyMenuStateChangeStart(int menuState, boolean resize, Runnable callback)557 private void notifyMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { 558 mController.onMenuStateChangeStart(menuState, resize, callback); 559 } 560 notifyMenuStateChangeFinish(int menuState)561 private void notifyMenuStateChangeFinish(int menuState) { 562 mMenuState = menuState; 563 mController.onMenuStateChangeFinish(menuState); 564 } 565 expandPip()566 private void expandPip() { 567 // Do not notify menu visibility when hiding the menu, the controller will do this when it 568 // handles the message 569 hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* resize */, 570 ANIM_TYPE_HIDE); 571 mPipUiEventLogger.log( 572 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); 573 } 574 dismissPip()575 private void dismissPip() { 576 if (mMenuState != MENU_STATE_NONE) { 577 // Do not call hideMenu() directly. Instead, let the menu controller handle it just as 578 // any other dismissal that will update the touch state and fade out the PIP task 579 // and the menu view at the same time. 580 mController.onPipDismiss(); 581 mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE); 582 } 583 } 584 585 /** 586 * Execute the {@link PendingIntent} attached to the {@link PipMenuActionView}. 587 * If the given {@link PendingIntent} matches {@link #mCloseAction}, we need to make sure 588 * the PiP is removed after a certain timeout in case the app does not respond in a 589 * timely manner. 590 */ onActionViewClicked(@onNull PendingIntent intent, boolean isCloseAction)591 private void onActionViewClicked(@NonNull PendingIntent intent, boolean isCloseAction) { 592 try { 593 intent.send(); 594 } catch (PendingIntent.CanceledException e) { 595 ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 596 "%s: Failed to send action, %s", TAG, e); 597 } 598 if (isCloseAction) { 599 mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_CUSTOM_CLOSE); 600 mAllowTouches = false; 601 mMainExecutor.executeDelayed(() -> { 602 hideMenu(); 603 // TODO: it's unsafe to call onPipDismiss with a delay here since 604 // we may have a different PiP by the time this runnable is executed. 605 mController.onPipDismiss(); 606 mAllowTouches = true; 607 }, mPipForceCloseDelay); 608 } 609 } 610 enterSplit()611 private void enterSplit() { 612 // Do not notify menu visibility when hiding the menu, the controller will do this when it 613 // handles the message 614 hideMenu(mController::onEnterSplit, false /* notifyMenuVisibility */, true /* resize */, 615 ANIM_TYPE_HIDE); 616 } 617 618 showSettings()619 private void showSettings() { 620 final Pair<ComponentName, Integer> topPipActivityInfo = 621 PipUtils.getTopPipActivity(mContext); 622 if (topPipActivityInfo.first != null) { 623 final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS, 624 Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null)); 625 settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); 626 mContext.startActivityAsUser(settingsIntent, UserHandle.of(topPipActivityInfo.second)); 627 mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_SETTINGS); 628 } 629 } 630 cancelDelayedHide()631 private void cancelDelayedHide() { 632 mMainExecutor.removeCallbacks(mHideMenuRunnable); 633 } 634 repostDelayedHide(int delay)635 private void repostDelayedHide(int delay) { 636 int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay, 637 FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS); 638 mMainExecutor.removeCallbacks(mHideMenuRunnable); 639 mMainExecutor.executeDelayed(mHideMenuRunnable, recommendedTimeout); 640 } 641 getFadeOutDuration(@nimationType int animationType)642 private long getFadeOutDuration(@AnimationType int animationType) { 643 switch (animationType) { 644 case ANIM_TYPE_NONE: 645 return ANIMATION_NONE_DURATION_MS; 646 case ANIM_TYPE_HIDE: 647 return ANIMATION_HIDE_DURATION_MS; 648 case ANIM_TYPE_DISMISS: 649 return mDismissFadeOutDurationMs; 650 default: 651 throw new IllegalStateException("Invalid animation type " + animationType); 652 } 653 } 654 } 655