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
diff --git a/packages/SystemUI/res/drawable/pip_dismiss.xml b/packages/SystemUI/res/drawable/pip_dismiss.xml
new file mode 100644
index 0000000..f656eeb
--- /dev/null
+++ b/packages/SystemUI/res/drawable/pip_dismiss.xml
@@ -0,0 +1,24 @@
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="42.0dp"
+ android:height="42.0dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M38.000000,12.800000l-2.799999,-2.800000 -11.200001,11.200001 -11.200000,-11.200001 -2.800000,2.800000 11.200001,11.200000 -11.200001,11.200001 2.800000,2.799999 11.200000,-11.200001 11.200001,11.200001 2.799999,-2.799999 -11.200001,-11.200001z"/>
+</vector>
\ 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 0000000..3a75296
--- /dev/null
+++ b/packages/SystemUI/res/drawable/pip_dismiss_background.xml
@@ -0,0 +1,22 @@
+<!--
+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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <corners
+ android:radius="100dp" />
+ <solid
+ android:color="#66000000" />
+</shape>
\ 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 0000000..141e610
--- /dev/null
+++ b/packages/SystemUI/res/layout/pip_dismiss_view.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pip_dismiss_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/pip_dismiss_background"
+ android:foreground="@drawable/pip_dismiss"
+ android:alpha="0"
+ android:forceHasOverlappingRendering="false" />
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 549d50e..12f7881 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -694,4 +694,7 @@
<!-- The alpha to apply to the recents row when it doesn't have focus -->
<item name="recents_recents_row_dim_alpha" format="float" type="dimen">0.5</item>
+
+ <!-- The size of the PIP dismiss target. -->
+ <dimen name="pip_dismiss_target_size">48dp</dimen>
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index bfc8642..99e7876 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.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/pip/PipUI.java b/packages/SystemUI/src/com/android/systemui/pip/PipUI.java
new file mode 100644
index 0000000..67dda51
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/pip/PipUI.java
@@ -0,0 +1,62 @@
+/*
+ * 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;
+
+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;
+
+/**
+ * Controls the picture-in-picture window.
+ */
+public class PipUI extends SystemUI {
+
+ private boolean mSupportsPip;
+ private boolean mIsLeanBackOnly;
+
+ @Override
+ public void start() {
+ PackageManager pm = mContext.getPackageManager();
+ mSupportsPip = pm.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE);
+ mIsLeanBackOnly = pm.hasSystemFeature(FEATURE_LEANBACK_ONLY);
+ if (!mSupportsPip) {
+ return;
+ }
+ 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 (!mSupportsPip) {
+ return;
+ }
+ 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 0000000..a7ac719
--- /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 0000000..53a4868
--- /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 0000000..7aa9aa5
--- /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 cd98d19..65a1b44 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.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.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.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 @@
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 @@
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 @@
}
};
- /**
- * 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 @@
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 @@
/**
* Updates the PIP per configuration changed.
*/
- void onConfigurationChanged() {
+ public void onConfigurationChanged() {
loadConfigurationsAndApply();
mPipRecentsOverlayManager.onConfigurationChanged(mContext);
}
@@ -350,11 +306,7 @@
*/
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 ffe96af..3726fbc 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 @@
}
private final PipManager mPipManager = PipManager.getInstance();
- private Listener mListener;
private PipControlsView mPipControlsView;
private View mScrim;
private Animator mFocusGainAnimator;
diff --git a/packages/SystemUI/src/com/android/systemui/tv/pip/PipUI.java b/packages/SystemUI/src/com/android/systemui/tv/pip/PipUI.java
deleted file mode 100644
index 3306cb3..0000000
--- a/packages/SystemUI/src/com/android/systemui/tv/pip/PipUI.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.tv.pip;
-
-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;
-
- @Override
- public void start() {
- PackageManager pm = mContext.getPackageManager();
- mSupportPip = pm.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE);
- if (!mSupportPip) {
- return;
- }
- PipManager pipManager = PipManager.getInstance();
- pipManager.initialize(mContext);
- }
-
- @Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- if (!mSupportPip) {
- return;
- }
- PipManager.getInstance().onConfigurationChanged();
- }
-}