Adding API for apps to specify their aspect ratio when entering PIP.
Test: android.server.cts.ActivityManagerPinnedStackTests
Test: #testEnterPipAspectRatio
Test: #testEnterPipExtremeAspectRatios
Change-Id: I9efba942b9a6451dec07428fe1e428ef4a896867
diff --git a/api/current.txt b/api/current.txt
index f89488d..413d8b0 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -3461,6 +3461,7 @@
method public boolean dispatchTrackballEvent(android.view.MotionEvent);
method public void dump(java.lang.String, java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
method public void enterPictureInPictureMode();
+ method public void enterPictureInPictureMode(float);
method public android.view.View findViewById(int);
method public void finish();
method public void finishActivity(int);
@@ -3632,6 +3633,7 @@
method public void setIntent(android.content.Intent);
method public final void setMediaController(android.media.session.MediaController);
method public void setOverlayWithDecorCaptionEnabled(boolean);
+ method public void setPictureInPictureAspectRatio(float);
method public final deprecated void setProgress(int);
method public final deprecated void setProgressBarIndeterminate(boolean);
method public final deprecated void setProgressBarIndeterminateVisibility(boolean);
@@ -32078,10 +32080,10 @@
field public static final java.lang.String EXTRA_ERROR = "error";
field public static final java.lang.String EXTRA_EXCLUDE_SELF = "android.provider.extra.EXCLUDE_SELF";
field public static final java.lang.String EXTRA_INFO = "info";
+ field public static final java.lang.String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI";
field public static final java.lang.String EXTRA_LOADING = "loading";
field public static final java.lang.String EXTRA_ORIENTATION = "android.provider.extra.ORIENTATION";
field public static final java.lang.String EXTRA_PROMPT = "android.provider.extra.PROMPT";
- field public static final java.lang.String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI";
field public static final java.lang.String PROVIDER_INTERFACE = "android.content.action.DOCUMENTS_PROVIDER";
}
diff --git a/api/system-current.txt b/api/system-current.txt
index e2887b6..e1f568f 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -3577,6 +3577,7 @@
method public boolean dispatchTrackballEvent(android.view.MotionEvent);
method public void dump(java.lang.String, java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
method public void enterPictureInPictureMode();
+ method public void enterPictureInPictureMode(float);
method public android.view.View findViewById(int);
method public void finish();
method public void finishActivity(int);
@@ -3750,6 +3751,7 @@
method public void setIntent(android.content.Intent);
method public final void setMediaController(android.media.session.MediaController);
method public void setOverlayWithDecorCaptionEnabled(boolean);
+ method public void setPictureInPictureAspectRatio(float);
method public final deprecated void setProgress(int);
method public final deprecated void setProgressBarIndeterminate(boolean);
method public final deprecated void setProgressBarIndeterminateVisibility(boolean);
@@ -34741,10 +34743,10 @@
field public static final java.lang.String EXTRA_ERROR = "error";
field public static final java.lang.String EXTRA_EXCLUDE_SELF = "android.provider.extra.EXCLUDE_SELF";
field public static final java.lang.String EXTRA_INFO = "info";
+ field public static final java.lang.String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI";
field public static final java.lang.String EXTRA_LOADING = "loading";
field public static final java.lang.String EXTRA_ORIENTATION = "android.provider.extra.ORIENTATION";
field public static final java.lang.String EXTRA_PROMPT = "android.provider.extra.PROMPT";
- field public static final java.lang.String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI";
field public static final java.lang.String PROVIDER_INTERFACE = "android.content.action.DOCUMENTS_PROVIDER";
}
diff --git a/api/test-current.txt b/api/test-current.txt
index f7f5e50..870c224 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -3463,6 +3463,7 @@
method public boolean dispatchTrackballEvent(android.view.MotionEvent);
method public void dump(java.lang.String, java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
method public void enterPictureInPictureMode();
+ method public void enterPictureInPictureMode(float);
method public android.view.View findViewById(int);
method public void finish();
method public void finishActivity(int);
@@ -3634,6 +3635,7 @@
method public void setIntent(android.content.Intent);
method public final void setMediaController(android.media.session.MediaController);
method public void setOverlayWithDecorCaptionEnabled(boolean);
+ method public void setPictureInPictureAspectRatio(float);
method public final deprecated void setProgress(int);
method public final deprecated void setProgressBarIndeterminate(boolean);
method public final deprecated void setProgressBarIndeterminateVisibility(boolean);
@@ -32164,10 +32166,10 @@
field public static final java.lang.String EXTRA_ERROR = "error";
field public static final java.lang.String EXTRA_EXCLUDE_SELF = "android.provider.extra.EXCLUDE_SELF";
field public static final java.lang.String EXTRA_INFO = "info";
+ field public static final java.lang.String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI";
field public static final java.lang.String EXTRA_LOADING = "loading";
field public static final java.lang.String EXTRA_ORIENTATION = "android.provider.extra.ORIENTATION";
field public static final java.lang.String EXTRA_PROMPT = "android.provider.extra.PROMPT";
- field public static final java.lang.String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI";
field public static final java.lang.String PROVIDER_INTERFACE = "android.content.action.DOCUMENTS_PROVIDER";
}
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index b3e2f57..b381339 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1929,6 +1929,32 @@
}
/**
+ * Puts the activity in picture-in-picture mode with a given aspect ratio.
+ * @see android.R.attr#supportsPictureInPicture
+ *
+ * @param aspectRatio the new aspect ratio of the picture-in-picture.
+ */
+ public void enterPictureInPictureMode(float aspectRatio) {
+ try {
+ ActivityManagerNative.getDefault().enterPictureInPictureModeWithAspectRatio(mToken,
+ aspectRatio);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Updates the aspect ratio of the current picture-in-picture activity.
+ *
+ * @param aspectRatio the new aspect ratio of the picture-in-picture.
+ */
+ public void setPictureInPictureAspectRatio(float aspectRatio) {
+ try {
+ ActivityManagerNative.getDefault().setPictureInPictureAspectRatio(mToken, aspectRatio);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
* Called by the system when the device configuration changes while your
* activity is running. Note that this will <em>only</em> be called if
* you have selected configurations you would like to handle with the
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 80267205..6a3a799 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -562,7 +562,9 @@
boolean updateDisplayOverrideConfiguration(in Configuration values, int displayId) = 401;
void unregisterTaskStackListener(ITaskStackListener listener) = 402;
void moveStackToDisplay(int stackId, int displayId) = 403;
+ void enterPictureInPictureModeWithAspectRatio(in IBinder token, float aspectRatio) = 404;
+ void setPictureInPictureAspectRatio(in IBinder token, float aspectRatio) = 405;
// Please keep these transaction codes the same -- they are also
// sent by C++ code. when a new method is added, use the next available transaction id.
-}
\ No newline at end of file
+}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index d207c2b..44bc3ff 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2496,6 +2496,14 @@
Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT -->
<integer name="config_defaultPictureInPictureGravity">0x55</integer>
+ <!-- The minimum aspect ratio (width/height) that is supported for picture-in-picture. Any
+ ratio smaller than this is considered too tall and thin to be usable. -->
+ <item name="config_pictureInPictureMinAspectRatio" format="float" type="dimen">0.5</item>
+
+ <!-- The minimum aspect ratio (width/height) that is supported for picture-in-picture. Any
+ ratio larger than this is considered to wide and short to be usable. -->
+ <item name="config_pictureInPictureMaxAspectRatio" format="float" type="dimen">2.35</item>
+
<!-- Controls the snap mode for the docked stack divider
0 - 3 snap targets: left/top has 16:9 ratio, 1:1, and right/bottom has 16:9 ratio
1 - 3 snap targets: fixed ratio, 1:1, (1 - fixed ratio)
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 07c4c69..3be98b6 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -313,6 +313,8 @@
<java-symbol type="string" name="config_defaultPictureInPictureScreenEdgeInsets" />
<java-symbol type="string" name="config_defaultPictureInPictureSize" />
<java-symbol type="integer" name="config_defaultPictureInPictureGravity" />
+ <java-symbol type="dimen" name="config_pictureInPictureMinAspectRatio" />
+ <java-symbol type="dimen" name="config_pictureInPictureMaxAspectRatio" />
<java-symbol type="integer" name="config_wifi_framework_5GHz_preference_boost_threshold" />
<java-symbol type="integer" name="config_wifi_framework_5GHz_preference_boost_factor" />
<java-symbol type="integer" name="config_wifi_framework_5GHz_preference_penalty_threshold" />
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 42d16fb..989977a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -21,6 +21,7 @@
import android.app.ContentProviderHolder;
import android.app.IActivityManager;
import android.app.WaitResult;
+import android.graphics.PointF;
import android.os.IDeviceIdentifiersPolicyService;
import com.android.internal.telephony.TelephonyIntents;
import com.google.android.collect.Lists;
@@ -293,7 +294,6 @@
import static android.provider.Settings.Global.LENIENT_BACKGROUND_CHECK;
import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER;
import static android.provider.Settings.System.FONT_SCALE;
-import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.internal.util.XmlUtils.readBooleanAttribute;
@@ -1575,6 +1575,10 @@
int mThumbnailHeight;
float mFullscreenThumbnailScale;
+ /** The aspect ratio bounds of the PIP. */
+ float mMinPipAspectRatio;
+ float mMaxPipAspectRatio;
+
final ServiceThread mHandlerThread;
final MainHandler mHandler;
final UiHandler mUiHandler;
@@ -7469,6 +7473,15 @@
@Override
public void enterPictureInPictureMode(IBinder token) {
+ enterPictureInPictureMode(token, DEFAULT_DISPLAY, null /* aspectRatio */);
+ }
+
+ @Override
+ public void enterPictureInPictureModeWithAspectRatio(IBinder token, float aspectRatio) {
+ enterPictureInPictureMode(token, DEFAULT_DISPLAY, aspectRatio);
+ }
+
+ public void enterPictureInPictureMode(IBinder token, int displayId, Float aspectRatio) {
final long origId = Binder.clearCallingIdentity();
try {
synchronized(this) {
@@ -7478,7 +7491,6 @@
}
final ActivityRecord r = ActivityRecord.forTokenLocked(token);
-
if (r == null) {
throw new IllegalStateException("enterPictureInPictureMode: "
+ "Can't find activity for token=" + token);
@@ -7489,21 +7501,55 @@
+ "Picture-In-Picture not supported for r=" + r);
}
- // Use the default launch bounds for pinned stack if it doesn't exist yet or use the
- // current bounds.
- final ActivityStack pinnedStack = mStackSupervisor.getStack(PINNED_STACK_ID);
- final Rect bounds = (pinnedStack != null)
- ? pinnedStack.mBounds
- : mWindowManager.getPictureInPictureDefaultBounds(DEFAULT_DISPLAY);
+ if (aspectRatio != null && !isValidPictureInPictureAspectRatio(aspectRatio)) {
+ throw new IllegalArgumentException(String.format("enterPictureInPictureMode: "
+ + "Aspect ratio is too extreme (must be between %f and %f).",
+ mMinPipAspectRatio, mMaxPipAspectRatio));
+ }
- mStackSupervisor.moveActivityToPinnedStackLocked(
- r, "enterPictureInPictureMode", bounds);
+ final Rect bounds = isValidPictureInPictureAspectRatio(aspectRatio)
+ ? mWindowManager.getPictureInPictureBounds(displayId, aspectRatio)
+ : mWindowManager.getPictureInPictureDefaultBounds(displayId);
+ mStackSupervisor.moveActivityToPinnedStackLocked(r, "enterPictureInPictureMode",
+ bounds);
}
} finally {
Binder.restoreCallingIdentity(origId);
}
}
+ @Override
+ public void setPictureInPictureAspectRatio(IBinder token, float aspectRatio) {
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ synchronized(this) {
+ final ActivityRecord r = ActivityRecord.forTokenLocked(token);
+ if (r == null || r.getStack().mStackId != PINNED_STACK_ID) {
+ throw new IllegalStateException("setPictureInPictureAspectRatio: "
+ + "Requesting activity must be in picture-in-picture mode.");
+ }
+
+ if (!isValidPictureInPictureAspectRatio(aspectRatio)) {
+ throw new IllegalArgumentException(String.format(
+ "setPictureInPictureAspectRatio: Aspect ratio is too extreme (must be "
+ + "between %f and %f).", mMinPipAspectRatio,
+ mMaxPipAspectRatio));
+ }
+
+ mWindowManager.setPictureInPictureAspectRatio(aspectRatio);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+
+ private boolean isValidPictureInPictureAspectRatio(Float aspectRatio) {
+ if (aspectRatio == null) {
+ return false;
+ }
+ return mMinPipAspectRatio <= aspectRatio && aspectRatio <= mMaxPipAspectRatio;
+ }
+
// =========================================================
// PROCESS INFO
// =========================================================
@@ -13035,6 +13081,10 @@
com.android.internal.R.dimen.thumbnail_width);
mThumbnailHeight = res.getDimensionPixelSize(
com.android.internal.R.dimen.thumbnail_height);
+ mMinPipAspectRatio = res.getFloat(
+ com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
+ mMaxPipAspectRatio = res.getFloat(
+ com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
mAppErrors.loadAppsNotReportingCrashesFromConfigLocked(res.getString(
com.android.internal.R.string.config_appsNotReportingCrashes));
mUserController.mUserSwitchUiEnabled = !res.getBoolean(
diff --git a/services/core/java/com/android/server/wm/BoundsAnimationController.java b/services/core/java/com/android/server/wm/BoundsAnimationController.java
index 5bfece4..cf5cecda 100644
--- a/services/core/java/com/android/server/wm/BoundsAnimationController.java
+++ b/services/core/java/com/android/server/wm/BoundsAnimationController.java
@@ -96,8 +96,8 @@
private final class BoundsAnimator extends ValueAnimator
implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
private final AnimateBoundsUser mTarget;
- private final Rect mFrom;
- private final Rect mTo;
+ private final Rect mFrom = new Rect();
+ private final Rect mTo = new Rect();
private final Rect mTmpRect = new Rect();
private final Rect mTmpTaskBounds = new Rect();
private final boolean mMoveToFullScreen;
@@ -117,8 +117,8 @@
boolean moveToFullScreen, boolean replacement) {
super();
mTarget = target;
- mFrom = from;
- mTo = to;
+ mFrom.set(from);
+ mTo.set(to);
mMoveToFullScreen = moveToFullScreen;
mReplacement = replacement;
addUpdateListener(this);
diff --git a/services/core/java/com/android/server/wm/PinnedStackController.java b/services/core/java/com/android/server/wm/PinnedStackController.java
index 1ccf722..52c18f1 100644
--- a/services/core/java/com/android/server/wm/PinnedStackController.java
+++ b/services/core/java/com/android/server/wm/PinnedStackController.java
@@ -26,6 +26,7 @@
import android.animation.ValueAnimator;
import android.content.res.Resources;
import android.graphics.Point;
+import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Handler;
import android.os.IBinder;
@@ -167,6 +168,25 @@
}
/**
+ * Returns the current bounds (or the default bounds if there are no current bounds) with the
+ * specified aspect ratio.
+ */
+ Rect getAspectRatioBounds(Rect stackBounds, float aspectRatio) {
+ // Save the snap fraction, calculate the aspect ratio based on the current bounds
+ final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
+ getMovementBounds(stackBounds));
+ final float radius = PointF.length(stackBounds.width(), stackBounds.height());
+ final int height = (int) Math.round(Math.sqrt((radius * radius) /
+ (aspectRatio * aspectRatio + 1)));
+ final int width = Math.round(height * aspectRatio);
+ final int left = (int) (stackBounds.centerX() - width / 2f);
+ final int top = (int) (stackBounds.centerY() - height / 2f);
+ stackBounds.set(left, top, left + width, top + height);
+ mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
+ return stackBounds;
+ }
+
+ /**
* @return the default bounds to show the PIP when there is no active PIP.
*/
Rect getDefaultBounds() {
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index a0270c6..203ba72 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -132,6 +132,7 @@
// perfectly fit the region it would have been cropped to. We may also avoid certain logic we
// would otherwise apply while resizing, while resizing in the bounds animating mode.
private boolean mBoundsAnimating = false;
+ private Rect mBoundsAnimationTarget = new Rect();
// Temporary storage for the new bounds that should be used after the configuration change.
// Will be cleared once the client retrieves the new bounds via getBoundsForNewConfiguration().
@@ -329,6 +330,30 @@
mDisplayContent.getLogicalDisplayRect(out);
}
+ /**
+ * Sets the bounds animation target bounds. This can't currently be done in onAnimationStart()
+ * since that is started on the UiThread.
+ */
+ void setAnimatingBounds(Rect bounds) {
+ if (bounds != null) {
+ mBoundsAnimationTarget.set(bounds);
+ } else {
+ mBoundsAnimationTarget.setEmpty();
+ }
+ }
+
+ /**
+ * @return the bounds that the task stack is currently being animated towards, or the current
+ * stack bounds if there is no animation in progress.
+ */
+ void getAnimatingBounds(Rect outBounds) {
+ if (!mBoundsAnimationTarget.isEmpty()) {
+ outBounds.set(mBoundsAnimationTarget);
+ return;
+ }
+ getBounds(outBounds);
+ }
+
/** Bounds of the stack with other system factors taken into consideration. */
@Override
public void getDimBounds(Rect out) {
@@ -1391,6 +1416,7 @@
public void onAnimationEnd() {
synchronized (mService.mWindowMap) {
mBoundsAnimating = false;
+ mBoundsAnimationTarget.setEmpty();
mService.requestTraversal();
}
if (mStackId == PINNED_STACK_ID) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index a533d84..8be87a1 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -172,7 +172,6 @@
import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
import static android.app.StatusBarManager.DISABLE_MASK;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.DOCKED_INVALID;
import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
@@ -3404,7 +3403,7 @@
public Rect getPictureInPictureDefaultBounds(int displayId) {
synchronized (mWindowMap) {
if (!mSupportsPictureInPicture) {
- return new Rect();
+ return null;
}
final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
@@ -3416,7 +3415,7 @@
public Rect getPictureInPictureMovementBounds(int displayId) {
synchronized (mWindowMap) {
if (!mSupportsPictureInPicture) {
- return new Rect();
+ return null;
}
final Rect stackBounds = new Rect();
@@ -3430,6 +3429,47 @@
}
}
+ public void setPictureInPictureAspectRatio(float aspectRatio) {
+ synchronized (mWindowMap) {
+ if (!mSupportsPictureInPicture) {
+ return;
+ }
+
+ final TaskStack stack = mStackIdToStack.get(PINNED_STACK_ID);
+ if (stack == null) {
+ return;
+ }
+
+ animateResizePinnedStack(getPictureInPictureBounds(
+ stack.getDisplayContent().getDisplayId(), aspectRatio), -1);
+ }
+ }
+
+ public Rect getPictureInPictureBounds(int displayId, float aspectRatio) {
+ synchronized (mWindowMap) {
+ if (!mSupportsPictureInPicture) {
+ return null;
+ }
+
+ final Rect stackBounds;
+ final DisplayContent displayContent;
+ final TaskStack stack = mStackIdToStack.get(PINNED_STACK_ID);
+ if (stack != null) {
+ // If the stack exists, then use its final bounds to calculate the new aspect ratio
+ // bounds.
+ displayContent = stack.getDisplayContent();
+ stackBounds = new Rect();
+ stack.getAnimatingBounds(stackBounds);
+ } else {
+ // Otherwise, just calculate the aspect ratio bounds from the default bounds
+ displayContent = mRoot.getDisplayContent(displayId);
+ stackBounds = displayContent.getPinnedStackController().getDefaultBounds();
+ }
+ return displayContent.getPinnedStackController().getAspectRatioBounds(stackBounds,
+ aspectRatio);
+ }
+ }
+
/**
* Place a TaskStack on a DisplayContent. Will create a new TaskStack if none is found with
* specified stackId.
@@ -8456,6 +8496,7 @@
}
final Rect originalBounds = new Rect();
stack.getBounds(originalBounds);
+ stack.setAnimatingBounds(bounds);
UiThread.getHandler().post(new Runnable() {
@Override
public void run() {