blob: ec15dd16f46e9dc744a40ce24f03173c74b9a142 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.systemui.pip.phone;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS;
import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS;
import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_ACTIONS;
import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_ALLOW_TIMEOUT;
import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_CONTROLLER_MESSENGER;
import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_DISMISS_FRACTION;
import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_MENU_STATE;
import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_MOVEMENT_BOUNDS;
import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_STACK_BOUNDS;
import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_WILL_RESIZE_MENU;
import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE;
import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL;
import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.PendingIntent.CanceledException;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Translucent activity that gets started on top of a task in PIP to allow the user to control it.
*/
public class PipMenuActivity extends Activity {
private static final String TAG = "PipMenuActivity";
public static final int MESSAGE_SHOW_MENU = 1;
public static final int MESSAGE_POKE_MENU = 2;
public static final int MESSAGE_HIDE_MENU = 3;
public static final int MESSAGE_UPDATE_ACTIONS = 4;
public static final int MESSAGE_UPDATE_DISMISS_FRACTION = 5;
public static final int MESSAGE_ANIMATION_ENDED = 6;
public static final int MESSAGE_POINTER_EVENT = 7;
private static final int INITIAL_DISMISS_DELAY = 3500;
private static final int POST_INTERACTION_DISMISS_DELAY = 2000;
private static final long MENU_FADE_DURATION = 125;
private static final float MENU_BACKGROUND_ALPHA = 0.3f;
private static final float DISMISS_BACKGROUND_ALPHA = 0.6f;
private static final float DISABLED_ACTION_ALPHA = 0.54f;
private int mMenuState;
private boolean mResize = true;
private boolean mAllowMenuTimeout = true;
private boolean mAllowTouches = true;
private final List<RemoteAction> mActions = new ArrayList<>();
private AccessibilityManager mAccessibilityManager;
private View mViewRoot;
private Drawable mBackgroundDrawable;
private View mMenuContainer;
private LinearLayout mActionsGroup;
private View mSettingsButton;
private View mDismissButton;
private int mBetweenActionPaddingLand;
private AnimatorSet mMenuContainerAnimator;
private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener =
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
final float alpha = (float) animation.getAnimatedValue();
mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA*alpha*255));
}
};
private Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SHOW_MENU: {
final Bundle data = (Bundle) msg.obj;
showMenu(data.getInt(EXTRA_MENU_STATE),
data.getParcelable(EXTRA_STACK_BOUNDS),
data.getParcelable(EXTRA_MOVEMENT_BOUNDS),
data.getBoolean(EXTRA_ALLOW_TIMEOUT),
data.getBoolean(EXTRA_WILL_RESIZE_MENU));
break;
}
case MESSAGE_POKE_MENU:
cancelDelayedFinish();
break;
case MESSAGE_HIDE_MENU:
hideMenu((Runnable) msg.obj);
break;
case MESSAGE_UPDATE_ACTIONS: {
final Bundle data = (Bundle) msg.obj;
final ParceledListSlice actions = data.getParcelable(EXTRA_ACTIONS);
setActions(data.getParcelable(EXTRA_STACK_BOUNDS), actions != null
? actions.getList() : Collections.EMPTY_LIST);
break;
}
case MESSAGE_UPDATE_DISMISS_FRACTION: {
final Bundle data = (Bundle) msg.obj;
updateDismissFraction(data.getFloat(EXTRA_DISMISS_FRACTION));
break;
}
case MESSAGE_ANIMATION_ENDED: {
mAllowTouches = true;
break;
}
case MESSAGE_POINTER_EVENT: {
final MotionEvent ev = (MotionEvent) msg.obj;
dispatchPointerEvent(ev);
break;
}
}
}
};
private Messenger mToControllerMessenger;
private Messenger mMessenger = new Messenger(mHandler);
private final Runnable mFinishRunnable = new Runnable() {
@Override
public void run() {
hideMenu();
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// Set the flags to allow us to watch for outside touches and also hide the menu and start
// manipulating the PIP in the same touch gesture
getWindow().addFlags(LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
super.onCreate(savedInstanceState);
setContentView(R.layout.pip_menu_activity);
mAccessibilityManager = getSystemService(AccessibilityManager.class);
mBackgroundDrawable = new ColorDrawable(Color.BLACK);
mBackgroundDrawable.setAlpha(0);
mViewRoot = findViewById(R.id.background);
mViewRoot.setBackground(mBackgroundDrawable);
mMenuContainer = findViewById(R.id.menu_container);
mMenuContainer.setAlpha(0);
mSettingsButton = findViewById(R.id.settings);
mSettingsButton.setAlpha(0);
mSettingsButton.setOnClickListener((v) -> {
if (v.getAlpha() != 0) {
showSettings();
}
});
mDismissButton = findViewById(R.id.dismiss);
mDismissButton.setAlpha(0);
mDismissButton.setOnClickListener(v -> dismissPip());
findViewById(R.id.expand_button).setOnClickListener(v -> {
if (mMenuContainer.getAlpha() != 0) {
expandPip();
}
});
mActionsGroup = findViewById(R.id.actions_group);
mBetweenActionPaddingLand = getResources().getDimensionPixelSize(
R.dimen.pip_between_action_padding_land);
updateFromIntent(getIntent());
setTitle(R.string.pip_menu_title);
setDisablePreviewScreenshots(true);
// Hide without an animation.
getWindow().setExitTransition(null);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
hideMenu();
return true;
}
return super.onKeyUp(keyCode, event);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
updateFromIntent(intent);
}
@Override
public void onUserInteraction() {
if (mAllowMenuTimeout) {
repostDelayedFinish(POST_INTERACTION_DISMISS_DELAY);
}
}
@Override
protected void onUserLeaveHint() {
super.onUserLeaveHint();
// If another task is starting on top of the menu, then hide and finish it so that it can be
// recreated on the top next time it starts
hideMenu();
}
@Override
protected void onStop() {
super.onStop();
// In cases such as device lock, hide and finish it so that it can be recreated on the top
// next time it starts, see also {@link #onUserLeaveHint}
hideMenu();
cancelDelayedFinish();
}
@Override
protected void onDestroy() {
super.onDestroy();
// Fallback, if we are destroyed for any other reason (like when the task is being reset),
// also reset the callback.
notifyActivityCallback(null);
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
if (!isInPictureInPictureMode) {
finish();
}
}
/**
* Dispatch a pointer event from {@link PipTouchHandler}.
*/
private void dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
dispatchTouchEvent(event);
} else {
dispatchGenericMotionEvent(event);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!mAllowTouches) {
return false;
}
// On the first action outside the window, hide the menu
switch (ev.getAction()) {
case MotionEvent.ACTION_OUTSIDE:
hideMenu();
return true;
}
return super.dispatchTouchEvent(ev);
}
@Override
public void finish() {
notifyActivityCallback(null);
super.finish();
}
@Override
public void setTaskDescription(ActivityManager.TaskDescription taskDescription) {
// Do nothing
}
private void showMenu(int menuState, Rect stackBounds, Rect movementBounds,
boolean allowMenuTimeout, boolean resizeMenuOnShow) {
mAllowMenuTimeout = allowMenuTimeout;
if (mMenuState != menuState) {
// Disallow touches if the menu needs to resize while showing, and we are transitioning
// to/from a full menu state.
boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow &&
(mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL);
mAllowTouches = !disallowTouchesUntilAnimationEnd;
cancelDelayedFinish();
updateActionViews(stackBounds);
if (mMenuContainerAnimator != null) {
mMenuContainerAnimator.cancel();
}
notifyMenuStateChange(menuState, resizeMenuOnShow);
mMenuContainerAnimator = new AnimatorSet();
ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
mMenuContainer.getAlpha(), 1f);
menuAnim.addUpdateListener(mMenuBgUpdateListener);
ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
mSettingsButton.getAlpha(), 1f);
ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
mDismissButton.getAlpha(), 1f);
if (menuState == MENU_STATE_FULL) {
mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim);
} else {
mMenuContainerAnimator.playTogether(dismissAnim);
}
mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN);
mMenuContainerAnimator.setDuration(MENU_FADE_DURATION);
if (allowMenuTimeout) {
mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
repostDelayedFinish(INITIAL_DISMISS_DELAY);
}
});
}
mMenuContainerAnimator.start();
} else {
// If we are already visible, then just start the delayed dismiss and unregister any
// existing input consumers from the previous drag
if (allowMenuTimeout) {
repostDelayedFinish(POST_INTERACTION_DISMISS_DELAY);
}
}
}
private void hideMenu() {
hideMenu(null);
}
private void hideMenu(Runnable animationEndCallback) {
hideMenu(animationEndCallback, true /* notifyMenuVisibility */, false /* isDismissing */);
}
private void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility,
boolean isDismissing) {
if (mMenuState != MENU_STATE_NONE) {
cancelDelayedFinish();
if (notifyMenuVisibility) {
notifyMenuStateChange(MENU_STATE_NONE, mResize);
}
mMenuContainerAnimator = new AnimatorSet();
ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
mMenuContainer.getAlpha(), 0f);
menuAnim.addUpdateListener(mMenuBgUpdateListener);
ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
mSettingsButton.getAlpha(), 0f);
ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
mDismissButton.getAlpha(), 0f);
mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim);
mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT);
mMenuContainerAnimator.setDuration(MENU_FADE_DURATION);
mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (animationFinishedRunnable != null) {
animationFinishedRunnable.run();
}
if (!isDismissing) {
// If we are dismissing the PiP, then don't try to pre-emptively finish the
// menu activity
finish();
}
}
});
mMenuContainerAnimator.start();
} else {
// If the menu is not visible, just finish now
finish();
}
}
private void updateFromIntent(Intent intent) {
mToControllerMessenger = intent.getParcelableExtra(EXTRA_CONTROLLER_MESSENGER);
if (mToControllerMessenger == null) {
Log.w(TAG, "Controller messenger is null. Stopping.");
finish();
return;
}
notifyActivityCallback(mMessenger);
ParceledListSlice actions = intent.getParcelableExtra(EXTRA_ACTIONS);
if (actions != null) {
mActions.clear();
mActions.addAll(actions.getList());
}
final int menuState = intent.getIntExtra(EXTRA_MENU_STATE, MENU_STATE_NONE);
if (menuState != MENU_STATE_NONE) {
Rect stackBounds = intent.getParcelableExtra(EXTRA_STACK_BOUNDS);
Rect movementBounds = intent.getParcelableExtra(EXTRA_MOVEMENT_BOUNDS);
boolean allowMenuTimeout = intent.getBooleanExtra(EXTRA_ALLOW_TIMEOUT, true);
boolean willResizeMenu = intent.getBooleanExtra(EXTRA_WILL_RESIZE_MENU, false);
showMenu(menuState, stackBounds, movementBounds, allowMenuTimeout, willResizeMenu);
}
}
private void setActions(Rect stackBounds, List<RemoteAction> actions) {
mActions.clear();
mActions.addAll(actions);
updateActionViews(stackBounds);
}
private void updateActionViews(Rect stackBounds) {
ViewGroup expandContainer = findViewById(R.id.expand_container);
ViewGroup actionsContainer = findViewById(R.id.actions_container);
actionsContainer.setOnTouchListener((v, ev) -> {
// Do nothing, prevent click through to parent
return true;
});
if (mActions.isEmpty() || mMenuState == MENU_STATE_CLOSE) {
actionsContainer.setVisibility(View.INVISIBLE);
} else {
actionsContainer.setVisibility(View.VISIBLE);
if (mActionsGroup != null) {
// Ensure we have as many buttons as actions
final LayoutInflater inflater = LayoutInflater.from(this);
while (mActionsGroup.getChildCount() < mActions.size()) {
final ImageButton actionView = (ImageButton) inflater.inflate(
R.layout.pip_menu_action, mActionsGroup, false);
mActionsGroup.addView(actionView);
}
// Update the visibility of all views
for (int i = 0; i < mActionsGroup.getChildCount(); i++) {
mActionsGroup.getChildAt(i).setVisibility(i < mActions.size()
? View.VISIBLE
: View.GONE);
}
// Recreate the layout
final boolean isLandscapePip = stackBounds != null &&
(stackBounds.width() > stackBounds.height());
for (int i = 0; i < mActions.size(); i++) {
final RemoteAction action = mActions.get(i);
final ImageButton actionView = (ImageButton) mActionsGroup.getChildAt(i);
// TODO: Check if the action drawable has changed before we reload it
action.getIcon().loadDrawableAsync(this, d -> {
d.setTint(Color.WHITE);
actionView.setImageDrawable(d);
}, mHandler);
actionView.setContentDescription(action.getContentDescription());
if (action.isEnabled()) {
actionView.setOnClickListener(v -> {
mHandler.post(() -> {
try {
action.getActionIntent().send();
} catch (CanceledException e) {
Log.w(TAG, "Failed to send action", e);
}
});
});
}
actionView.setEnabled(action.isEnabled());
actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA);
// Update the margin between actions
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
actionView.getLayoutParams();
lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0;
}
}
// Update the expand container margin to adjust the center of the expand button to
// account for the existence of the action container
FrameLayout.LayoutParams expandedLp =
(FrameLayout.LayoutParams) expandContainer.getLayoutParams();
expandedLp.topMargin = getResources().getDimensionPixelSize(
R.dimen.pip_action_padding);
expandedLp.bottomMargin = getResources().getDimensionPixelSize(
R.dimen.pip_expand_container_edge_margin);
expandContainer.requestLayout();
}
}
private void updateDismissFraction(float fraction) {
int alpha;
final float menuAlpha = 1 - fraction;
if (mMenuState == MENU_STATE_FULL) {
mMenuContainer.setAlpha(menuAlpha);
mSettingsButton.setAlpha(menuAlpha);
mDismissButton.setAlpha(menuAlpha);
final float interpolatedAlpha =
MENU_BACKGROUND_ALPHA * menuAlpha + DISMISS_BACKGROUND_ALPHA * fraction;
alpha = (int) (interpolatedAlpha * 255);
} else {
if (mMenuState == MENU_STATE_CLOSE) {
mDismissButton.setAlpha(menuAlpha);
}
alpha = (int) (fraction * DISMISS_BACKGROUND_ALPHA * 255);
}
mBackgroundDrawable.setAlpha(alpha);
}
private void notifyMenuStateChange(int menuState, boolean resize) {
mMenuState = menuState;
mResize = resize;
Message m = Message.obtain();
m.what = PipMenuActivityController.MESSAGE_MENU_STATE_CHANGED;
m.arg1 = menuState;
m.arg2 = resize ? 1 : 0;
sendMessage(m, "Could not notify controller of PIP menu visibility");
}
private void expandPip() {
// Do not notify menu visibility when hiding the menu, the controller will do this when it
// handles the message
hideMenu(() -> {
sendEmptyMessage(PipMenuActivityController.MESSAGE_EXPAND_PIP,
"Could not notify controller to expand PIP");
}, false /* notifyMenuVisibility */, false /* isDismissing */);
}
private void dismissPip() {
// Do not notify menu visibility when hiding the menu, the controller will do this when it
// handles the message
hideMenu(() -> {
sendEmptyMessage(PipMenuActivityController.MESSAGE_DISMISS_PIP,
"Could not notify controller to dismiss PIP");
}, false /* notifyMenuVisibility */, true /* isDismissing */);
}
private void showSettings() {
final Pair<ComponentName, Integer> topPipActivityInfo =
PipUtils.getTopPipActivity(this, ActivityManager.getService());
if (topPipActivityInfo.first != null) {
final UserHandle user = UserHandle.of(topPipActivityInfo.second);
final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS,
Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null));
settingsIntent.putExtra(Intent.EXTRA_USER_HANDLE, user);
settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
startActivity(settingsIntent);
}
}
private void notifyActivityCallback(Messenger callback) {
Message m = Message.obtain();
m.what = PipMenuActivityController.MESSAGE_UPDATE_ACTIVITY_CALLBACK;
m.replyTo = callback;
m.arg1 = mResize ? 1 : 0;
sendMessage(m, "Could not notify controller of activity finished");
}
private void sendEmptyMessage(int what, String errorMsg) {
Message m = Message.obtain();
m.what = what;
sendMessage(m, errorMsg);
}
private void sendMessage(Message m, String errorMsg) {
if (mToControllerMessenger == null) {
return;
}
try {
mToControllerMessenger.send(m);
} catch (RemoteException e) {
Log.e(TAG, errorMsg, e);
}
}
private void cancelDelayedFinish() {
mHandler.removeCallbacks(mFinishRunnable);
}
private void repostDelayedFinish(int delay) {
int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS);
mHandler.removeCallbacks(mFinishRunnable);
mHandler.postDelayed(mFinishRunnable, recommendedTimeout);
}
}