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