• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
20 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
21 import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS;
22 import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
23 import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS;
24 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
25 
26 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_ACTIONS;
27 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_ALLOW_TIMEOUT;
28 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_CONTROLLER_MESSENGER;
29 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_DISMISS_FRACTION;
30 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_MENU_STATE;
31 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_SHOW_MENU_WITH_DELAY;
32 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_SHOW_RESIZE_HANDLE;
33 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_STACK_BOUNDS;
34 import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_WILL_RESIZE_MENU;
35 import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE;
36 import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL;
37 import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE;
38 
39 import android.animation.Animator;
40 import android.animation.AnimatorListenerAdapter;
41 import android.animation.AnimatorSet;
42 import android.animation.ObjectAnimator;
43 import android.animation.ValueAnimator;
44 import android.annotation.Nullable;
45 import android.app.Activity;
46 import android.app.ActivityManager;
47 import android.app.PendingIntent.CanceledException;
48 import android.app.RemoteAction;
49 import android.content.ComponentName;
50 import android.content.Intent;
51 import android.content.pm.ParceledListSlice;
52 import android.graphics.Color;
53 import android.graphics.Rect;
54 import android.graphics.drawable.ColorDrawable;
55 import android.graphics.drawable.Drawable;
56 import android.net.Uri;
57 import android.os.Bundle;
58 import android.os.Handler;
59 import android.os.Looper;
60 import android.os.Message;
61 import android.os.Messenger;
62 import android.os.RemoteException;
63 import android.os.UserHandle;
64 import android.util.Log;
65 import android.util.Pair;
66 import android.view.KeyEvent;
67 import android.view.LayoutInflater;
68 import android.view.MotionEvent;
69 import android.view.View;
70 import android.view.ViewGroup;
71 import android.view.WindowManager.LayoutParams;
72 import android.view.accessibility.AccessibilityManager;
73 import android.view.accessibility.AccessibilityNodeInfo;
74 import android.widget.FrameLayout;
75 import android.widget.ImageButton;
76 import android.widget.LinearLayout;
77 
78 import com.android.systemui.Interpolators;
79 import com.android.systemui.R;
80 
81 import java.util.ArrayList;
82 import java.util.Collections;
83 import java.util.List;
84 
85 /**
86  * Translucent activity that gets started on top of a task in PIP to allow the user to control it.
87  */
88 public class PipMenuActivity extends Activity {
89 
90     private static final String TAG = "PipMenuActivity";
91 
92     private static final int MESSAGE_INVALID_TYPE = -1;
93 
94     public static final int MESSAGE_SHOW_MENU = 1;
95     public static final int MESSAGE_POKE_MENU = 2;
96     public static final int MESSAGE_HIDE_MENU = 3;
97     public static final int MESSAGE_UPDATE_ACTIONS = 4;
98     public static final int MESSAGE_UPDATE_DISMISS_FRACTION = 5;
99     public static final int MESSAGE_ANIMATION_ENDED = 6;
100     public static final int MESSAGE_POINTER_EVENT = 7;
101     public static final int MESSAGE_MENU_EXPANDED = 8;
102     public static final int MESSAGE_FADE_OUT_MENU = 9;
103 
104     private static final int INITIAL_DISMISS_DELAY = 3500;
105     private static final int POST_INTERACTION_DISMISS_DELAY = 2000;
106     private static final long MENU_FADE_DURATION = 125;
107     private static final long MENU_SLOW_FADE_DURATION = 175;
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 DISMISS_BACKGROUND_ALPHA = 0.6f;
112 
113     private static final float DISABLED_ACTION_ALPHA = 0.54f;
114 
115     private static final boolean ENABLE_RESIZE_HANDLE = false;
116 
117     private int mMenuState;
118     private boolean mResize = true;
119     private boolean mAllowMenuTimeout = true;
120     private boolean mAllowTouches = true;
121 
122     private final List<RemoteAction> mActions = new ArrayList<>();
123 
124     private AccessibilityManager mAccessibilityManager;
125     private View mViewRoot;
126     private Drawable mBackgroundDrawable;
127     private View mMenuContainer;
128     private LinearLayout mActionsGroup;
129     private View mSettingsButton;
130     private View mDismissButton;
131     private View mResizeHandle;
132     private int mBetweenActionPaddingLand;
133 
134     private AnimatorSet mMenuContainerAnimator;
135 
136     private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener =
137             new ValueAnimator.AnimatorUpdateListener() {
138                 @Override
139                 public void onAnimationUpdate(ValueAnimator animation) {
140                     final float alpha = (float) animation.getAnimatedValue();
141                     mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA*alpha*255));
142                 }
143             };
144 
145     private Handler mHandler = new Handler(Looper.getMainLooper()) {
146         @Override
147         public void handleMessage(Message msg) {
148             switch (msg.what) {
149                 case MESSAGE_SHOW_MENU: {
150                     final Bundle data = (Bundle) msg.obj;
151                     showMenu(data.getInt(EXTRA_MENU_STATE),
152                             data.getParcelable(EXTRA_STACK_BOUNDS),
153                             data.getBoolean(EXTRA_ALLOW_TIMEOUT),
154                             data.getBoolean(EXTRA_WILL_RESIZE_MENU),
155                             data.getBoolean(EXTRA_SHOW_MENU_WITH_DELAY),
156                             data.getBoolean(EXTRA_SHOW_RESIZE_HANDLE));
157                     break;
158                 }
159                 case MESSAGE_POKE_MENU:
160                     cancelDelayedFinish();
161                     break;
162                 case MESSAGE_HIDE_MENU:
163                     hideMenu((Runnable) msg.obj);
164                     break;
165                 case MESSAGE_UPDATE_ACTIONS: {
166                     final Bundle data = (Bundle) msg.obj;
167                     final ParceledListSlice actions = data.getParcelable(EXTRA_ACTIONS);
168                     setActions(data.getParcelable(EXTRA_STACK_BOUNDS), actions != null
169                             ? actions.getList() : Collections.EMPTY_LIST);
170                     break;
171                 }
172                 case MESSAGE_UPDATE_DISMISS_FRACTION: {
173                     final Bundle data = (Bundle) msg.obj;
174                     updateDismissFraction(data.getFloat(EXTRA_DISMISS_FRACTION));
175                     break;
176                 }
177                 case MESSAGE_ANIMATION_ENDED: {
178                     mAllowTouches = true;
179                     break;
180                 }
181                 case MESSAGE_POINTER_EVENT: {
182                     final MotionEvent ev = (MotionEvent) msg.obj;
183                     dispatchPointerEvent(ev);
184                     break;
185                 }
186                 case MESSAGE_MENU_EXPANDED : {
187                     mMenuContainerAnimator.setStartDelay(MENU_SHOW_ON_EXPAND_START_DELAY);
188                     mMenuContainerAnimator.start();
189                     break;
190                 }
191                 case MESSAGE_FADE_OUT_MENU: {
192                     fadeOutMenu();
193                     break;
194                 }
195             }
196         }
197     };
198     private Messenger mToControllerMessenger;
199     private Messenger mMessenger = new Messenger(mHandler);
200 
201     private final Runnable mFinishRunnable = this::hideMenu;
202 
203     @Override
onCreate(@ullable Bundle savedInstanceState)204     protected void onCreate(@Nullable Bundle savedInstanceState) {
205         // Set the flags to allow us to watch for outside touches and also hide the menu and start
206         // manipulating the PIP in the same touch gesture
207         getWindow().addFlags(LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
208 
209         super.onCreate(savedInstanceState);
210         setContentView(R.layout.pip_menu_activity);
211 
212         mAccessibilityManager = getSystemService(AccessibilityManager.class);
213         mBackgroundDrawable = new ColorDrawable(Color.BLACK);
214         mBackgroundDrawable.setAlpha(0);
215         mViewRoot = findViewById(R.id.background);
216         mViewRoot.setBackground(mBackgroundDrawable);
217         mMenuContainer = findViewById(R.id.menu_container);
218         mMenuContainer.setAlpha(0);
219         mSettingsButton = findViewById(R.id.settings);
220         mSettingsButton.setAlpha(0);
221         mSettingsButton.setOnClickListener((v) -> {
222             if (v.getAlpha() != 0) {
223                 showSettings();
224             }
225         });
226         mDismissButton = findViewById(R.id.dismiss);
227         mDismissButton.setAlpha(0);
228         mDismissButton.setOnClickListener(v -> dismissPip());
229         findViewById(R.id.expand_button).setOnClickListener(v -> {
230             if (mMenuContainer.getAlpha() != 0) {
231                 expandPip();
232             }
233         });
234         mResizeHandle = findViewById(R.id.resize_handle);
235         mResizeHandle.setAlpha(0);
236         mActionsGroup = findViewById(R.id.actions_group);
237         mBetweenActionPaddingLand = getResources().getDimensionPixelSize(
238                 R.dimen.pip_between_action_padding_land);
239 
240         updateFromIntent(getIntent());
241         setTitle(R.string.pip_menu_title);
242         setDisablePreviewScreenshots(true);
243 
244         // Hide without an animation.
245         getWindow().setExitTransition(null);
246 
247         initAccessibility();
248     }
249 
initAccessibility()250     private void initAccessibility() {
251         getWindow().getDecorView().setAccessibilityDelegate(new View.AccessibilityDelegate() {
252             @Override
253             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
254                 super.onInitializeAccessibilityNodeInfo(host, info);
255                 String label = getResources().getString(R.string.pip_menu_title);
256                 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, label));
257             }
258 
259             @Override
260             public boolean performAccessibilityAction(View host, int action, Bundle args) {
261                 if (action == ACTION_CLICK && mMenuState == MENU_STATE_CLOSE) {
262                     Message m = Message.obtain();
263                     m.what = PipMenuActivityController.MESSAGE_SHOW_MENU;
264                     sendMessage(m, "Could not notify controller to show PIP menu");
265                 }
266                 return super.performAccessibilityAction(host, action, args);
267             }
268         });
269     }
270 
271     @Override
onKeyUp(int keyCode, KeyEvent event)272     public boolean onKeyUp(int keyCode, KeyEvent event) {
273         if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
274             hideMenu();
275             return true;
276         }
277         return super.onKeyUp(keyCode, event);
278     }
279 
280     @Override
onNewIntent(Intent intent)281     protected void onNewIntent(Intent intent) {
282         super.onNewIntent(intent);
283         updateFromIntent(intent);
284     }
285 
286     @Override
onUserInteraction()287     public void onUserInteraction() {
288         if (mAllowMenuTimeout) {
289             repostDelayedFinish(POST_INTERACTION_DISMISS_DELAY);
290         }
291     }
292 
293     @Override
onUserLeaveHint()294     protected void onUserLeaveHint() {
295         super.onUserLeaveHint();
296 
297         // If another task is starting on top of the menu, then hide and finish it so that it can be
298         // recreated on the top next time it starts
299         hideMenu();
300     }
301 
302     @Override
onTopResumedActivityChanged(boolean isTopResumedActivity)303     public void onTopResumedActivityChanged(boolean isTopResumedActivity) {
304         super.onTopResumedActivityChanged(isTopResumedActivity);
305         if (!isTopResumedActivity && mMenuState != MENU_STATE_NONE) {
306             hideMenu();
307         }
308     }
309 
310     @Override
onStop()311     protected void onStop() {
312         super.onStop();
313 
314         // In cases such as device lock, hide and finish it so that it can be recreated on the top
315         // next time it starts, see also {@link #onUserLeaveHint}
316         hideMenu();
317         cancelDelayedFinish();
318     }
319 
320     @Override
onDestroy()321     protected void onDestroy() {
322         super.onDestroy();
323 
324         // Fallback, if we are destroyed for any other reason (like when the task is being reset),
325         // also reset the callback.
326         notifyActivityCallback(null);
327     }
328 
329     @Override
onPictureInPictureModeChanged(boolean isInPictureInPictureMode)330     public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
331         if (!isInPictureInPictureMode) {
332             finish();
333         }
334     }
335 
336     /**
337      * Dispatch a pointer event from {@link PipTouchHandler}.
338      */
dispatchPointerEvent(MotionEvent event)339     private void dispatchPointerEvent(MotionEvent event) {
340         if (event.isTouchEvent()) {
341             dispatchTouchEvent(event);
342         } else {
343             dispatchGenericMotionEvent(event);
344         }
345     }
346 
347     @Override
dispatchTouchEvent(MotionEvent ev)348     public boolean dispatchTouchEvent(MotionEvent ev) {
349         if (!mAllowTouches) {
350             return false;
351         }
352 
353         // On the first action outside the window, hide the menu
354         switch (ev.getAction()) {
355             case MotionEvent.ACTION_OUTSIDE:
356                 hideMenu();
357                 return true;
358         }
359         return super.dispatchTouchEvent(ev);
360     }
361 
362     @Override
finish()363     public void finish() {
364         notifyActivityCallback(null);
365         super.finish();
366     }
367 
368     @Override
setTaskDescription(ActivityManager.TaskDescription taskDescription)369     public void setTaskDescription(ActivityManager.TaskDescription taskDescription) {
370         // Do nothing
371     }
372 
showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle)373     private void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
374             boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) {
375         mAllowMenuTimeout = allowMenuTimeout;
376         if (mMenuState != menuState) {
377             // Disallow touches if the menu needs to resize while showing, and we are transitioning
378             // to/from a full menu state.
379             boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow &&
380                     (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL);
381             mAllowTouches = !disallowTouchesUntilAnimationEnd;
382             cancelDelayedFinish();
383             updateActionViews(stackBounds);
384             if (mMenuContainerAnimator != null) {
385                 mMenuContainerAnimator.cancel();
386             }
387             mMenuContainerAnimator = new AnimatorSet();
388             ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
389                     mMenuContainer.getAlpha(), 1f);
390             menuAnim.addUpdateListener(mMenuBgUpdateListener);
391             ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
392                     mSettingsButton.getAlpha(), 1f);
393             ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
394                     mDismissButton.getAlpha(), 1f);
395             ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA,
396                     mResizeHandle.getAlpha(),
397                     ENABLE_RESIZE_HANDLE && menuState == MENU_STATE_CLOSE && showResizeHandle
398                             ? 1f : 0f);
399             if (menuState == MENU_STATE_FULL) {
400                 mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim,
401                         resizeAnim);
402             } else {
403                 mMenuContainerAnimator.playTogether(dismissAnim, resizeAnim);
404             }
405             mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN);
406             mMenuContainerAnimator.setDuration(menuState == MENU_STATE_CLOSE
407                     ? MENU_FADE_DURATION
408                     : MENU_SLOW_FADE_DURATION);
409             if (allowMenuTimeout) {
410                 mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
411                     @Override
412                     public void onAnimationEnd(Animator animation) {
413                         repostDelayedFinish(INITIAL_DISMISS_DELAY);
414                     }
415                 });
416             }
417             if (withDelay) {
418                 // starts the menu container animation after window expansion is completed
419                 notifyMenuStateChange(menuState, resizeMenuOnShow, MESSAGE_MENU_EXPANDED);
420             } else {
421                 notifyMenuStateChange(menuState, resizeMenuOnShow, MESSAGE_INVALID_TYPE);
422                 mMenuContainerAnimator.start();
423             }
424         } else {
425             // If we are already visible, then just start the delayed dismiss and unregister any
426             // existing input consumers from the previous drag
427             if (allowMenuTimeout) {
428                 repostDelayedFinish(POST_INTERACTION_DISMISS_DELAY);
429             }
430         }
431     }
432 
433     /**
434      * Different from {@link #hideMenu()}, this function does not try to finish this menu activity
435      * and instead, it fades out the controls by setting the alpha to 0 directly without menu
436      * visibility callbacks invoked.
437      */
fadeOutMenu()438     private void fadeOutMenu() {
439         mMenuContainer.setAlpha(0f);
440         mSettingsButton.setAlpha(0f);
441         mDismissButton.setAlpha(0f);
442         mResizeHandle.setAlpha(0f);
443     }
444 
hideMenu()445     private void hideMenu() {
446         hideMenu(null);
447     }
448 
hideMenu(Runnable animationEndCallback)449     private void hideMenu(Runnable animationEndCallback) {
450         hideMenu(animationEndCallback, true /* notifyMenuVisibility */, false /* isDismissing */,
451                 true /* animate */);
452     }
453 
hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility, boolean isDismissing, boolean animate)454     private void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility,
455             boolean isDismissing, boolean animate) {
456         if (mMenuState != MENU_STATE_NONE) {
457             cancelDelayedFinish();
458             if (notifyMenuVisibility) {
459                 notifyMenuStateChange(MENU_STATE_NONE, mResize, MESSAGE_INVALID_TYPE);
460             }
461             mMenuContainerAnimator = new AnimatorSet();
462             ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
463                     mMenuContainer.getAlpha(), 0f);
464             menuAnim.addUpdateListener(mMenuBgUpdateListener);
465             ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
466                     mSettingsButton.getAlpha(), 0f);
467             ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
468                     mDismissButton.getAlpha(), 0f);
469             ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA,
470                     mResizeHandle.getAlpha(), 0f);
471             mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, resizeAnim);
472             mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT);
473             mMenuContainerAnimator.setDuration(animate ? MENU_FADE_DURATION : 0);
474             mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
475                 @Override
476                 public void onAnimationEnd(Animator animation) {
477                     if (animationFinishedRunnable != null) {
478                         animationFinishedRunnable.run();
479                     }
480 
481                     if (!isDismissing) {
482                         // If we are dismissing the PiP, then don't try to pre-emptively finish the
483                         // menu activity
484                         finish();
485                     }
486                 }
487             });
488             mMenuContainerAnimator.start();
489         } else {
490             // If the menu is not visible, just finish now
491             finish();
492         }
493     }
494 
updateFromIntent(Intent intent)495     private void updateFromIntent(Intent intent) {
496         mToControllerMessenger = intent.getParcelableExtra(EXTRA_CONTROLLER_MESSENGER);
497         if (mToControllerMessenger == null) {
498             Log.w(TAG, "Controller messenger is null. Stopping.");
499             finish();
500             return;
501         }
502         notifyActivityCallback(mMessenger);
503 
504         ParceledListSlice actions = intent.getParcelableExtra(EXTRA_ACTIONS);
505         if (actions != null) {
506             mActions.clear();
507             mActions.addAll(actions.getList());
508         }
509 
510         final int menuState = intent.getIntExtra(EXTRA_MENU_STATE, MENU_STATE_NONE);
511         if (menuState != MENU_STATE_NONE) {
512             Rect stackBounds = intent.getParcelableExtra(EXTRA_STACK_BOUNDS);
513             boolean allowMenuTimeout = intent.getBooleanExtra(EXTRA_ALLOW_TIMEOUT, true);
514             boolean willResizeMenu = intent.getBooleanExtra(EXTRA_WILL_RESIZE_MENU, false);
515             boolean withDelay = intent.getBooleanExtra(EXTRA_SHOW_MENU_WITH_DELAY, false);
516             boolean showResizeHandle = intent.getBooleanExtra(EXTRA_SHOW_RESIZE_HANDLE, false);
517             showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay,
518                     showResizeHandle);
519         }
520     }
521 
setActions(Rect stackBounds, List<RemoteAction> actions)522     private void setActions(Rect stackBounds, List<RemoteAction> actions) {
523         mActions.clear();
524         mActions.addAll(actions);
525         updateActionViews(stackBounds);
526     }
527 
updateActionViews(Rect stackBounds)528     private void updateActionViews(Rect stackBounds) {
529         ViewGroup expandContainer = findViewById(R.id.expand_container);
530         ViewGroup actionsContainer = findViewById(R.id.actions_container);
531         actionsContainer.setOnTouchListener((v, ev) -> {
532             // Do nothing, prevent click through to parent
533             return true;
534         });
535 
536         if (mActions.isEmpty() || mMenuState == MENU_STATE_CLOSE) {
537             actionsContainer.setVisibility(View.INVISIBLE);
538         } else {
539             actionsContainer.setVisibility(View.VISIBLE);
540             if (mActionsGroup != null) {
541                 // Ensure we have as many buttons as actions
542                 final LayoutInflater inflater = LayoutInflater.from(this);
543                 while (mActionsGroup.getChildCount() < mActions.size()) {
544                     final ImageButton actionView = (ImageButton) inflater.inflate(
545                             R.layout.pip_menu_action, mActionsGroup, false);
546                     mActionsGroup.addView(actionView);
547                 }
548 
549                 // Update the visibility of all views
550                 for (int i = 0; i < mActionsGroup.getChildCount(); i++) {
551                     mActionsGroup.getChildAt(i).setVisibility(i < mActions.size()
552                             ? View.VISIBLE
553                             : View.GONE);
554                 }
555 
556                 // Recreate the layout
557                 final boolean isLandscapePip = stackBounds != null &&
558                         (stackBounds.width() > stackBounds.height());
559                 for (int i = 0; i < mActions.size(); i++) {
560                     final RemoteAction action = mActions.get(i);
561                     final ImageButton actionView = (ImageButton) mActionsGroup.getChildAt(i);
562 
563                     // TODO: Check if the action drawable has changed before we reload it
564                     action.getIcon().loadDrawableAsync(this, d -> {
565                         if (d != null) {
566                             d.setTint(Color.WHITE);
567                             actionView.setImageDrawable(d);
568                         }
569                     }, mHandler);
570                     actionView.setContentDescription(action.getContentDescription());
571                     if (action.isEnabled()) {
572                         actionView.setOnClickListener(v -> {
573                             mHandler.post(() -> {
574                                 try {
575                                     action.getActionIntent().send();
576                                 } catch (CanceledException e) {
577                                     Log.w(TAG, "Failed to send action", e);
578                                 }
579                             });
580                         });
581                     }
582                     actionView.setEnabled(action.isEnabled());
583                     actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA);
584 
585                     // Update the margin between actions
586                     LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
587                             actionView.getLayoutParams();
588                     lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0;
589                 }
590             }
591 
592             // Update the expand container margin to adjust the center of the expand button to
593             // account for the existence of the action container
594             FrameLayout.LayoutParams expandedLp =
595                     (FrameLayout.LayoutParams) expandContainer.getLayoutParams();
596             expandedLp.topMargin = getResources().getDimensionPixelSize(
597                     R.dimen.pip_action_padding);
598             expandedLp.bottomMargin = getResources().getDimensionPixelSize(
599                     R.dimen.pip_expand_container_edge_margin);
600             expandContainer.requestLayout();
601         }
602     }
603 
updateDismissFraction(float fraction)604     private void updateDismissFraction(float fraction) {
605         int alpha;
606         final float menuAlpha = 1 - fraction;
607         if (mMenuState == MENU_STATE_FULL) {
608             mMenuContainer.setAlpha(menuAlpha);
609             mSettingsButton.setAlpha(menuAlpha);
610             mDismissButton.setAlpha(menuAlpha);
611             final float interpolatedAlpha =
612                     MENU_BACKGROUND_ALPHA * menuAlpha + DISMISS_BACKGROUND_ALPHA * fraction;
613             alpha = (int) (interpolatedAlpha * 255);
614         } else {
615             if (mMenuState == MENU_STATE_CLOSE) {
616                 mDismissButton.setAlpha(menuAlpha);
617             }
618             alpha = (int) (fraction * DISMISS_BACKGROUND_ALPHA * 255);
619         }
620         mBackgroundDrawable.setAlpha(alpha);
621     }
622 
notifyMenuStateChange(int menuState, boolean resize, int callbackWhat)623     private void notifyMenuStateChange(int menuState, boolean resize, int callbackWhat) {
624         mMenuState = menuState;
625         mResize = resize;
626         Message m = Message.obtain();
627         m.what = PipMenuActivityController.MESSAGE_MENU_STATE_CHANGED;
628         m.arg1 = menuState;
629         m.arg2 = resize ? 1 : 0;
630         if (callbackWhat != MESSAGE_INVALID_TYPE) {
631             // This message could be sent across processes when in secondary user.
632             // Make the receiver end sending back via our own Messenger
633             m.replyTo = mMessenger;
634             final Bundle data = new Bundle(1);
635             data.putInt(PipMenuActivityController.EXTRA_MESSAGE_CALLBACK_WHAT, callbackWhat);
636             m.obj = data;
637         }
638         sendMessage(m, "Could not notify controller of PIP menu visibility");
639     }
640 
expandPip()641     private void expandPip() {
642         // Do not notify menu visibility when hiding the menu, the controller will do this when it
643         // handles the message
644         hideMenu(() -> {
645             sendEmptyMessage(PipMenuActivityController.MESSAGE_EXPAND_PIP,
646                     "Could not notify controller to expand PIP");
647         }, false /* notifyMenuVisibility */, false /* isDismissing */, true /* animate */);
648     }
649 
dismissPip()650     private void dismissPip() {
651         // Since tapping on the close-button invokes a double-tap wait callback in PipTouchHandler,
652         // we want to disable animating the fadeout animation of the buttons in order to call on
653         // PipTouchHandler#onPipDismiss fast enough.
654         final boolean animate = mMenuState != MENU_STATE_CLOSE;
655         // Do not notify menu visibility when hiding the menu, the controller will do this when it
656         // handles the message
657         hideMenu(() -> {
658             sendEmptyMessage(PipMenuActivityController.MESSAGE_DISMISS_PIP,
659                     "Could not notify controller to dismiss PIP");
660         }, false /* notifyMenuVisibility */, true /* isDismissing */, animate);
661     }
662 
showSettings()663     private void showSettings() {
664         final Pair<ComponentName, Integer> topPipActivityInfo =
665                 PipUtils.getTopPipActivity(this, ActivityManager.getService());
666         if (topPipActivityInfo.first != null) {
667             final UserHandle user = UserHandle.of(topPipActivityInfo.second);
668             final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS,
669                     Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null));
670             settingsIntent.putExtra(Intent.EXTRA_USER_HANDLE, user);
671             settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
672             startActivity(settingsIntent);
673         }
674     }
675 
notifyActivityCallback(Messenger callback)676     private void notifyActivityCallback(Messenger callback) {
677         Message m = Message.obtain();
678         m.what = PipMenuActivityController.MESSAGE_UPDATE_ACTIVITY_CALLBACK;
679         m.replyTo = callback;
680         m.arg1 = mResize ?  1 : 0;
681         sendMessage(m, "Could not notify controller of activity finished");
682     }
683 
sendEmptyMessage(int what, String errorMsg)684     private void sendEmptyMessage(int what, String errorMsg) {
685         Message m = Message.obtain();
686         m.what = what;
687         sendMessage(m, errorMsg);
688     }
689 
sendMessage(Message m, String errorMsg)690     private void sendMessage(Message m, String errorMsg) {
691         if (mToControllerMessenger == null) {
692             return;
693         }
694         try {
695             mToControllerMessenger.send(m);
696         } catch (RemoteException e) {
697             Log.e(TAG, errorMsg, e);
698         }
699     }
700 
cancelDelayedFinish()701     private void cancelDelayedFinish() {
702         mHandler.removeCallbacks(mFinishRunnable);
703     }
704 
repostDelayedFinish(int delay)705     private void repostDelayedFinish(int delay) {
706         int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
707                 FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS);
708         mHandler.removeCallbacks(mFinishRunnable);
709         mHandler.postDelayed(mFinishRunnable, recommendedTimeout);
710     }
711 }
712