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