From 73bc159dcc3e3d32cd6e68f8e72b0e9f9de6a1e9 Mon Sep 17 00:00:00 2001 From: Winson Date: Tue, 18 Oct 2016 18:47:43 -0700 Subject: [PATCH] Adding PIP logic for phones. - Adding basic behavior to move PIP window and launch back into fullscreen, as well as drag it to dismiss. Test: Deferring CTS tests as this interaction is only temporary and not final Change-Id: I5272a045090c20c45b345813d10dc385c3f83221 --- .../SystemUI/res/drawable/pip_dismiss.xml | 24 + .../res/drawable/pip_dismiss_background.xml | 22 + .../SystemUI/res/layout/pip_dismiss_view.xml | 24 + packages/SystemUI/res/values/dimens.xml | 3 + .../android/systemui/SystemUIApplication.java | 2 +- .../android/systemui/{tv => }/pip/PipUI.java | 31 +- .../pip/phone/PipDismissViewController.java | 124 +++++ .../systemui/pip/phone/PipManager.java | 66 +++ .../systemui/pip/phone/PipTouchHandler.java | 440 ++++++++++++++++++ .../android/systemui/tv/pip/PipManager.java | 52 +-- .../tv/pip/PipRecentsControlsView.java | 1 - 11 files changed, 727 insertions(+), 62 deletions(-) create mode 100644 packages/SystemUI/res/drawable/pip_dismiss.xml create mode 100644 packages/SystemUI/res/drawable/pip_dismiss_background.xml create mode 100644 packages/SystemUI/res/layout/pip_dismiss_view.xml rename packages/SystemUI/src/com/android/systemui/{tv => }/pip/PipUI.java (58%) create mode 100644 packages/SystemUI/src/com/android/systemui/pip/phone/PipDismissViewController.java create mode 100644 packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java create mode 100644 packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java diff --git a/packages/SystemUI/res/drawable/pip_dismiss.xml b/packages/SystemUI/res/drawable/pip_dismiss.xml new file mode 100644 index 0000000000000..f656eeb2e5eef --- /dev/null +++ b/packages/SystemUI/res/drawable/pip_dismiss.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/pip_dismiss_background.xml b/packages/SystemUI/res/drawable/pip_dismiss_background.xml new file mode 100644 index 0000000000000..3a7529686dfb0 --- /dev/null +++ b/packages/SystemUI/res/drawable/pip_dismiss_background.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/pip_dismiss_view.xml b/packages/SystemUI/res/layout/pip_dismiss_view.xml new file mode 100644 index 0000000000000..141e610d22944 --- /dev/null +++ b/packages/SystemUI/res/layout/pip_dismiss_view.xml @@ -0,0 +1,24 @@ + + + diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 549d50e272f04..12f7881c11d9b 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -694,4 +694,7 @@ 0.5 + + + 48dp diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index bfc86425be692..99e787676ef59 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -42,7 +42,7 @@ import com.android.systemui.stackdivider.Divider; import com.android.systemui.statusbar.SystemBars; import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.tuner.TunerService; -import com.android.systemui.tv.pip.PipUI; +import com.android.systemui.pip.PipUI; import com.android.systemui.usb.StorageNotification; import com.android.systemui.volume.VolumeUI; diff --git a/packages/SystemUI/src/com/android/systemui/tv/pip/PipUI.java b/packages/SystemUI/src/com/android/systemui/pip/PipUI.java similarity index 58% rename from packages/SystemUI/src/com/android/systemui/tv/pip/PipUI.java rename to packages/SystemUI/src/com/android/systemui/pip/PipUI.java index 3306cb35a9e69..67dda51da2370 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/pip/PipUI.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipUI.java @@ -14,38 +14,49 @@ * limitations under the License. */ -package com.android.systemui.tv.pip; +package com.android.systemui.pip; + +import static android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY; +import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import android.content.pm.PackageManager; import android.content.res.Configuration; import com.android.systemui.SystemUI; -import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; - /** * Controls the picture-in-picture window. */ public class PipUI extends SystemUI { - private boolean mSupportPip; + + private boolean mSupportsPip; + private boolean mIsLeanBackOnly; @Override public void start() { PackageManager pm = mContext.getPackageManager(); - mSupportPip = pm.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE); - if (!mSupportPip) { + mSupportsPip = pm.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE); + mIsLeanBackOnly = pm.hasSystemFeature(FEATURE_LEANBACK_ONLY); + if (!mSupportsPip) { return; } - PipManager pipManager = PipManager.getInstance(); - pipManager.initialize(mContext); + if (mIsLeanBackOnly) { + com.android.systemui.tv.pip.PipManager.getInstance().initialize(mContext); + } else { + com.android.systemui.pip.phone.PipManager.getInstance().initialize(mContext); + } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - if (!mSupportPip) { + if (!mSupportsPip) { return; } - PipManager.getInstance().onConfigurationChanged(); + if (mIsLeanBackOnly) { + com.android.systemui.tv.pip.PipManager.getInstance().onConfigurationChanged(); + } else { + com.android.systemui.pip.phone.PipManager.getInstance().onConfigurationChanged(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipDismissViewController.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipDismissViewController.java new file mode 100644 index 0000000000000..a7ac719149f9a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipDismissViewController.java @@ -0,0 +1,124 @@ +/* + * 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 android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnLayoutChangeListener; +import android.view.WindowManager; + +import com.android.systemui.Interpolators; +import com.android.systemui.R; + +public class PipDismissViewController { + + // This delay controls how long to wait before we show the target when the user first moves + // the PIP, to prevent the target from animating if the user just wants to fling the PIP + private static final int SHOW_TARGET_DELAY = 100; + private static final int SHOW_TARGET_DURATION = 200; + + private Context mContext; + private WindowManager mWindowManager; + + private View mDismissView; + private Rect mDismissTargetScreenBounds = new Rect(); + + public PipDismissViewController(Context context) { + mContext = context; + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + } + + /** + * Creates the dismiss target for showing via {@link #showDismissTarget()}. + */ + public void createDismissTarget() { + if (mDismissView == null) { + // Create a new view for the dismiss target + int dismissTargetSize = mContext.getResources().getDimensionPixelSize( + R.dimen.pip_dismiss_target_size); + LayoutInflater inflater = LayoutInflater.from(mContext); + mDismissView = inflater.inflate(R.layout.pip_dismiss_view, null); + mDismissView.addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (mDismissView != null) { + mDismissView.getBoundsOnScreen(mDismissTargetScreenBounds); + } + } + }); + + // Add the target to the window + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + dismissTargetSize, + dismissTargetSize, + WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + lp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + mWindowManager.addView(mDismissView, lp); + } + mDismissView.animate().cancel(); + } + + /** + * Shows the dismiss target. + */ + public void showDismissTarget() { + mDismissView.animate() + .alpha(1f) + .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) + .setStartDelay(SHOW_TARGET_DELAY) + .setDuration(SHOW_TARGET_DURATION) + .start(); + } + + /** + * Hides and destroys the dismiss target. + */ + public void destroyDismissTarget() { + if (mDismissView != null) { + mDismissView.animate() + .alpha(0f) + .setInterpolator(Interpolators.FAST_OUT_LINEAR_IN) + .setStartDelay(0) + .setDuration(SHOW_TARGET_DURATION) + .withEndAction(new Runnable() { + @Override + public void run() { + mWindowManager.removeView(mDismissView); + mDismissView = null; + } + }) + .start(); + } + } + + /** + * @return the dismiss target screen bounds. + */ + public Rect getDismissBounds() { + return mDismissTargetScreenBounds; + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java new file mode 100644 index 0000000000000..53a4868bc085d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java @@ -0,0 +1,66 @@ +/* + * 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 android.app.ActivityManagerNative; +import android.app.IActivityManager; +import android.content.Context; +import android.view.IWindowManager; +import android.view.WindowManagerGlobal; + +/** + * Manages the picture-in-picture (PIP) UI and states for Phones. + */ +public class PipManager { + private static final String TAG = "PipManager"; + + private static PipManager sPipController; + + private Context mContext; + private IActivityManager mActivityManager; + private IWindowManager mWindowManager; + + private PipTouchHandler mTouchHandler; + + private PipManager() {} + + /** + * Initializes {@link PipManager}. + */ + public void initialize(Context context) { + mContext = context; + mActivityManager = ActivityManagerNative.getDefault(); + mWindowManager = WindowManagerGlobal.getWindowManagerService(); + + mTouchHandler = new PipTouchHandler(context, mActivityManager, mWindowManager); + } + + /** + * Updates the PIP per configuration changed. + */ + public void onConfigurationChanged() {} + + /** + * Gets an instance of {@link PipManager}. + */ + public static PipManager getInstance() { + if (sPipController == null) { + sPipController = new PipManager(); + } + return sPipController; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java new file mode 100644 index 0000000000000..7aa9aa5a18dc4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java @@ -0,0 +1,440 @@ +/* + * 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.app.ActivityManager.StackId.PINNED_STACK_ID; +import static android.view.WindowManager.INPUT_CONSUMER_PIP; + +import static com.android.systemui.Interpolators.FAST_OUT_LINEAR_IN; +import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.systemui.recents.misc.Utilities.RECT_EVALUATOR; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.app.ActivityManager.StackInfo; +import android.app.IActivityManager; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; +import android.view.IWindowManager; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +import com.android.internal.os.BackgroundThread; +import com.android.systemui.statusbar.FlingAnimationUtils; + +/** + * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding + * the PIP. + */ +public class PipTouchHandler { + private static final String TAG = "PipTouchHandler"; + private static final boolean DEBUG_ALLOW_OUT_OF_BOUNDS_STACK = false; + + private static final int SNAP_STACK_DURATION = 225; + private static final int DISMISS_STACK_DURATION = 375; + private static final int EXPAND_STACK_DURATION = 225; + + private static final float SCROLL_FRICTION_MULTIPLIER = 8f; + + private final Context mContext; + private final IActivityManager mActivityManager; + private final ViewConfiguration mViewConfig; + private final InputChannel mInputChannel = new InputChannel(); + + private final PipInputEventReceiver mInputEventReceiver; + private final PipDismissViewController mDismissViewController; + + private final Rect mPinnedStackBounds = new Rect(); + private final Rect mBoundedPinnedStackBounds = new Rect(); + private ValueAnimator mPinnedStackBoundsAnimator = null; + + private final PointF mDownTouch = new PointF(); + private final PointF mLastTouch = new PointF(); + private boolean mIsDragging; + private int mActivePointerId; + + private final FlingAnimationUtils mFlingAnimationUtils; + private final Scroller mScroller; + private VelocityTracker mVelocityTracker; + + /** + * Input handler used for Pip windows. + */ + private final class PipInputEventReceiver extends InputEventReceiver { + public PipInputEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEvent(InputEvent event) { + boolean handled = true; + try { + // To be implemented for input handling over Pip windows + if (event instanceof MotionEvent) { + MotionEvent ev = (MotionEvent) event; + handleTouchEvent(ev); + } + } finally { + finishInputEvent(event, handled); + } + } + } + + public PipTouchHandler(Context context, IActivityManager activityManager, + IWindowManager windowManager) { + + // Initialize the Pip input consumer + try { + windowManager.destroyInputConsumer(INPUT_CONSUMER_PIP); + windowManager.createInputConsumer(INPUT_CONSUMER_PIP, mInputChannel); + } catch (RemoteException e) { + Log.e(TAG, "Failed to create PIP input consumer", e); + } + mContext = context; + mActivityManager = activityManager; + mViewConfig = ViewConfiguration.get(context); + mInputEventReceiver = new PipInputEventReceiver(mInputChannel, Looper.myLooper()); + mDismissViewController = new PipDismissViewController(context); + mScroller = new Scroller(context); + mScroller.setFriction(mViewConfig.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER); + mFlingAnimationUtils = new FlingAnimationUtils(context, 2f); + } + + private void handleTouchEvent(MotionEvent ev) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: { + // Cancel any existing animations on the pinned stack + if (mPinnedStackBoundsAnimator != null) { + mPinnedStackBoundsAnimator.cancel(); + } + + updateBoundedPinnedStackBounds(); + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + mActivePointerId = ev.getPointerId(0); + mLastTouch.set(ev.getX(), ev.getY()); + mDownTouch.set(mLastTouch); + mIsDragging = false; + // TODO: Consider setting a timer such at after X time, we show the dismiss target + // if the user hasn't already dragged some distance + mDismissViewController.createDismissTarget(); + break; + } + case MotionEvent.ACTION_MOVE: { + // Update the velocity tracker + mVelocityTracker.addMovement(ev); + + int activePointerIndex = ev.findPointerIndex(mActivePointerId); + if (!mIsDragging) { + // Check if the pointer has moved far enough + float movement = PointF.length(mDownTouch.x - ev.getX(activePointerIndex), + mDownTouch.y - ev.getY(activePointerIndex)); + if (movement > mViewConfig.getScaledTouchSlop()) { + mIsDragging = true; + mDismissViewController.showDismissTarget(); + } + } + + if (mIsDragging) { + // Move the pinned stack + float dx = ev.getX(activePointerIndex) - mLastTouch.x; + float dy = ev.getY(activePointerIndex) - mLastTouch.y; + float left = Math.max(mBoundedPinnedStackBounds.left, Math.min( + mBoundedPinnedStackBounds.right, mPinnedStackBounds.left + dx)); + float top = Math.max(mBoundedPinnedStackBounds.top, Math.min( + mBoundedPinnedStackBounds.bottom, mPinnedStackBounds.top + dy)); + if (DEBUG_ALLOW_OUT_OF_BOUNDS_STACK) { + left = mPinnedStackBounds.left + dx; + top = mPinnedStackBounds.top + dy; + } + movePinnedStack(left, top); + } + mLastTouch.set(ev.getX(), ev.getY()); + break; + } + case MotionEvent.ACTION_POINTER_UP: { + // Update the velocity tracker + mVelocityTracker.addMovement(ev); + + int pointerIndex = ev.getActionIndex(); + int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // Select a new active pointer id and reset the movement state + final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + mLastTouch.set(ev.getX(newPointerIndex), ev.getY(newPointerIndex)); + } + break; + } + case MotionEvent.ACTION_UP: { + // Update the velocity tracker + mVelocityTracker.addMovement(ev); + + if (mIsDragging) { + mVelocityTracker.computeCurrentVelocity(1000, + ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity()); + float velocityX = mVelocityTracker.getXVelocity(); + float velocityY = mVelocityTracker.getYVelocity(); + float velocity = PointF.length(velocityX, velocityY); + if (velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond()) { + flingToSnapTarget(velocity, velocityX, velocityY); + } else { + int activePointerIndex = ev.findPointerIndex(mActivePointerId); + int x = (int) ev.getX(activePointerIndex); + int y = (int) ev.getY(activePointerIndex); + Rect dismissBounds = mDismissViewController.getDismissBounds(); + if (dismissBounds.contains(x, y)) { + animateDismissPinnedStack(dismissBounds); + } else { + animateToSnapTarget(); + } + } + } else { + expandPinnedStackToFullscreen(); + } + mDismissViewController.destroyDismissTarget(); + + // Fall through to clean up + } + case MotionEvent.ACTION_CANCEL: { + mIsDragging = false; + recycleVelocityTracker(); + break; + } + } + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** + * Creates an animation that continues the fling to a snap target. + */ + private void flingToSnapTarget(float velocity, float velocityX, float velocityY) { + mScroller.fling(mPinnedStackBounds.left, mPinnedStackBounds.top, + (int) velocityX, (int) velocityY, + mBoundedPinnedStackBounds.left, mBoundedPinnedStackBounds.right, + mBoundedPinnedStackBounds.top, mBoundedPinnedStackBounds.bottom); + Rect toBounds = findClosestBoundedPinnedStackSnapTarget( + mScroller.getFinalX(), mScroller.getFinalY()); + mScroller.abortAnimation(); + if (!mPinnedStackBounds.equals(toBounds)) { + mPinnedStackBoundsAnimator = createResizePinnedStackAnimation( + toBounds, 0, FAST_OUT_SLOW_IN); + mFlingAnimationUtils.apply(mPinnedStackBoundsAnimator, 0, + distanceBetweenRectOffsets(mPinnedStackBounds, toBounds), + velocity); + mPinnedStackBoundsAnimator.start(); + } + } + + /** + * Animates the pinned stack to the closest snap target. + */ + private void animateToSnapTarget() { + Rect toBounds = findClosestBoundedPinnedStackSnapTarget( + mPinnedStackBounds.left, mPinnedStackBounds.top); + if (!mPinnedStackBounds.equals(toBounds)) { + mPinnedStackBoundsAnimator = createResizePinnedStackAnimation( + toBounds, SNAP_STACK_DURATION, FAST_OUT_SLOW_IN); + mPinnedStackBoundsAnimator.start(); + } + } + + /** + * Animates the dismissal of the pinned stack into the given bounds. + */ + private void animateDismissPinnedStack(Rect dismissBounds) { + Rect toBounds = new Rect(dismissBounds.centerX(), + dismissBounds.centerY(), + dismissBounds.centerX() + 1, + dismissBounds.centerY() + 1); + mPinnedStackBoundsAnimator = createResizePinnedStackAnimation( + toBounds, DISMISS_STACK_DURATION, FAST_OUT_LINEAR_IN); + mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + BackgroundThread.getHandler().post(() -> { + try { + mActivityManager.removeStack(PINNED_STACK_ID); + } catch (RemoteException e) { + Log.e(TAG, "Failed to remove PIP", e); + } + }); + } + }); + mPinnedStackBoundsAnimator.start(); + } + + /** + * Resizes the pinned stack back to fullscreen. + */ + private void expandPinnedStackToFullscreen() { + BackgroundThread.getHandler().post(() -> { + try { + mActivityManager.resizeStack(PINNED_STACK_ID, null /* bounds */, + true /* allowResizeInDockedMode */, true /* preserveWindows */, + true /* animate */, EXPAND_STACK_DURATION); + } catch (RemoteException e) { + Log.e(TAG, "Error showing PIP menu activity", e); + } + }); + } + + /** + * Updates the movement bounds of the pinned stack. + */ + private void updateBoundedPinnedStackBounds() { + try { + StackInfo info = mActivityManager.getStackInfo(PINNED_STACK_ID); + mPinnedStackBounds.set(info.bounds); + mBoundedPinnedStackBounds.set(mActivityManager.getPictureInPictureMovementBounds()); + } catch (RemoteException e) { + Log.e(TAG, "Could not fetch PIP movement bounds.", e); + } + } + + /** + * Moves the pinned stack to the given {@param left} and {@param top} offsets. + */ + private void movePinnedStack(float left, float top) { + if ((int) left != mPinnedStackBounds.left || (int) top != mPinnedStackBounds.top) { + mPinnedStackBounds.offsetTo((int) left, (int) top); + BackgroundThread.getHandler().post(() -> { + try { + mActivityManager.resizePinnedStack(mPinnedStackBounds, + null /* tempPinnedBounds */); + } catch (RemoteException e) { + Log.e(TAG, "Could not move pinned stack to offset: (" + left + ", " + top + ")", + e); + } + }); + } + } + + /** + * Resizes the pinned stack to the given {@param bounds}. + */ + private void resizePinnedStack(Rect bounds) { + if (!mPinnedStackBounds.equals(bounds)) { + mPinnedStackBounds.set(bounds); + BackgroundThread.getHandler().post(() -> { + try { + mActivityManager.resizePinnedStack(bounds, null); + } catch (RemoteException e) { + Log.e(TAG, "Could not resize pinned stack to bounds: (" + bounds + ")"); + } + }); + } + } + + /** + * Creates a resize-stack animation. + */ + private ValueAnimator createResizePinnedStackAnimation(Rect toBounds, int duration, + Interpolator interpolator) { + ValueAnimator anim = ValueAnimator.ofObject(RECT_EVALUATOR, + mPinnedStackBounds, toBounds); + anim.setDuration(duration); + anim.setInterpolator(interpolator); + anim.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + resizePinnedStack((Rect) animation.getAnimatedValue()); + } + }); + return anim; + } + + /** + * @return the closest absolute bounded stack left/top to the given {@param x} and {@param y}. + */ + private Rect findClosestBoundedPinnedStackSnapTarget(int x, int y) { + Point[] snapTargets; + int orientation = mContext.getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + snapTargets = new Point[] { + new Point(mBoundedPinnedStackBounds.left, mBoundedPinnedStackBounds.top), + new Point(mBoundedPinnedStackBounds.right, mBoundedPinnedStackBounds.top), + new Point(mBoundedPinnedStackBounds.left, mBoundedPinnedStackBounds.bottom), + new Point(mBoundedPinnedStackBounds.right, mBoundedPinnedStackBounds.bottom) + }; + } else { + snapTargets = new Point[] { + new Point(mBoundedPinnedStackBounds.left, mBoundedPinnedStackBounds.top), + new Point(mBoundedPinnedStackBounds.right, mBoundedPinnedStackBounds.top), + new Point(mBoundedPinnedStackBounds.left, mBoundedPinnedStackBounds.bottom), + new Point(mBoundedPinnedStackBounds.right, mBoundedPinnedStackBounds.bottom) + }; + } + + Point closestSnapTarget = null; + float minDistance = Float.MAX_VALUE; + for (Point p : snapTargets) { + float distance = distanceToPoint(p, x, y); + if (distance < minDistance) { + closestSnapTarget = p; + minDistance = distance; + } + } + + Rect toBounds = new Rect(mPinnedStackBounds); + toBounds.offsetTo(closestSnapTarget.x, closestSnapTarget.y); + return toBounds; + } + + /** + * @return the distance between point {@param p} and the given {@param x} and {@param y}. + */ + private float distanceToPoint(Point p, int x, int y) { + return PointF.length(p.x - x, p.y - y); + } + + + /** + * @return the distance between points {@param p1} and {@param p2}. + */ + private float distanceBetweenRectOffsets(Rect r1, Rect r2) { + return PointF.length(r1.left - r2.left, r1.top - r2.top); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java b/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java index cd98d19b814a2..65a1b44e8f2fb 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/tv/pip/PipManager.java @@ -25,7 +25,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Rect; import android.media.session.MediaController; @@ -33,16 +32,12 @@ import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.Debug; import android.os.Handler; -import android.os.Looper; import android.os.RemoteException; import android.os.SystemProperties; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.IWindowManager; -import android.view.InputChannel; -import android.view.InputEvent; -import android.view.InputEventReceiver; import android.view.WindowManagerGlobal; import com.android.systemui.Prefs; import com.android.systemui.R; @@ -53,7 +48,6 @@ import java.util.ArrayList; import java.util.List; import static android.app.ActivityManager.StackId.PINNED_STACK_ID; -import static android.view.WindowManager.INPUT_CONSUMER_PIP; import static com.android.systemui.Prefs.Key.TV_PICTURE_IN_PICTURE_ONBOARDING_SHOWN; /** @@ -67,8 +61,6 @@ public class PipManager { private static PipManager sPipManager; - private static final int MAX_RUNNING_TASKS_COUNT = 10; - /** * List of package and class name which are considered as Settings, * so PIP location should be adjusted to the left of the side panel. @@ -163,9 +155,6 @@ public class PipManager { private boolean mOnboardingShown; private String[] mLastPackagesResourceGranted; - private InputChannel mInputChannel; - private PipInputEventReceiver mInputEventReceiver; - private final Runnable mResizePinnedStackRunnable = new Runnable() { @Override public void run() { @@ -203,25 +192,6 @@ public class PipManager { } }; - /** - * Input handler used for Pip windows. Currently eats all the input events. - */ - private final class PipInputEventReceiver extends InputEventReceiver { - public PipInputEventReceiver(InputChannel inputChannel, Looper looper) { - super(inputChannel, looper); - } - - @Override - public void onInputEvent(InputEvent event) { - boolean handled = true; - try { - // To be implemented for input handling over Pip windows - } finally { - finishInputEvent(event, handled); - } - } - } - private PipManager() { } /** @@ -246,20 +216,6 @@ public class PipManager { mPipRecentsOverlayManager = new PipRecentsOverlayManager(context); mMediaSessionManager = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE); - - PackageManager pm = mContext.getPackageManager(); - if (!pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY)) { - // Initialize the Pip input consumer - mInputChannel = new InputChannel(); - try { - IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); - wm.destroyInputConsumer(INPUT_CONSUMER_PIP); - wm.createInputConsumer(INPUT_CONSUMER_PIP, mInputChannel); - mInputEventReceiver = new PipInputEventReceiver(mInputChannel, Looper.myLooper()); - } catch (RemoteException e) { - Log.e(TAG, "Failed to create PIP input consumer", e); - } - } } private void loadConfigurationsAndApply() { @@ -290,7 +246,7 @@ public class PipManager { /** * Updates the PIP per configuration changed. */ - void onConfigurationChanged() { + public void onConfigurationChanged() { loadConfigurationsAndApply(); mPipRecentsOverlayManager.onConfigurationChanged(mContext); } @@ -350,11 +306,7 @@ public class PipManager { */ private void showPipOverlay() { if (DEBUG) Log.d(TAG, "showPipOverlay()"); - // Temporary workaround to prevent the overlay on phones - PackageManager pm = mContext.getPackageManager(); - if (pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY)) { - PipOverlayActivity.showPipOverlay(mContext); - } + PipOverlayActivity.showPipOverlay(mContext); } /** diff --git a/packages/SystemUI/src/com/android/systemui/tv/pip/PipRecentsControlsView.java b/packages/SystemUI/src/com/android/systemui/tv/pip/PipRecentsControlsView.java index ffe96afae3a36..3726fbcf80093 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/pip/PipRecentsControlsView.java +++ b/packages/SystemUI/src/com/android/systemui/tv/pip/PipRecentsControlsView.java @@ -49,7 +49,6 @@ public class PipRecentsControlsView extends FrameLayout { } private final PipManager mPipManager = PipManager.getInstance(); - private Listener mListener; private PipControlsView mPipControlsView; private View mScrim; private Animator mFocusGainAnimator;