Merge "Move TV PiP away from keep clear areas" into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
3f574e253e
@@ -33,4 +33,10 @@
|
|||||||
<!-- The default gravity for the picture-in-picture window.
|
<!-- The default gravity for the picture-in-picture window.
|
||||||
Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT -->
|
Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT -->
|
||||||
<integer name="config_defaultPictureInPictureGravity">0x55</integer>
|
<integer name="config_defaultPictureInPictureGravity">0x55</integer>
|
||||||
|
|
||||||
|
<!-- Fraction of screen width/height restricted keep clear areas can move the PiP. -->
|
||||||
|
<fraction name="config_pipMaxRestrictedMoveDistance">15%</fraction>
|
||||||
|
|
||||||
|
<!-- Duration (in milliseconds) the PiP stays stashed before automatically unstashing. -->
|
||||||
|
<integer name="config_pipStashDuration">5000</integer>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
24
libs/WindowManager/Shell/res/values-television/dimen.xml
Normal file
24
libs/WindowManager/Shell/res/values-television/dimen.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (C) 2022 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- These resources are around just to allow their values to be customized
|
||||||
|
for TV products. Do not translate. -->
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
|
||||||
|
<!-- Padding between PIP and keep clear areas that caused it to move. -->
|
||||||
|
<dimen name="pip_keep_clear_area_padding">16dp</dimen>
|
||||||
|
</resources>
|
||||||
@@ -69,7 +69,8 @@ public abstract class TvPipModule {
|
|||||||
TaskStackListenerImpl taskStackListener,
|
TaskStackListenerImpl taskStackListener,
|
||||||
DisplayController displayController,
|
DisplayController displayController,
|
||||||
WindowManagerShellWrapper windowManagerShellWrapper,
|
WindowManagerShellWrapper windowManagerShellWrapper,
|
||||||
@ShellMainThread ShellExecutor mainExecutor) {
|
@ShellMainThread ShellExecutor mainExecutor,
|
||||||
|
@ShellMainThread Handler mainHandler) {
|
||||||
return Optional.of(
|
return Optional.of(
|
||||||
TvPipController.create(
|
TvPipController.create(
|
||||||
context,
|
context,
|
||||||
@@ -83,7 +84,8 @@ public abstract class TvPipModule {
|
|||||||
taskStackListener,
|
taskStackListener,
|
||||||
displayController,
|
displayController,
|
||||||
windowManagerShellWrapper,
|
windowManagerShellWrapper,
|
||||||
mainExecutor));
|
mainExecutor,
|
||||||
|
mainHandler));
|
||||||
}
|
}
|
||||||
|
|
||||||
@WMSingleton
|
@WMSingleton
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public class PipBoundsAlgorithm {
|
|||||||
/**
|
/**
|
||||||
* TODO: move the resources to SysUI package.
|
* TODO: move the resources to SysUI package.
|
||||||
*/
|
*/
|
||||||
protected void reloadResources(Context context) {
|
private void reloadResources(Context context) {
|
||||||
final Resources res = context.getResources();
|
final Resources res = context.getResources();
|
||||||
mDefaultAspectRatio = res.getFloat(
|
mDefaultAspectRatio = res.getFloat(
|
||||||
R.dimen.config_pictureInPictureDefaultAspectRatio);
|
R.dimen.config_pictureInPictureDefaultAspectRatio);
|
||||||
|
|||||||
@@ -55,11 +55,15 @@ public class PipBoundsState {
|
|||||||
public static final int STASH_TYPE_NONE = 0;
|
public static final int STASH_TYPE_NONE = 0;
|
||||||
public static final int STASH_TYPE_LEFT = 1;
|
public static final int STASH_TYPE_LEFT = 1;
|
||||||
public static final int STASH_TYPE_RIGHT = 2;
|
public static final int STASH_TYPE_RIGHT = 2;
|
||||||
|
public static final int STASH_TYPE_BOTTOM = 3;
|
||||||
|
public static final int STASH_TYPE_TOP = 4;
|
||||||
|
|
||||||
@IntDef(prefix = { "STASH_TYPE_" }, value = {
|
@IntDef(prefix = { "STASH_TYPE_" }, value = {
|
||||||
STASH_TYPE_NONE,
|
STASH_TYPE_NONE,
|
||||||
STASH_TYPE_LEFT,
|
STASH_TYPE_LEFT,
|
||||||
STASH_TYPE_RIGHT
|
STASH_TYPE_RIGHT,
|
||||||
|
STASH_TYPE_BOTTOM,
|
||||||
|
STASH_TYPE_TOP
|
||||||
})
|
})
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
public @interface StashType {}
|
public @interface StashType {}
|
||||||
|
|||||||
@@ -28,15 +28,21 @@ import static com.android.wm.shell.pip.tv.TvPipBoundsState.ORIENTATION_VERTICAL;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.util.ArraySet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Size;
|
import android.util.Size;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.android.wm.shell.R;
|
||||||
import com.android.wm.shell.common.DisplayLayout;
|
import com.android.wm.shell.common.DisplayLayout;
|
||||||
import com.android.wm.shell.pip.PipBoundsAlgorithm;
|
import com.android.wm.shell.pip.PipBoundsAlgorithm;
|
||||||
import com.android.wm.shell.pip.PipSnapAlgorithm;
|
import com.android.wm.shell.pip.PipSnapAlgorithm;
|
||||||
|
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains pip bounds calculations that are specific to TV.
|
* Contains pip bounds calculations that are specific to TV.
|
||||||
@@ -46,91 +52,129 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
|||||||
private static final String TAG = TvPipBoundsAlgorithm.class.getSimpleName();
|
private static final String TAG = TvPipBoundsAlgorithm.class.getSimpleName();
|
||||||
private static final boolean DEBUG = TvPipController.DEBUG;
|
private static final boolean DEBUG = TvPipController.DEBUG;
|
||||||
|
|
||||||
private final @android.annotation.NonNull TvPipBoundsState mTvPipBoundsState;
|
private final @NonNull TvPipBoundsState mTvPipBoundsState;
|
||||||
|
|
||||||
private int mFixedExpandedHeightInPx;
|
private int mFixedExpandedHeightInPx;
|
||||||
private int mFixedExpandedWidthInPx;
|
private int mFixedExpandedWidthInPx;
|
||||||
|
|
||||||
|
private final TvPipKeepClearAlgorithm mKeepClearAlgorithm;
|
||||||
|
|
||||||
public TvPipBoundsAlgorithm(Context context,
|
public TvPipBoundsAlgorithm(Context context,
|
||||||
@NonNull TvPipBoundsState tvPipBoundsState,
|
@NonNull TvPipBoundsState tvPipBoundsState,
|
||||||
@NonNull PipSnapAlgorithm pipSnapAlgorithm) {
|
@NonNull PipSnapAlgorithm pipSnapAlgorithm) {
|
||||||
super(context, tvPipBoundsState, pipSnapAlgorithm);
|
super(context, tvPipBoundsState, pipSnapAlgorithm);
|
||||||
this.mTvPipBoundsState = tvPipBoundsState;
|
this.mTvPipBoundsState = tvPipBoundsState;
|
||||||
|
this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(SystemClock::uptimeMillis);
|
||||||
|
reloadResources(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void reloadResources(Context context) {
|
||||||
protected void reloadResources(Context context) {
|
|
||||||
super.reloadResources(context);
|
|
||||||
final Resources res = context.getResources();
|
final Resources res = context.getResources();
|
||||||
mFixedExpandedHeightInPx = res.getDimensionPixelSize(
|
mFixedExpandedHeightInPx = res.getDimensionPixelSize(
|
||||||
com.android.internal.R.dimen.config_pictureInPictureExpandedHorizontalHeight);
|
com.android.internal.R.dimen.config_pictureInPictureExpandedHorizontalHeight);
|
||||||
mFixedExpandedWidthInPx = res.getDimensionPixelSize(
|
mFixedExpandedWidthInPx = res.getDimensionPixelSize(
|
||||||
com.android.internal.R.dimen.config_pictureInPictureExpandedVerticalWidth);
|
com.android.internal.R.dimen.config_pictureInPictureExpandedVerticalWidth);
|
||||||
|
mKeepClearAlgorithm.setPipAreaPadding(
|
||||||
|
res.getDimensionPixelSize(R.dimen.pip_keep_clear_area_padding));
|
||||||
|
mKeepClearAlgorithm.setMaxRestrictedDistanceFraction(
|
||||||
|
res.getFraction(R.fraction.config_pipMaxRestrictedMoveDistance, 1, 1));
|
||||||
|
mKeepClearAlgorithm.setStashDuration(res.getInteger(R.integer.config_pipStashDuration));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigurationChanged(Context context) {
|
||||||
|
super.onConfigurationChanged(context);
|
||||||
|
reloadResources(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the destination bounds to place the PIP window on entry. */
|
/** Returns the destination bounds to place the PIP window on entry. */
|
||||||
@Override
|
@Override
|
||||||
public Rect getEntryDestinationBounds() {
|
public Rect getEntryDestinationBounds() {
|
||||||
if (DEBUG) Log.d(TAG, "getEntryDestinationBounds()");
|
if (DEBUG) Log.d(TAG, "getEntryDestinationBounds()");
|
||||||
if (mTvPipBoundsState.getTvExpandedAspectRatio() != 0
|
if (mTvPipBoundsState.isTvExpandedPipSupported()
|
||||||
|
&& mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0
|
||||||
&& !mTvPipBoundsState.isTvPipManuallyCollapsed()) {
|
&& !mTvPipBoundsState.isTvPipManuallyCollapsed()) {
|
||||||
updatePositionOnExpandToggled(Gravity.NO_GRAVITY, true);
|
updateExpandedPipSize();
|
||||||
|
updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true);
|
||||||
|
mTvPipBoundsState.setTvPipExpanded(true);
|
||||||
}
|
}
|
||||||
return getTvPipBounds(true);
|
return getTvPipBounds().getBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the current bounds adjusted to the new aspect ratio, if valid. */
|
/** Returns the current bounds adjusted to the new aspect ratio, if valid. */
|
||||||
@Override
|
@Override
|
||||||
public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) {
|
public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) {
|
||||||
if (DEBUG) Log.d(TAG, "getAdjustedDestinationBounds: " + newAspectRatio);
|
if (DEBUG) Log.d(TAG, "getAdjustedDestinationBounds: " + newAspectRatio);
|
||||||
return getTvPipBounds(mTvPipBoundsState.isTvPipExpanded());
|
return getTvPipBounds().getBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The normal bounds at a different position on the screen.
|
* Calculates the PiP bounds.
|
||||||
*/
|
*/
|
||||||
public Rect getTvNormalBounds() {
|
public Placement getTvPipBounds() {
|
||||||
Rect normalBounds = getNormalBounds();
|
final Size pipSize = getPipSize();
|
||||||
Rect insetBounds = new Rect();
|
final Rect displayBounds = mTvPipBoundsState.getDisplayBounds();
|
||||||
|
final Size screenSize = new Size(displayBounds.width(), displayBounds.height());
|
||||||
|
final Rect insetBounds = new Rect();
|
||||||
getInsetBounds(insetBounds);
|
getInsetBounds(insetBounds);
|
||||||
|
|
||||||
|
Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas();
|
||||||
|
Set<Rect> unrestrictedKeepClearAreas = mTvPipBoundsState.getUnrestrictedKeepClearAreas();
|
||||||
|
|
||||||
if (mTvPipBoundsState.isImeShowing()) {
|
if (mTvPipBoundsState.isImeShowing()) {
|
||||||
if (DEBUG) Log.d(TAG, "IME showing, height: " + mTvPipBoundsState.getImeHeight());
|
if (DEBUG) Log.d(TAG, "IME showing, height: " + mTvPipBoundsState.getImeHeight());
|
||||||
insetBounds.bottom -= mTvPipBoundsState.getImeHeight();
|
|
||||||
|
final Rect imeBounds = new Rect(
|
||||||
|
0,
|
||||||
|
insetBounds.bottom - mTvPipBoundsState.getImeHeight(),
|
||||||
|
insetBounds.right,
|
||||||
|
insetBounds.bottom);
|
||||||
|
|
||||||
|
unrestrictedKeepClearAreas = new ArraySet<>(unrestrictedKeepClearAreas);
|
||||||
|
unrestrictedKeepClearAreas.add(imeBounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
Rect result = new Rect();
|
mKeepClearAlgorithm.setGravity(mTvPipBoundsState.getTvPipGravity());
|
||||||
Gravity.apply(mTvPipBoundsState.getTvPipGravity(), normalBounds.width(),
|
mKeepClearAlgorithm.setScreenSize(screenSize);
|
||||||
normalBounds.height(), insetBounds, result);
|
mKeepClearAlgorithm.setMovementBounds(insetBounds);
|
||||||
|
mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset());
|
||||||
|
|
||||||
|
final Placement placement = mKeepClearAlgorithm.calculatePipPosition(
|
||||||
|
pipSize,
|
||||||
|
restrictedKeepClearAreas,
|
||||||
|
unrestrictedKeepClearAreas);
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "normalBounds: " + normalBounds.toShortString());
|
Log.d(TAG, "pipSize: " + pipSize);
|
||||||
|
Log.d(TAG, "screenSize: " + screenSize);
|
||||||
|
Log.d(TAG, "stashOffset: " + mTvPipBoundsState.getStashOffset());
|
||||||
Log.d(TAG, "insetBounds: " + insetBounds.toShortString());
|
Log.d(TAG, "insetBounds: " + insetBounds.toShortString());
|
||||||
|
Log.d(TAG, "pipSize: " + pipSize);
|
||||||
Log.d(TAG, "gravity: " + Gravity.toString(mTvPipBoundsState.getTvPipGravity()));
|
Log.d(TAG, "gravity: " + Gravity.toString(mTvPipBoundsState.getTvPipGravity()));
|
||||||
Log.d(TAG, "resultBounds: " + result.toShortString());
|
Log.d(TAG, "restrictedKeepClearAreas: " + restrictedKeepClearAreas);
|
||||||
|
Log.d(TAG, "unrestrictedKeepClearAreas: " + unrestrictedKeepClearAreas);
|
||||||
|
Log.d(TAG, "placement: " + placement);
|
||||||
}
|
}
|
||||||
|
|
||||||
mTvPipBoundsState.setTvPipExpanded(false);
|
return placement;
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return previous gravity if it is to be saved, or Gravity.NO_GRAVITY if not.
|
* @return previous gravity if it is to be saved, or {@link Gravity#NO_GRAVITY} if not.
|
||||||
*/
|
*/
|
||||||
int updatePositionOnExpandToggled(int previousGravity, boolean expanding) {
|
int updateGravityOnExpandToggled(int previousGravity, boolean expanding) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "updatePositionOnExpandToggle(), expanding: " + expanding
|
Log.d(TAG, "updateGravityOnExpandToggled(), expanding: " + expanding
|
||||||
+ ", mOrientation: " + mTvPipBoundsState.getTvFixedPipOrientation()
|
+ ", mOrientation: " + mTvPipBoundsState.getTvFixedPipOrientation()
|
||||||
+ ", previous gravity: " + Gravity.toString(previousGravity));
|
+ ", previous gravity: " + Gravity.toString(previousGravity));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mTvPipBoundsState.isTvExpandedPipEnabled()) {
|
if (!mTvPipBoundsState.isTvExpandedPipSupported()) {
|
||||||
return Gravity.NO_GRAVITY;
|
return Gravity.NO_GRAVITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expanding && mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_UNDETERMINED) {
|
if (expanding && mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_UNDETERMINED) {
|
||||||
float expandedRatio = mTvPipBoundsState.getTvExpandedAspectRatio();
|
float expandedRatio = mTvPipBoundsState.getDesiredTvExpandedAspectRatio();
|
||||||
if (expandedRatio == 0) {
|
if (expandedRatio == 0) {
|
||||||
return Gravity.NO_GRAVITY;
|
return Gravity.NO_GRAVITY;
|
||||||
}
|
}
|
||||||
@@ -139,7 +183,6 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
|||||||
} else {
|
} else {
|
||||||
mTvPipBoundsState.setTvFixedPipOrientation(ORIENTATION_HORIZONTAL);
|
mTvPipBoundsState.setTvFixedPipOrientation(ORIENTATION_HORIZONTAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int gravityToSave = Gravity.NO_GRAVITY;
|
int gravityToSave = Gravity.NO_GRAVITY;
|
||||||
@@ -181,10 +224,10 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return true if position changed
|
* @return true if gravity changed
|
||||||
*/
|
*/
|
||||||
boolean updatePosition(int keycode) {
|
boolean updateGravity(int keycode) {
|
||||||
if (DEBUG) Log.d(TAG, "updatePosition, keycode: " + keycode);
|
if (DEBUG) Log.d(TAG, "updateGravity, keycode: " + keycode);
|
||||||
|
|
||||||
// Check if position change is valid
|
// Check if position change is valid
|
||||||
if (mTvPipBoundsState.isTvPipExpanded()) {
|
if (mTvPipBoundsState.isTvPipExpanded()) {
|
||||||
@@ -247,26 +290,31 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Size getPipSize() {
|
||||||
|
final boolean isExpanded =
|
||||||
|
mTvPipBoundsState.isTvExpandedPipSupported() && mTvPipBoundsState.isTvPipExpanded()
|
||||||
|
&& mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0;
|
||||||
|
if (isExpanded) {
|
||||||
|
return mTvPipBoundsState.getTvExpandedSize();
|
||||||
|
} else {
|
||||||
|
final Rect normalBounds = getNormalBounds();
|
||||||
|
return new Size(normalBounds.width(), normalBounds.height());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the PiP bounds.
|
* Updates {@link TvPipBoundsState#getTvExpandedSize()} based on
|
||||||
|
* {@link TvPipBoundsState#getDesiredTvExpandedAspectRatio()}, the screen size.
|
||||||
*/
|
*/
|
||||||
public Rect getTvPipBounds(boolean expandedIfPossible) {
|
void updateExpandedPipSize() {
|
||||||
if (DEBUG) {
|
final DisplayLayout displayLayout = mTvPipBoundsState.getDisplayLayout();
|
||||||
Log.d(TAG, "getExpandedBoundsIfPossible with gravity "
|
final float expandedRatio =
|
||||||
+ Gravity.toString(mTvPipBoundsState.getTvPipGravity())
|
mTvPipBoundsState.getDesiredTvExpandedAspectRatio(); // width / height
|
||||||
+ ", fixed orientation: " + mTvPipBoundsState.getTvFixedPipOrientation());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mTvPipBoundsState.isTvExpandedPipEnabled() || !expandedIfPossible) {
|
final Size expandedSize;
|
||||||
return getTvNormalBounds();
|
|
||||||
}
|
|
||||||
|
|
||||||
DisplayLayout displayLayout = mTvPipBoundsState.getDisplayLayout();
|
|
||||||
float expandedRatio = mTvPipBoundsState.getTvExpandedAspectRatio(); // width / height
|
|
||||||
Size expandedSize;
|
|
||||||
if (expandedRatio == 0) {
|
if (expandedRatio == 0) {
|
||||||
Log.d(TAG, "Expanded mode not supported");
|
Log.d(TAG, "updateExpandedPipSize(): Expanded mode aspect ratio of 0 not supported");
|
||||||
return getTvNormalBounds();
|
return;
|
||||||
} else if (expandedRatio < 1) {
|
} else if (expandedRatio < 1) {
|
||||||
// vertical
|
// vertical
|
||||||
if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
|
if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
|
||||||
@@ -300,26 +348,14 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expandedSize == null) {
|
|
||||||
return getTvNormalBounds();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "expanded size, width: " + expandedSize.getWidth()
|
|
||||||
+ ", height: " + expandedSize.getHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
Rect insetBounds = new Rect();
|
|
||||||
getInsetBounds(insetBounds);
|
|
||||||
|
|
||||||
Rect expandedBounds = new Rect();
|
|
||||||
Gravity.apply(mTvPipBoundsState.getTvPipGravity(), expandedSize.getWidth(),
|
|
||||||
expandedSize.getHeight(), insetBounds, expandedBounds);
|
|
||||||
if (DEBUG) Log.d(TAG, "expanded bounds: " + expandedBounds.toShortString());
|
|
||||||
|
|
||||||
mTvPipBoundsState.setTvExpandedSize(expandedSize);
|
mTvPipBoundsState.setTvExpandedSize(expandedSize);
|
||||||
mTvPipBoundsState.setTvPipExpanded(true);
|
if (DEBUG) {
|
||||||
return expandedBounds;
|
Log.d(TAG, "updateExpandedPipSize(): expanded size, width=" + expandedSize.getWidth()
|
||||||
|
+ ", height=" + expandedSize.getHeight());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void keepUnstashedForCurrentKeepClearAreas() {
|
||||||
|
mKeepClearAlgorithm.keepUnstashedForCurrentKeepClearAreas();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,10 +53,10 @@ public class TvPipBoundsState extends PipBoundsState {
|
|||||||
|
|
||||||
public static final int DEFAULT_TV_GRAVITY = Gravity.BOTTOM | Gravity.RIGHT;
|
public static final int DEFAULT_TV_GRAVITY = Gravity.BOTTOM | Gravity.RIGHT;
|
||||||
|
|
||||||
private boolean mIsTvExpandedPipEnabled;
|
private final boolean mIsTvExpandedPipSupported;
|
||||||
private boolean mIsTvPipExpanded;
|
private boolean mIsTvPipExpanded;
|
||||||
private boolean mTvPipManuallyCollapsed;
|
private boolean mTvPipManuallyCollapsed;
|
||||||
private float mTvExpandedAspectRatio;
|
private float mDesiredTvExpandedAspectRatio;
|
||||||
private @Orientation int mTvFixedPipOrientation;
|
private @Orientation int mTvFixedPipOrientation;
|
||||||
private int mTvPipGravity;
|
private int mTvPipGravity;
|
||||||
private @Nullable Size mTvExpandedSize;
|
private @Nullable Size mTvExpandedSize;
|
||||||
@@ -64,8 +64,8 @@ public class TvPipBoundsState extends PipBoundsState {
|
|||||||
|
|
||||||
public TvPipBoundsState(@NonNull Context context) {
|
public TvPipBoundsState(@NonNull Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
setIsTvExpandedPipEnabled(context.getPackageManager().hasSystemFeature(
|
mIsTvExpandedPipSupported = context.getPackageManager().hasSystemFeature(
|
||||||
PackageManager.FEATURE_EXPANDED_PICTURE_IN_PICTURE));
|
PackageManager.FEATURE_EXPANDED_PICTURE_IN_PICTURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +75,7 @@ public class TvPipBoundsState extends PipBoundsState {
|
|||||||
public void setBoundsStateForEntry(ComponentName componentName, ActivityInfo activityInfo,
|
public void setBoundsStateForEntry(ComponentName componentName, ActivityInfo activityInfo,
|
||||||
PictureInPictureParams params, PipBoundsAlgorithm pipBoundsAlgorithm) {
|
PictureInPictureParams params, PipBoundsAlgorithm pipBoundsAlgorithm) {
|
||||||
super.setBoundsStateForEntry(componentName, activityInfo, params, pipBoundsAlgorithm);
|
super.setBoundsStateForEntry(componentName, activityInfo, params, pipBoundsAlgorithm);
|
||||||
setTvExpandedAspectRatio(params.getExpandedAspectRatio(), true);
|
setDesiredTvExpandedAspectRatio(params.getExpandedAspectRatio(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resets the TV PiP state for a new activity. */
|
/** Resets the TV PiP state for a new activity. */
|
||||||
@@ -85,32 +85,32 @@ public class TvPipBoundsState extends PipBoundsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Set the tv expanded bounds of PIP */
|
/** Set the tv expanded bounds of PIP */
|
||||||
public void setTvExpandedSize(@Nullable Size bounds) {
|
public void setTvExpandedSize(@Nullable Size size) {
|
||||||
mTvExpandedSize = bounds;
|
mTvExpandedSize = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the PIP tv expanded bounds. */
|
/** Get the expanded size of the PiP. */
|
||||||
@Nullable
|
@Nullable
|
||||||
public Size getTvExpandedSize() {
|
public Size getTvExpandedSize() {
|
||||||
return mTvExpandedSize;
|
return mTvExpandedSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set the PIP aspect ratio for the expanded PIP (TV) that is desired by the app. */
|
/** Set the PIP aspect ratio for the expanded PIP (TV) that is desired by the app. */
|
||||||
public void setTvExpandedAspectRatio(float aspectRatio, boolean override) {
|
public void setDesiredTvExpandedAspectRatio(float aspectRatio, boolean override) {
|
||||||
if (override || mTvFixedPipOrientation == ORIENTATION_UNDETERMINED || aspectRatio == 0) {
|
if (override || mTvFixedPipOrientation == ORIENTATION_UNDETERMINED || aspectRatio == 0) {
|
||||||
mTvExpandedAspectRatio = aspectRatio;
|
mDesiredTvExpandedAspectRatio = aspectRatio;
|
||||||
resetTvPipState();
|
resetTvPipState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((aspectRatio > 1 && mTvFixedPipOrientation == ORIENTATION_HORIZONTAL)
|
if ((aspectRatio > 1 && mTvFixedPipOrientation == ORIENTATION_HORIZONTAL)
|
||||||
|| (aspectRatio <= 1 && mTvFixedPipOrientation == ORIENTATION_VERTICAL)) {
|
|| (aspectRatio <= 1 && mTvFixedPipOrientation == ORIENTATION_VERTICAL)) {
|
||||||
mTvExpandedAspectRatio = aspectRatio;
|
mDesiredTvExpandedAspectRatio = aspectRatio;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the PIP aspect ratio for the expanded PIP (TV) that is desired by the app. */
|
/** Get the PIP aspect ratio for the expanded PIP (TV) that is desired by the app. */
|
||||||
public float getTvExpandedAspectRatio() {
|
public float getDesiredTvExpandedAspectRatio() {
|
||||||
return mTvExpandedAspectRatio;
|
return mDesiredTvExpandedAspectRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the orientation the expanded TV PiP activity has been fixed to. */
|
/** Sets the orientation the expanded TV PiP activity has been fixed to. */
|
||||||
@@ -154,13 +154,9 @@ public class TvPipBoundsState extends PipBoundsState {
|
|||||||
return mTvPipManuallyCollapsed;
|
return mTvPipManuallyCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets whether expanded PiP is supported by the device. */
|
/** Returns whether expanded PiP is supported by the device. */
|
||||||
public void setIsTvExpandedPipEnabled(boolean enabled) {
|
public boolean isTvExpandedPipSupported() {
|
||||||
mIsTvExpandedPipEnabled = enabled;
|
return mIsTvExpandedPipSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns whether expanded PiP is supported by the device. */
|
|
||||||
public boolean isTvExpandedPipEnabled() {
|
|
||||||
return mIsTvExpandedPipEnabled;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ import android.content.pm.ParceledListSlice;
|
|||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.os.Handler;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.DisplayInfo;
|
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
|
|
||||||
import com.android.wm.shell.R;
|
import com.android.wm.shell.R;
|
||||||
@@ -45,9 +45,11 @@ import com.android.wm.shell.common.TaskStackListenerImpl;
|
|||||||
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
|
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
|
||||||
import com.android.wm.shell.pip.Pip;
|
import com.android.wm.shell.pip.Pip;
|
||||||
import com.android.wm.shell.pip.PipAnimationController;
|
import com.android.wm.shell.pip.PipAnimationController;
|
||||||
|
import com.android.wm.shell.pip.PipBoundsState;
|
||||||
import com.android.wm.shell.pip.PipMediaController;
|
import com.android.wm.shell.pip.PipMediaController;
|
||||||
import com.android.wm.shell.pip.PipTaskOrganizer;
|
import com.android.wm.shell.pip.PipTaskOrganizer;
|
||||||
import com.android.wm.shell.pip.PipTransitionController;
|
import com.android.wm.shell.pip.PipTransitionController;
|
||||||
|
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
|
||||||
|
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
@@ -62,6 +64,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
private static final String TAG = "TvPipController";
|
private static final String TAG = "TvPipController";
|
||||||
static final boolean DEBUG = false;
|
static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
private static final double EPS = 1e-7;
|
||||||
private static final int NONEXISTENT_TASK_ID = -1;
|
private static final int NONEXISTENT_TASK_ID = -1;
|
||||||
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@@ -97,11 +100,13 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
private final TvPipNotificationController mPipNotificationController;
|
private final TvPipNotificationController mPipNotificationController;
|
||||||
private final TvPipMenuController mTvPipMenuController;
|
private final TvPipMenuController mTvPipMenuController;
|
||||||
private final ShellExecutor mMainExecutor;
|
private final ShellExecutor mMainExecutor;
|
||||||
|
private final Handler mMainHandler;
|
||||||
private final TvPipImpl mImpl = new TvPipImpl();
|
private final TvPipImpl mImpl = new TvPipImpl();
|
||||||
|
|
||||||
private @State int mState = STATE_NO_PIP;
|
private @State int mState = STATE_NO_PIP;
|
||||||
private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY;
|
private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY;
|
||||||
private int mPinnedTaskId = NONEXISTENT_TASK_ID;
|
private int mPinnedTaskId = NONEXISTENT_TASK_ID;
|
||||||
|
private Runnable mUnstashRunnable;
|
||||||
|
|
||||||
private int mResizeAnimationDuration;
|
private int mResizeAnimationDuration;
|
||||||
|
|
||||||
@@ -117,7 +122,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
TaskStackListenerImpl taskStackListener,
|
TaskStackListenerImpl taskStackListener,
|
||||||
DisplayController displayController,
|
DisplayController displayController,
|
||||||
WindowManagerShellWrapper wmShell,
|
WindowManagerShellWrapper wmShell,
|
||||||
ShellExecutor mainExecutor) {
|
ShellExecutor mainExecutor,
|
||||||
|
Handler mainHandler) {
|
||||||
return new TvPipController(
|
return new TvPipController(
|
||||||
context,
|
context,
|
||||||
tvPipBoundsState,
|
tvPipBoundsState,
|
||||||
@@ -130,7 +136,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
taskStackListener,
|
taskStackListener,
|
||||||
displayController,
|
displayController,
|
||||||
wmShell,
|
wmShell,
|
||||||
mainExecutor).mImpl;
|
mainExecutor,
|
||||||
|
mainHandler).mImpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TvPipController(
|
private TvPipController(
|
||||||
@@ -145,9 +152,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
TaskStackListenerImpl taskStackListener,
|
TaskStackListenerImpl taskStackListener,
|
||||||
DisplayController displayController,
|
DisplayController displayController,
|
||||||
WindowManagerShellWrapper wmShell,
|
WindowManagerShellWrapper wmShell,
|
||||||
ShellExecutor mainExecutor) {
|
ShellExecutor mainExecutor,
|
||||||
|
Handler mainHandler) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mMainExecutor = mainExecutor;
|
mMainExecutor = mainExecutor;
|
||||||
|
mMainHandler = mainHandler;
|
||||||
|
|
||||||
mTvPipBoundsState = tvPipBoundsState;
|
mTvPipBoundsState = tvPipBoundsState;
|
||||||
mTvPipBoundsState.setDisplayId(context.getDisplayId());
|
mTvPipBoundsState.setDisplayId(context.getDisplayId());
|
||||||
@@ -182,6 +191,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
|
|
||||||
loadConfigurations();
|
loadConfigurations();
|
||||||
mPipNotificationController.onConfigurationChanged(mContext);
|
mPipNotificationController.onConfigurationChanged(mContext);
|
||||||
|
mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,13 +216,20 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
}
|
}
|
||||||
|
|
||||||
setState(STATE_PIP_MENU);
|
setState(STATE_PIP_MENU);
|
||||||
movePinnedStack();
|
updatePinnedStackBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void closeMenu() {
|
public void closeMenu() {
|
||||||
if (DEBUG) Log.d(TAG, "closeMenu(), state before=" + stateToName(mState));
|
if (DEBUG) Log.d(TAG, "closeMenu(), state before=" + stateToName(mState));
|
||||||
setState(STATE_PIP);
|
setState(STATE_PIP);
|
||||||
|
mTvPipBoundsAlgorithm.keepUnstashedForCurrentKeepClearAreas();
|
||||||
|
updatePinnedStackBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInMoveModeChanged() {
|
||||||
|
updatePinnedStackBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,21 +248,21 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
if (DEBUG) Log.d(TAG, "togglePipExpansion()");
|
if (DEBUG) Log.d(TAG, "togglePipExpansion()");
|
||||||
boolean expanding = !mTvPipBoundsState.isTvPipExpanded();
|
boolean expanding = !mTvPipBoundsState.isTvPipExpanded();
|
||||||
int saveGravity = mTvPipBoundsAlgorithm
|
int saveGravity = mTvPipBoundsAlgorithm
|
||||||
.updatePositionOnExpandToggled(mPreviousGravity, expanding);
|
.updateGravityOnExpandToggled(mPreviousGravity, expanding);
|
||||||
if (saveGravity != Gravity.NO_GRAVITY) {
|
if (saveGravity != Gravity.NO_GRAVITY) {
|
||||||
mPreviousGravity = saveGravity;
|
mPreviousGravity = saveGravity;
|
||||||
}
|
}
|
||||||
mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
|
mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
|
||||||
mTvPipBoundsState.setTvPipExpanded(expanding);
|
mTvPipBoundsState.setTvPipExpanded(expanding);
|
||||||
movePinnedStack();
|
updatePinnedStackBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void movePip(int keycode) {
|
public void movePip(int keycode) {
|
||||||
if (mTvPipBoundsAlgorithm.updatePosition(keycode)) {
|
if (mTvPipBoundsAlgorithm.updateGravity(keycode)) {
|
||||||
mTvPipMenuController.updateGravity(mTvPipBoundsState.getTvPipGravity());
|
mTvPipMenuController.updateGravity(mTvPipBoundsState.getTvPipGravity());
|
||||||
mPreviousGravity = Gravity.NO_GRAVITY;
|
mPreviousGravity = Gravity.NO_GRAVITY;
|
||||||
movePinnedStack();
|
updatePinnedStackBounds();
|
||||||
} else {
|
} else {
|
||||||
if (DEBUG) Log.d(TAG, "Position hasn't changed");
|
if (DEBUG) Log.d(TAG, "Position hasn't changed");
|
||||||
}
|
}
|
||||||
@@ -265,20 +282,48 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
Set<Rect> unrestricted) {
|
Set<Rect> unrestricted) {
|
||||||
if (mTvPipBoundsState.getDisplayId() == displayId) {
|
if (mTvPipBoundsState.getDisplayId() == displayId) {
|
||||||
mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted);
|
mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted);
|
||||||
movePinnedStack();
|
updatePinnedStackBounds();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Animate to the updated position of the PiP based on the state and position of the PiP.
|
* Update the PiP bounds based on the state of the PiP and keep clear areas.
|
||||||
|
* Animates to the current PiP bounds, and schedules unstashing the PiP if necessary.
|
||||||
*/
|
*/
|
||||||
private void movePinnedStack() {
|
private void updatePinnedStackBounds() {
|
||||||
if (mState == STATE_NO_PIP) {
|
if (mState == STATE_NO_PIP) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Rect bounds = mTvPipBoundsAlgorithm.getTvPipBounds(mTvPipBoundsState.isTvPipExpanded());
|
final boolean stayAtAnchorPosition = mTvPipMenuController.isInMoveMode();
|
||||||
if (DEBUG) Log.d(TAG, "movePinnedStack() - new pip bounds: " + bounds.toShortString());
|
final boolean disallowStashing = mState == STATE_PIP_MENU || stayAtAnchorPosition;
|
||||||
|
final Placement placement = mTvPipBoundsAlgorithm.getTvPipBounds();
|
||||||
|
|
||||||
|
int stashType =
|
||||||
|
disallowStashing ? PipBoundsState.STASH_TYPE_NONE : placement.getStashType();
|
||||||
|
mTvPipBoundsState.setStashed(stashType);
|
||||||
|
|
||||||
|
if (stayAtAnchorPosition) {
|
||||||
|
movePinnedStackTo(placement.getAnchorBounds());
|
||||||
|
} else if (disallowStashing) {
|
||||||
|
movePinnedStackTo(placement.getUnstashedBounds());
|
||||||
|
} else {
|
||||||
|
movePinnedStackTo(placement.getBounds());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mUnstashRunnable != null) {
|
||||||
|
mMainHandler.removeCallbacks(mUnstashRunnable);
|
||||||
|
mUnstashRunnable = null;
|
||||||
|
}
|
||||||
|
if (!disallowStashing && placement.getUnstashDestinationBounds() != null) {
|
||||||
|
mUnstashRunnable = () -> movePinnedStackTo(placement.getUnstashDestinationBounds());
|
||||||
|
mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Animates the PiP to the given bounds. */
|
||||||
|
private void movePinnedStackTo(Rect bounds) {
|
||||||
|
if (DEBUG) Log.d(TAG, "movePinnedStackTo() - new pip bounds: " + bounds.toShortString());
|
||||||
mPipTaskOrganizer.scheduleAnimateResizePip(bounds,
|
mPipTaskOrganizer.scheduleAnimateResizePip(bounds,
|
||||||
mResizeAnimationDuration, rect -> {
|
mResizeAnimationDuration, rect -> {
|
||||||
if (DEBUG) Log.d(TAG, "movePinnedStack() animation done");
|
if (DEBUG) Log.d(TAG, "movePinnedStack() animation done");
|
||||||
@@ -359,6 +404,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
if (DEBUG) Log.d(TAG, " > show menu");
|
if (DEBUG) Log.d(TAG, " > show menu");
|
||||||
mTvPipMenuController.showMenu();
|
mTvPipMenuController.showMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatePinnedStackBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadConfigurations() {
|
private void loadConfigurations() {
|
||||||
@@ -366,12 +413,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
|
mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DisplayInfo getDisplayInfo() {
|
|
||||||
final DisplayInfo displayInfo = new DisplayInfo();
|
|
||||||
mContext.getDisplay().getDisplayInfo(displayInfo);
|
|
||||||
return displayInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
|
private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
|
||||||
taskStackListener.addListener(new TaskStackListenerCallback() {
|
taskStackListener.addListener(new TaskStackListenerCallback() {
|
||||||
@Override
|
@Override
|
||||||
@@ -417,7 +458,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
mTvPipBoundsState.setImeVisibility(imeVisible, imeHeight);
|
mTvPipBoundsState.setImeVisibility(imeVisible, imeHeight);
|
||||||
|
|
||||||
if (mState != STATE_NO_PIP) {
|
if (mState != STATE_NO_PIP) {
|
||||||
movePinnedStack();
|
updatePinnedStackBounds();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +470,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
mTvPipBoundsState.setAspectRatio(ratio);
|
mTvPipBoundsState.setAspectRatio(ratio);
|
||||||
|
|
||||||
if (!mTvPipBoundsState.isTvPipExpanded() && ratioChanged) {
|
if (!mTvPipBoundsState.isTvPipExpanded() && ratioChanged) {
|
||||||
movePinnedStack();
|
updatePinnedStackBounds();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,41 +479,45 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
if (DEBUG) Log.d(TAG, "onExpandedAspectRatioChanged: " + ratio);
|
if (DEBUG) Log.d(TAG, "onExpandedAspectRatioChanged: " + ratio);
|
||||||
|
|
||||||
// 0) No update to the ratio --> don't do anything
|
// 0) No update to the ratio --> don't do anything
|
||||||
if (mTvPipBoundsState.getTvExpandedAspectRatio() == ratio) {
|
|
||||||
|
if (Math.abs(mTvPipBoundsState.getDesiredTvExpandedAspectRatio() - ratio)
|
||||||
|
< EPS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mTvPipBoundsState.setTvExpandedAspectRatio(ratio, false);
|
mTvPipBoundsState.setDesiredTvExpandedAspectRatio(ratio, false);
|
||||||
|
|
||||||
// 1) PiP is expanded and only aspect ratio changed, but wasn't disabled
|
// 1) PiP is expanded and only aspect ratio changed, but wasn't disabled
|
||||||
// --> update bounds, but don't toggle
|
// --> update bounds, but don't toggle
|
||||||
if (mTvPipBoundsState.isTvPipExpanded() && ratio != 0) {
|
if (mTvPipBoundsState.isTvPipExpanded() && ratio != 0) {
|
||||||
movePinnedStack();
|
mTvPipBoundsAlgorithm.updateExpandedPipSize();
|
||||||
|
updatePinnedStackBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) PiP is expanded, but expanded PiP was disabled
|
// 2) PiP is expanded, but expanded PiP was disabled
|
||||||
// --> collapse PiP
|
// --> collapse PiP
|
||||||
if (mTvPipBoundsState.isTvPipExpanded() && ratio == 0) {
|
if (mTvPipBoundsState.isTvPipExpanded() && ratio == 0) {
|
||||||
int saveGravity = mTvPipBoundsAlgorithm
|
int saveGravity = mTvPipBoundsAlgorithm
|
||||||
.updatePositionOnExpandToggled(mPreviousGravity, false);
|
.updateGravityOnExpandToggled(mPreviousGravity, false);
|
||||||
if (saveGravity != Gravity.NO_GRAVITY) {
|
if (saveGravity != Gravity.NO_GRAVITY) {
|
||||||
mPreviousGravity = saveGravity;
|
mPreviousGravity = saveGravity;
|
||||||
}
|
}
|
||||||
mTvPipBoundsState.setTvPipExpanded(false);
|
mTvPipBoundsState.setTvPipExpanded(false);
|
||||||
movePinnedStack();
|
updatePinnedStackBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) PiP not expanded and not manually collapsed and expand was enabled
|
// 3) PiP not expanded and not manually collapsed and expand was enabled
|
||||||
// --> expand to new ratio
|
// --> expand to new ratio
|
||||||
if (!mTvPipBoundsState.isTvPipExpanded() && ratio != 0
|
if (!mTvPipBoundsState.isTvPipExpanded() && ratio != 0
|
||||||
&& !mTvPipBoundsState.isTvPipManuallyCollapsed()) {
|
&& !mTvPipBoundsState.isTvPipManuallyCollapsed()) {
|
||||||
|
mTvPipBoundsAlgorithm.updateExpandedPipSize();
|
||||||
int saveGravity = mTvPipBoundsAlgorithm
|
int saveGravity = mTvPipBoundsAlgorithm
|
||||||
.updatePositionOnExpandToggled(mPreviousGravity, true);
|
.updateGravityOnExpandToggled(mPreviousGravity, true);
|
||||||
if (saveGravity != Gravity.NO_GRAVITY) {
|
if (saveGravity != Gravity.NO_GRAVITY) {
|
||||||
mPreviousGravity = saveGravity;
|
mPreviousGravity = saveGravity;
|
||||||
}
|
}
|
||||||
mTvPipBoundsState.setTvPipExpanded(true);
|
mTvPipBoundsState.setTvPipExpanded(true);
|
||||||
movePinnedStack();
|
updatePinnedStackBounds();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,741 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.wm.shell.pip.tv
|
||||||
|
|
||||||
|
import android.graphics.Point
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.util.Size
|
||||||
|
import android.view.Gravity
|
||||||
|
import com.android.wm.shell.pip.PipBoundsState
|
||||||
|
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_BOTTOM
|
||||||
|
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_LEFT
|
||||||
|
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE
|
||||||
|
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT
|
||||||
|
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_TOP
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private const val DEFAULT_PIP_MARGINS = 48
|
||||||
|
private const val DEFAULT_STASH_DURATION = 5000L
|
||||||
|
private const val RELAX_DEPTH = 1
|
||||||
|
private const val DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION = 0.15
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class calculates an appropriate position for a Picture-In-Picture (PiP) window, taking
|
||||||
|
* into account app defined keep clear areas.
|
||||||
|
*
|
||||||
|
* @param clock A function returning a current timestamp (in milliseconds)
|
||||||
|
*/
|
||||||
|
class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||||
|
/**
|
||||||
|
* Result of the positioning algorithm.
|
||||||
|
*
|
||||||
|
* @param bounds The bounds the PiP should be placed at
|
||||||
|
* @param anchorBounds The bounds of the PiP anchor position
|
||||||
|
* (where the PiP would be placed if there were no keep clear areas)
|
||||||
|
* @param stashType Where the PiP has been stashed, if at all
|
||||||
|
* @param unstashDestinationBounds If stashed, the PiP should move to this position after
|
||||||
|
* [stashDuration] has passed.
|
||||||
|
* @param unstashTime If stashed, the time at which the PiP should move
|
||||||
|
* to [unstashDestinationBounds]
|
||||||
|
*/
|
||||||
|
data class Placement(
|
||||||
|
val bounds: Rect,
|
||||||
|
val anchorBounds: Rect,
|
||||||
|
@PipBoundsState.StashType val stashType: Int = STASH_TYPE_NONE,
|
||||||
|
val unstashDestinationBounds: Rect? = null,
|
||||||
|
val unstashTime: Long = 0L
|
||||||
|
) {
|
||||||
|
/** Bounds to use if the PiP should not be stashed. */
|
||||||
|
fun getUnstashedBounds() = unstashDestinationBounds ?: bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The size of the screen */
|
||||||
|
private var screenSize = Size(0, 0)
|
||||||
|
|
||||||
|
/** The bounds the PiP is allowed to move in */
|
||||||
|
private var movementBounds = Rect()
|
||||||
|
|
||||||
|
/** Padding to add between a keep clear area that caused the PiP to move and the PiP */
|
||||||
|
var pipAreaPadding = DEFAULT_PIP_MARGINS
|
||||||
|
|
||||||
|
/** The distance the PiP peeks into the screen when stashed */
|
||||||
|
var stashOffset = DEFAULT_PIP_MARGINS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long (in milliseconds) the PiP should stay stashed for after the last time the
|
||||||
|
* keep clear areas causing the PiP to stash have changed.
|
||||||
|
*/
|
||||||
|
var stashDuration = DEFAULT_STASH_DURATION
|
||||||
|
|
||||||
|
/** The fraction of screen width/height restricted keep clear areas can move the PiP */
|
||||||
|
var maxRestrictedDistanceFraction = DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION
|
||||||
|
|
||||||
|
private var pipGravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
private var transformedScreenBounds = Rect()
|
||||||
|
private var transformedMovementBounds = Rect()
|
||||||
|
|
||||||
|
private var lastAreasOverlappingUnstashPosition: Set<Rect> = emptySet()
|
||||||
|
private var lastStashTime: Long = Long.MIN_VALUE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the position the PiP should be placed at, taking into consideration the
|
||||||
|
* given keep clear areas.
|
||||||
|
*
|
||||||
|
* Restricted keep clear areas can move the PiP only by a limited amount, and may be ignored
|
||||||
|
* if there is no space for the PiP to move to.
|
||||||
|
* Apps holding the permission [android.Manifest.permission.USE_UNRESTRICTED_KEEP_CLEAR_AREAS]
|
||||||
|
* can declare unrestricted keep clear areas, which can move the PiP farther and placement will
|
||||||
|
* always try to respect these areas.
|
||||||
|
*
|
||||||
|
* If no free space the PiP is allowed to move to can be found, a stashed position is returned
|
||||||
|
* as [Placement.bounds], along with a position to move to once [Placement.unstashTime] has
|
||||||
|
* passed as [Placement.unstashDestinationBounds].
|
||||||
|
*
|
||||||
|
* @param pipSize The size of the PiP window
|
||||||
|
* @param restrictedAreas The restricted keep clear areas
|
||||||
|
* @param unrestrictedAreas The unrestricted keep clear areas
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun calculatePipPosition(
|
||||||
|
pipSize: Size,
|
||||||
|
restrictedAreas: Set<Rect>,
|
||||||
|
unrestrictedAreas: Set<Rect>
|
||||||
|
): Placement {
|
||||||
|
val transformedRestrictedAreas = transformAndFilterAreas(restrictedAreas)
|
||||||
|
val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas)
|
||||||
|
val pipAnchorBounds = getNormalPipAnchorBounds(pipSize, transformedMovementBounds)
|
||||||
|
|
||||||
|
val result = calculatePipPositionTransformed(
|
||||||
|
pipAnchorBounds,
|
||||||
|
transformedRestrictedAreas,
|
||||||
|
transformedUnrestrictedAreas
|
||||||
|
)
|
||||||
|
|
||||||
|
val screenSpaceBounds = fromTransformedSpace(result.bounds)
|
||||||
|
return Placement(
|
||||||
|
screenSpaceBounds,
|
||||||
|
fromTransformedSpace(result.anchorBounds),
|
||||||
|
getStashType(screenSpaceBounds, movementBounds),
|
||||||
|
result.unstashDestinationBounds?.let { fromTransformedSpace(it) },
|
||||||
|
result.unstashTime
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out areas that encompass the entire movement bounds and returns them mapped to
|
||||||
|
* the base case space.
|
||||||
|
*
|
||||||
|
* Areas encompassing the entire movement bounds can occur when a full-screen View gets focused,
|
||||||
|
* but we don't want this to cause the PiP to get stashed.
|
||||||
|
*/
|
||||||
|
private fun transformAndFilterAreas(areas: Set<Rect>): Set<Rect> {
|
||||||
|
return areas.mapNotNullTo(mutableSetOf()) {
|
||||||
|
when {
|
||||||
|
it.contains(movementBounds) -> null
|
||||||
|
else -> toTransformedSpace(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the position the PiP should be placed at, taking into consideration the
|
||||||
|
* given keep clear areas.
|
||||||
|
* All parameters are transformed from screen space to the base case space, where the PiP
|
||||||
|
* anchor is in the bottom right corner / on the right side.
|
||||||
|
*
|
||||||
|
* @see [calculatePipPosition]
|
||||||
|
*/
|
||||||
|
private fun calculatePipPositionTransformed(
|
||||||
|
pipAnchorBounds: Rect,
|
||||||
|
restrictedAreas: Set<Rect>,
|
||||||
|
unrestrictedAreas: Set<Rect>
|
||||||
|
): Placement {
|
||||||
|
if (restrictedAreas.isEmpty() && unrestrictedAreas.isEmpty()) {
|
||||||
|
return Placement(pipAnchorBounds, pipAnchorBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try to find a free position to move to
|
||||||
|
val freeMovePos = findFreeMovePosition(pipAnchorBounds, restrictedAreas, unrestrictedAreas)
|
||||||
|
if (freeMovePos != null) {
|
||||||
|
lastAreasOverlappingUnstashPosition = emptySet()
|
||||||
|
return Placement(freeMovePos, pipAnchorBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no free position is found, we have to stash the PiP.
|
||||||
|
// Find the position the PiP should return to once it unstashes by doing a relaxed
|
||||||
|
// search, or ignoring restricted areas, or returning to the anchor position
|
||||||
|
val unstashBounds =
|
||||||
|
findRelaxedMovePosition(pipAnchorBounds, restrictedAreas, unrestrictedAreas)
|
||||||
|
?: findFreeMovePosition(pipAnchorBounds, emptySet(), unrestrictedAreas)
|
||||||
|
?: pipAnchorBounds
|
||||||
|
|
||||||
|
val keepClearAreas = restrictedAreas + unrestrictedAreas
|
||||||
|
val areasOverlappingUnstashPosition =
|
||||||
|
keepClearAreas.filter { Rect.intersects(it, unstashBounds) }.toSet()
|
||||||
|
val areasOverlappingUnstashPositionChanged =
|
||||||
|
!lastAreasOverlappingUnstashPosition.containsAll(areasOverlappingUnstashPosition)
|
||||||
|
lastAreasOverlappingUnstashPosition = areasOverlappingUnstashPosition
|
||||||
|
|
||||||
|
val now = clock()
|
||||||
|
if (areasOverlappingUnstashPositionChanged) {
|
||||||
|
lastStashTime = now
|
||||||
|
}
|
||||||
|
|
||||||
|
// If overlapping areas haven't changed and the stash duration has passed, we can
|
||||||
|
// place the PiP at the unstash position
|
||||||
|
val unstashTime = lastStashTime + stashDuration
|
||||||
|
if (now >= unstashTime) {
|
||||||
|
return Placement(unstashBounds, pipAnchorBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we'll stash it close to the unstash position
|
||||||
|
val stashedBounds = getNearbyStashedPosition(unstashBounds, keepClearAreas)
|
||||||
|
return Placement(
|
||||||
|
stashedBounds,
|
||||||
|
pipAnchorBounds,
|
||||||
|
getStashType(stashedBounds, transformedMovementBounds),
|
||||||
|
unstashBounds,
|
||||||
|
unstashTime
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PipBoundsState.StashType
|
||||||
|
private fun getStashType(stashedBounds: Rect, movementBounds: Rect): Int {
|
||||||
|
return when {
|
||||||
|
stashedBounds.left < movementBounds.left -> STASH_TYPE_LEFT
|
||||||
|
stashedBounds.right > movementBounds.right -> STASH_TYPE_RIGHT
|
||||||
|
stashedBounds.top < movementBounds.top -> STASH_TYPE_TOP
|
||||||
|
stashedBounds.bottom > movementBounds.bottom -> STASH_TYPE_BOTTOM
|
||||||
|
else -> STASH_TYPE_NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findRelaxedMovePosition(
|
||||||
|
pipAnchorBounds: Rect,
|
||||||
|
restrictedAreas: Set<Rect>,
|
||||||
|
unrestrictedAreas: Set<Rect>
|
||||||
|
): Rect? {
|
||||||
|
if (RELAX_DEPTH <= 0) {
|
||||||
|
// relaxed search disabled
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return findRelaxedMovePosition(
|
||||||
|
RELAX_DEPTH,
|
||||||
|
pipAnchorBounds,
|
||||||
|
restrictedAreas.toMutableSet(),
|
||||||
|
unrestrictedAreas
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findRelaxedMovePosition(
|
||||||
|
depth: Int,
|
||||||
|
pipAnchorBounds: Rect,
|
||||||
|
restrictedAreas: MutableSet<Rect>,
|
||||||
|
unrestrictedAreas: Set<Rect>
|
||||||
|
): Rect? {
|
||||||
|
if (depth == 0) {
|
||||||
|
return findFreeMovePosition(pipAnchorBounds, restrictedAreas, unrestrictedAreas)
|
||||||
|
}
|
||||||
|
|
||||||
|
val candidates = mutableListOf<Rect>()
|
||||||
|
val areasToExclude = restrictedAreas.toList()
|
||||||
|
for (area in areasToExclude) {
|
||||||
|
restrictedAreas.remove(area)
|
||||||
|
val candidate = findRelaxedMovePosition(
|
||||||
|
depth - 1,
|
||||||
|
pipAnchorBounds,
|
||||||
|
restrictedAreas,
|
||||||
|
unrestrictedAreas
|
||||||
|
)
|
||||||
|
restrictedAreas.add(area)
|
||||||
|
|
||||||
|
if (candidate != null) {
|
||||||
|
candidates.add(candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidates.minByOrNull { candidateCost(it, pipAnchorBounds) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cost function to evaluate candidate bounds */
|
||||||
|
private fun candidateCost(candidateBounds: Rect, pipAnchorBounds: Rect): Int {
|
||||||
|
// squared euclidean distance of corresponding rect corners
|
||||||
|
val dx = candidateBounds.left - pipAnchorBounds.left
|
||||||
|
val dy = candidateBounds.top - pipAnchorBounds.top
|
||||||
|
return dx * dx + dy * dy
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findFreeMovePosition(
|
||||||
|
pipAnchorBounds: Rect,
|
||||||
|
restrictedAreas: Set<Rect>,
|
||||||
|
unrestrictedAreas: Set<Rect>
|
||||||
|
): Rect? {
|
||||||
|
val movementBounds = transformedMovementBounds
|
||||||
|
val candidateEdgeRects = mutableListOf<Rect>()
|
||||||
|
val minRestrictedLeft =
|
||||||
|
pipAnchorBounds.right - screenSize.width * maxRestrictedDistanceFraction
|
||||||
|
|
||||||
|
candidateEdgeRects.add(
|
||||||
|
movementBounds.offsetCopy(movementBounds.width() + pipAreaPadding, 0)
|
||||||
|
)
|
||||||
|
candidateEdgeRects.addAll(unrestrictedAreas)
|
||||||
|
candidateEdgeRects.addAll(restrictedAreas.filter { it.left >= minRestrictedLeft })
|
||||||
|
|
||||||
|
// throw out edges that are too close to the left screen edge to fit the PiP
|
||||||
|
val minLeft = movementBounds.left + pipAnchorBounds.width()
|
||||||
|
candidateEdgeRects.retainAll { it.left - pipAreaPadding > minLeft }
|
||||||
|
candidateEdgeRects.sortBy { -it.left }
|
||||||
|
|
||||||
|
val maxRestrictedDY = (screenSize.height * maxRestrictedDistanceFraction).roundToInt()
|
||||||
|
|
||||||
|
val candidateBounds = mutableListOf<Rect>()
|
||||||
|
for (edgeRect in candidateEdgeRects) {
|
||||||
|
val edge = edgeRect.left - pipAreaPadding
|
||||||
|
val dx = (edge - pipAnchorBounds.width()) - pipAnchorBounds.left
|
||||||
|
val candidatePipBounds = pipAnchorBounds.offsetCopy(dx, 0)
|
||||||
|
val searchUp = true
|
||||||
|
val searchDown = !isPipAnchoredToCorner()
|
||||||
|
|
||||||
|
if (searchUp) {
|
||||||
|
val event = findMinMoveUp(candidatePipBounds, restrictedAreas, unrestrictedAreas)
|
||||||
|
val padding = if (event.start) 0 else pipAreaPadding
|
||||||
|
val dy = event.pos - pipAnchorBounds.bottom - padding
|
||||||
|
val maxDY = if (event.unrestricted) movementBounds.height() else maxRestrictedDY
|
||||||
|
val candidate = pipAnchorBounds.offsetCopy(dx, dy)
|
||||||
|
val isOnScreen = candidate.top > movementBounds.top
|
||||||
|
val hangingMidAir = !candidate.intersectsY(edgeRect)
|
||||||
|
if (isOnScreen && abs(dy) <= maxDY && !hangingMidAir) {
|
||||||
|
candidateBounds.add(candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchDown) {
|
||||||
|
val event = findMinMoveDown(candidatePipBounds, restrictedAreas, unrestrictedAreas)
|
||||||
|
val padding = if (event.start) 0 else pipAreaPadding
|
||||||
|
val dy = event.pos - pipAnchorBounds.top + padding
|
||||||
|
val maxDY = if (event.unrestricted) movementBounds.height() else maxRestrictedDY
|
||||||
|
val candidate = pipAnchorBounds.offsetCopy(dx, dy)
|
||||||
|
val isOnScreen = candidate.bottom < movementBounds.bottom
|
||||||
|
val hangingMidAir = !candidate.intersectsY(edgeRect)
|
||||||
|
if (isOnScreen && abs(dy) <= maxDY && !hangingMidAir) {
|
||||||
|
candidateBounds.add(candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidateBounds.sortBy { candidateCost(it, pipAnchorBounds) }
|
||||||
|
return candidateBounds.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNearbyStashedPosition(bounds: Rect, keepClearAreas: Set<Rect>): Rect {
|
||||||
|
val screenBounds = transformedScreenBounds
|
||||||
|
val stashCandidates = Array(2) { Rect(bounds) }
|
||||||
|
val areasOverlappingPipX = keepClearAreas.filter { it.intersectsX(bounds) }
|
||||||
|
val areasOverlappingPipY = keepClearAreas.filter { it.intersectsY(bounds) }
|
||||||
|
|
||||||
|
if (screenBounds.bottom - bounds.bottom <= bounds.top - screenBounds.top) {
|
||||||
|
// bottom is closer than top, stash downwards
|
||||||
|
val fullStashTop = screenBounds.bottom - stashOffset
|
||||||
|
|
||||||
|
val maxBottom = areasOverlappingPipX.maxByOrNull { it.bottom }!!.bottom
|
||||||
|
val partialStashTop = maxBottom + pipAreaPadding
|
||||||
|
|
||||||
|
val downPosition = stashCandidates[0]
|
||||||
|
downPosition.offsetTo(bounds.left, min(fullStashTop, partialStashTop))
|
||||||
|
} else {
|
||||||
|
// top is closer than bottom, stash upwards
|
||||||
|
val fullStashY = screenBounds.top - bounds.height() + stashOffset
|
||||||
|
|
||||||
|
val minTop = areasOverlappingPipX.minByOrNull { it.top }!!.top
|
||||||
|
val partialStashY = minTop - bounds.height() - pipAreaPadding
|
||||||
|
|
||||||
|
val upPosition = stashCandidates[0]
|
||||||
|
upPosition.offsetTo(bounds.left, max(fullStashY, partialStashY))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screenBounds.right - bounds.right <= bounds.left - screenBounds.left) {
|
||||||
|
// right is closer than left, stash rightwards
|
||||||
|
val fullStashLeft = screenBounds.right - stashOffset
|
||||||
|
|
||||||
|
val maxRight = areasOverlappingPipY.maxByOrNull { it.right }!!.right
|
||||||
|
val partialStashLeft = maxRight + pipAreaPadding
|
||||||
|
|
||||||
|
val rightPosition = stashCandidates[1]
|
||||||
|
rightPosition.offsetTo(min(fullStashLeft, partialStashLeft), bounds.top)
|
||||||
|
} else {
|
||||||
|
// left is closer than right, stash leftwards
|
||||||
|
val fullStashLeft = screenBounds.left - bounds.width() + stashOffset
|
||||||
|
|
||||||
|
val minLeft = areasOverlappingPipY.minByOrNull { it.left }!!.left
|
||||||
|
val partialStashLeft = minLeft - bounds.width() - pipAreaPadding
|
||||||
|
|
||||||
|
val rightPosition = stashCandidates[1]
|
||||||
|
rightPosition.offsetTo(max(fullStashLeft, partialStashLeft), bounds.top)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stashCandidates.minByOrNull {
|
||||||
|
val dx = abs(it.left - bounds.left)
|
||||||
|
val dy = abs(it.top - bounds.top)
|
||||||
|
dx * bounds.height() + dy * bounds.width()
|
||||||
|
}!!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents the PiP from being stashed for the current set of keep clear areas.
|
||||||
|
* The PiP may stash again if keep clear areas change.
|
||||||
|
*/
|
||||||
|
fun keepUnstashedForCurrentKeepClearAreas() {
|
||||||
|
lastStashTime = Long.MIN_VALUE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the size of the screen.
|
||||||
|
*
|
||||||
|
* @param size The new size of the screen
|
||||||
|
*/
|
||||||
|
fun setScreenSize(size: Size) {
|
||||||
|
if (screenSize == size) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
screenSize = size
|
||||||
|
transformedScreenBounds =
|
||||||
|
toTransformedSpace(Rect(0, 0, screenSize.width, screenSize.height))
|
||||||
|
transformedMovementBounds = toTransformedSpace(transformedMovementBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the bounds within which the PiP is allowed to move.
|
||||||
|
*
|
||||||
|
* @param bounds The new movement bounds
|
||||||
|
*/
|
||||||
|
fun setMovementBounds(bounds: Rect) {
|
||||||
|
if (movementBounds == bounds) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
movementBounds.set(bounds)
|
||||||
|
transformedMovementBounds = toTransformedSpace(movementBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the corner/side of the PiP's home position.
|
||||||
|
*/
|
||||||
|
fun setGravity(gravity: Int) {
|
||||||
|
if (pipGravity == gravity) return
|
||||||
|
|
||||||
|
pipGravity = gravity
|
||||||
|
transformedScreenBounds =
|
||||||
|
toTransformedSpace(Rect(0, 0, screenSize.width, screenSize.height))
|
||||||
|
transformedMovementBounds = toTransformedSpace(movementBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param open Whether this event marks the opening of an occupied segment
|
||||||
|
* @param pos The coordinate of this event
|
||||||
|
* @param unrestricted Whether this event was generated by an unrestricted keep clear area
|
||||||
|
* @param start Marks the special start event. Earlier events are skipped when sweeping
|
||||||
|
*/
|
||||||
|
data class SweepLineEvent(
|
||||||
|
val open: Boolean,
|
||||||
|
val pos: Int,
|
||||||
|
val unrestricted: Boolean,
|
||||||
|
val start: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [SweepLineEvent] representing the minimal move up from [pipBounds] that clears
|
||||||
|
* the given keep clear areas.
|
||||||
|
*/
|
||||||
|
private fun findMinMoveUp(
|
||||||
|
pipBounds: Rect,
|
||||||
|
restrictedAreas: Set<Rect>,
|
||||||
|
unrestrictedAreas: Set<Rect>
|
||||||
|
): SweepLineEvent {
|
||||||
|
val events = mutableListOf<SweepLineEvent>()
|
||||||
|
val generateEvents: (Boolean) -> (Rect) -> Unit = { unrestricted ->
|
||||||
|
{ area ->
|
||||||
|
if (pipBounds.intersectsX(area)) {
|
||||||
|
events.add(SweepLineEvent(true, area.bottom, unrestricted))
|
||||||
|
events.add(SweepLineEvent(false, area.top, unrestricted))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restrictedAreas.forEach(generateEvents(false))
|
||||||
|
unrestrictedAreas.forEach(generateEvents(true))
|
||||||
|
|
||||||
|
return sweepLineFindEarliestGap(
|
||||||
|
events,
|
||||||
|
pipBounds.height() + pipAreaPadding,
|
||||||
|
pipBounds.bottom,
|
||||||
|
pipBounds.height()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [SweepLineEvent] representing the minimal move down from [pipBounds] that clears
|
||||||
|
* the given keep clear areas.
|
||||||
|
*/
|
||||||
|
private fun findMinMoveDown(
|
||||||
|
pipBounds: Rect,
|
||||||
|
restrictedAreas: Set<Rect>,
|
||||||
|
unrestrictedAreas: Set<Rect>
|
||||||
|
): SweepLineEvent {
|
||||||
|
val events = mutableListOf<SweepLineEvent>()
|
||||||
|
val generateEvents: (Boolean) -> (Rect) -> Unit = { unrestricted ->
|
||||||
|
{ area ->
|
||||||
|
if (pipBounds.intersectsX(area)) {
|
||||||
|
events.add(SweepLineEvent(true, -area.top, unrestricted))
|
||||||
|
events.add(SweepLineEvent(false, -area.bottom, unrestricted))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restrictedAreas.forEach(generateEvents(false))
|
||||||
|
unrestrictedAreas.forEach(generateEvents(true))
|
||||||
|
|
||||||
|
val earliestEvent = sweepLineFindEarliestGap(
|
||||||
|
events,
|
||||||
|
pipBounds.height() + pipAreaPadding,
|
||||||
|
-pipBounds.top,
|
||||||
|
pipBounds.height()
|
||||||
|
)
|
||||||
|
|
||||||
|
return earliestEvent.copy(pos = -earliestEvent.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a list of events representing the starts & ends of occupied segments, and
|
||||||
|
* returns the earliest event whose position is unoccupied and has [gapSize] distance to the
|
||||||
|
* next event.
|
||||||
|
*
|
||||||
|
* @param events List of [SweepLineEvent] representing occupied segments
|
||||||
|
* @param gapSize Size of the gap to search for
|
||||||
|
* @param startPos The position to start the search on.
|
||||||
|
* Inserts a special event marked with [SweepLineEvent.start].
|
||||||
|
* @param startGapSize Used instead of [gapSize] for the start event
|
||||||
|
*/
|
||||||
|
private fun sweepLineFindEarliestGap(
|
||||||
|
events: MutableList<SweepLineEvent>,
|
||||||
|
gapSize: Int,
|
||||||
|
startPos: Int,
|
||||||
|
startGapSize: Int
|
||||||
|
): SweepLineEvent {
|
||||||
|
events.add(
|
||||||
|
SweepLineEvent(
|
||||||
|
open = false,
|
||||||
|
pos = startPos,
|
||||||
|
unrestricted = true,
|
||||||
|
start = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
events.sortBy { -it.pos }
|
||||||
|
|
||||||
|
// sweep
|
||||||
|
var openCount = 0
|
||||||
|
var i = 0
|
||||||
|
while (i < events.size) {
|
||||||
|
val event = events[i]
|
||||||
|
if (!event.start) {
|
||||||
|
if (event.open) {
|
||||||
|
openCount++
|
||||||
|
} else {
|
||||||
|
openCount--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openCount == 0) {
|
||||||
|
// check if placement is possible
|
||||||
|
val candidate = event.pos
|
||||||
|
if (candidate > startPos) {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventGapSize = if (event.start) startGapSize else gapSize
|
||||||
|
val nextEvent = events.getOrNull(i + 1)
|
||||||
|
if (nextEvent == null || nextEvent.pos < candidate - eventGapSize) {
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return events.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldTransformFlipX(): Boolean {
|
||||||
|
return when (pipGravity) {
|
||||||
|
(Gravity.TOP), (Gravity.TOP or Gravity.CENTER_HORIZONTAL) -> true
|
||||||
|
(Gravity.TOP or Gravity.LEFT) -> true
|
||||||
|
(Gravity.LEFT), (Gravity.LEFT or Gravity.CENTER_VERTICAL) -> true
|
||||||
|
(Gravity.BOTTOM or Gravity.LEFT) -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldTransformFlipY(): Boolean {
|
||||||
|
return when (pipGravity) {
|
||||||
|
(Gravity.TOP or Gravity.LEFT) -> true
|
||||||
|
(Gravity.TOP or Gravity.RIGHT) -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldTransformRotate(): Boolean {
|
||||||
|
val horizontalGravity = pipGravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
||||||
|
val leftOrRight = horizontalGravity == Gravity.LEFT || horizontalGravity == Gravity.RIGHT
|
||||||
|
|
||||||
|
if (leftOrRight) return false
|
||||||
|
return when (pipGravity and Gravity.VERTICAL_GRAVITY_MASK) {
|
||||||
|
(Gravity.TOP) -> true
|
||||||
|
(Gravity.BOTTOM) -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the given rect from screen space into the base case space, where the PiP
|
||||||
|
* anchor is positioned in the bottom right corner or on the right side (for expanded PiP).
|
||||||
|
*
|
||||||
|
* @see [fromTransformedSpace]
|
||||||
|
*/
|
||||||
|
private fun toTransformedSpace(r: Rect): Rect {
|
||||||
|
var screenWidth = screenSize.width
|
||||||
|
var screenHeight = screenSize.height
|
||||||
|
|
||||||
|
val tl = Point(r.left, r.top)
|
||||||
|
val tr = Point(r.right, r.top)
|
||||||
|
val br = Point(r.right, r.bottom)
|
||||||
|
val bl = Point(r.left, r.bottom)
|
||||||
|
val corners = arrayOf(tl, tr, br, bl)
|
||||||
|
|
||||||
|
// rotate first (CW)
|
||||||
|
if (shouldTransformRotate()) {
|
||||||
|
corners.forEach { p ->
|
||||||
|
val px = p.x
|
||||||
|
val py = p.y
|
||||||
|
p.x = py
|
||||||
|
p.y = -px
|
||||||
|
p.y += screenWidth // shift back screen into positive quadrant
|
||||||
|
}
|
||||||
|
screenWidth = screenSize.height
|
||||||
|
screenHeight = screenSize.width
|
||||||
|
}
|
||||||
|
|
||||||
|
// flip second
|
||||||
|
corners.forEach {
|
||||||
|
if (shouldTransformFlipX()) it.x = screenWidth - it.x
|
||||||
|
if (shouldTransformFlipY()) it.y = screenHeight - it.y
|
||||||
|
}
|
||||||
|
|
||||||
|
val top = corners.minByOrNull { it.y }!!.y
|
||||||
|
val right = corners.maxByOrNull { it.x }!!.x
|
||||||
|
val bottom = corners.maxByOrNull { it.y }!!.y
|
||||||
|
val left = corners.minByOrNull { it.x }!!.x
|
||||||
|
|
||||||
|
return Rect(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the given rect from the base case space, where the PiP anchor is positioned in
|
||||||
|
* the bottom right corner or on the right side, back into screen space.
|
||||||
|
*
|
||||||
|
* @see [toTransformedSpace]
|
||||||
|
*/
|
||||||
|
private fun fromTransformedSpace(r: Rect): Rect {
|
||||||
|
val rotate = shouldTransformRotate()
|
||||||
|
val transformedScreenWidth = if (rotate) screenSize.height else screenSize.width
|
||||||
|
val transformedScreenHeight = if (rotate) screenSize.width else screenSize.height
|
||||||
|
|
||||||
|
val tl = Point(r.left, r.top)
|
||||||
|
val tr = Point(r.right, r.top)
|
||||||
|
val br = Point(r.right, r.bottom)
|
||||||
|
val bl = Point(r.left, r.bottom)
|
||||||
|
val corners = arrayOf(tl, tr, br, bl)
|
||||||
|
|
||||||
|
// flip first
|
||||||
|
corners.forEach {
|
||||||
|
if (shouldTransformFlipX()) it.x = transformedScreenWidth - it.x
|
||||||
|
if (shouldTransformFlipY()) it.y = transformedScreenHeight - it.y
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotate second (CCW)
|
||||||
|
if (rotate) {
|
||||||
|
corners.forEach { p ->
|
||||||
|
p.y -= screenSize.width // undo shift back screen into positive quadrant
|
||||||
|
val px = p.x
|
||||||
|
val py = p.y
|
||||||
|
p.x = -py
|
||||||
|
p.y = px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val top = corners.minByOrNull { it.y }!!.y
|
||||||
|
val right = corners.maxByOrNull { it.x }!!.x
|
||||||
|
val bottom = corners.maxByOrNull { it.y }!!.y
|
||||||
|
val left = corners.minByOrNull { it.x }!!.x
|
||||||
|
|
||||||
|
return Rect(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PiP anchor bounds in base case for given gravity */
|
||||||
|
private fun getNormalPipAnchorBounds(pipSize: Size, movementBounds: Rect): Rect {
|
||||||
|
var size = pipSize
|
||||||
|
val rotateCW = shouldTransformRotate()
|
||||||
|
if (rotateCW) {
|
||||||
|
size = Size(pipSize.height, pipSize.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pipBounds = Rect()
|
||||||
|
if (isPipAnchoredToCorner()) {
|
||||||
|
// bottom right
|
||||||
|
Gravity.apply(
|
||||||
|
Gravity.BOTTOM or Gravity.RIGHT,
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
movementBounds,
|
||||||
|
pipBounds
|
||||||
|
)
|
||||||
|
return pipBounds
|
||||||
|
} else {
|
||||||
|
// expanded, right side
|
||||||
|
Gravity.apply(Gravity.RIGHT, size.width, size.height, movementBounds, pipBounds)
|
||||||
|
return pipBounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isPipAnchoredToCorner(): Boolean {
|
||||||
|
val left = (pipGravity and Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT
|
||||||
|
val right = (pipGravity and Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.RIGHT
|
||||||
|
val top = (pipGravity and Gravity.VERTICAL_GRAVITY_MASK) == Gravity.TOP
|
||||||
|
val bottom = (pipGravity and Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM
|
||||||
|
|
||||||
|
val horizontal = left || right
|
||||||
|
val vertical = top || bottom
|
||||||
|
|
||||||
|
return horizontal && vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Rect.offsetCopy(dx: Int, dy: Int) = Rect(this).apply { offset(dx, dy) }
|
||||||
|
private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom
|
||||||
|
private fun Rect.intersectsX(other: Rect) = right >= other.left && left <= other.right
|
||||||
|
}
|
||||||
@@ -174,8 +174,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateExpansionState() {
|
void updateExpansionState() {
|
||||||
mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipEnabled()
|
mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipSupported()
|
||||||
&& mTvPipBoundsState.getTvExpandedAspectRatio() != 0);
|
&& mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0);
|
||||||
mPipMenuView.setIsExpanded(mTvPipBoundsState.isTvPipExpanded());
|
mPipMenuView.setIsExpanded(mTvPipBoundsState.isTvPipExpanded());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,12 +202,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isInMoveMode() {
|
||||||
|
return mInMoveMode;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnterMoveMode() {
|
public void onEnterMoveMode() {
|
||||||
if (DEBUG) Log.d(TAG, "onEnterMoveMode - " + mInMoveMode);
|
if (DEBUG) Log.d(TAG, "onEnterMoveMode - " + mInMoveMode);
|
||||||
mInMoveMode = true;
|
mInMoveMode = true;
|
||||||
mPipMenuView.showMenuButtons(false);
|
mPipMenuView.showMenuButtons(false);
|
||||||
mPipMenuView.showMovementHints(mDelegate.getPipGravity());
|
mPipMenuView.showMovementHints(mDelegate.getPipGravity());
|
||||||
|
mDelegate.onInMoveModeChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -217,6 +222,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
|||||||
mInMoveMode = false;
|
mInMoveMode = false;
|
||||||
mPipMenuView.showMenuButtons(true);
|
mPipMenuView.showMenuButtons(true);
|
||||||
mPipMenuView.hideMovementHints();
|
mPipMenuView.hideMovementHints();
|
||||||
|
mDelegate.onInMoveModeChanged();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -447,6 +453,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
|||||||
|
|
||||||
void movePip(int keycode);
|
void movePip(int keycode);
|
||||||
|
|
||||||
|
void onInMoveModeChanged();
|
||||||
|
|
||||||
int getPipGravity();
|
int getPipGravity();
|
||||||
|
|
||||||
void togglePipExpansion();
|
void togglePipExpansion();
|
||||||
|
|||||||
@@ -0,0 +1,469 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.wm.shell.pip.tv
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.testing.AndroidTestingRunner
|
||||||
|
import android.util.Size
|
||||||
|
import android.view.Gravity
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE
|
||||||
|
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_BOTTOM
|
||||||
|
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT
|
||||||
|
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import junit.framework.Assert.assertEquals
|
||||||
|
import junit.framework.Assert.assertNull
|
||||||
|
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
class TvPipKeepClearAlgorithmTest {
|
||||||
|
private val DEFAULT_PIP_SIZE = Size(384, 216)
|
||||||
|
private val EXPANDED_WIDE_PIP_SIZE = Size(384*2, 216)
|
||||||
|
private val DASHBOARD_WIDTH = 484
|
||||||
|
private val BOTTOM_SHEET_HEIGHT = 524
|
||||||
|
private val STASH_OFFSET = 64
|
||||||
|
private val PADDING = 16
|
||||||
|
private val SCREEN_SIZE = Size(1920, 1080)
|
||||||
|
private val SCREEN_EDGE_INSET = 50
|
||||||
|
|
||||||
|
private lateinit var pipSize: Size
|
||||||
|
private lateinit var movementBounds: Rect
|
||||||
|
private lateinit var algorithm: TvPipKeepClearAlgorithm
|
||||||
|
private var currentTime = 0L
|
||||||
|
private var restrictedAreas = mutableSetOf<Rect>()
|
||||||
|
private var unrestrictedAreas = mutableSetOf<Rect>()
|
||||||
|
private var gravity: Int = 0
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
movementBounds = Rect(0, 0, SCREEN_SIZE.width, SCREEN_SIZE.height)
|
||||||
|
movementBounds.inset(SCREEN_EDGE_INSET, SCREEN_EDGE_INSET)
|
||||||
|
|
||||||
|
restrictedAreas.clear()
|
||||||
|
unrestrictedAreas.clear()
|
||||||
|
currentTime = 0L
|
||||||
|
pipSize = DEFAULT_PIP_SIZE
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
|
||||||
|
algorithm = TvPipKeepClearAlgorithm({ currentTime })
|
||||||
|
algorithm.setScreenSize(SCREEN_SIZE)
|
||||||
|
algorithm.setMovementBounds(movementBounds)
|
||||||
|
algorithm.pipAreaPadding = PADDING
|
||||||
|
algorithm.stashOffset = STASH_OFFSET
|
||||||
|
algorithm.stashDuration = 5000L
|
||||||
|
algorithm.setGravity(gravity)
|
||||||
|
algorithm.maxRestrictedDistanceFraction = 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_BottomRight() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_TopRight() {
|
||||||
|
gravity = Gravity.TOP or Gravity.RIGHT
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_TopLeft() {
|
||||||
|
gravity = Gravity.TOP or Gravity.LEFT
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_BottomLeft() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.LEFT
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_Right() {
|
||||||
|
gravity = Gravity.RIGHT
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_Left() {
|
||||||
|
gravity = Gravity.LEFT
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_Top() {
|
||||||
|
gravity = Gravity.TOP
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_Bottom() {
|
||||||
|
gravity = Gravity.BOTTOM
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_TopCenterHorizontal() {
|
||||||
|
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_BottomCenterHorizontal() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_RightCenterVertical() {
|
||||||
|
gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAnchorPosition_LeftCenterVertical() {
|
||||||
|
gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
|
||||||
|
testAnchorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testAnchorPosition() {
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
|
||||||
|
assertEquals(getExpectedAnchorBounds(), placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_AnchorBottomRight_KeepClearNotObstructing_StayAtAnchor() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
|
||||||
|
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.LEFT)
|
||||||
|
unrestrictedAreas.add(sidebar)
|
||||||
|
|
||||||
|
val expectedBounds = getExpectedAnchorBounds()
|
||||||
|
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_AnchorBottomRight_UnrestrictedRightSidebar_PushedLeft() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
|
||||||
|
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
|
||||||
|
unrestrictedAreas.add(sidebar)
|
||||||
|
|
||||||
|
val expectedBounds = anchorBoundsOffsetBy(SCREEN_EDGE_INSET - sidebar.width() - PADDING, 0)
|
||||||
|
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_AnchorTopRight_UnrestrictedRightSidebar_PushedLeft() {
|
||||||
|
gravity = Gravity.TOP or Gravity.RIGHT
|
||||||
|
|
||||||
|
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
|
||||||
|
unrestrictedAreas.add(sidebar)
|
||||||
|
|
||||||
|
val expectedBounds = anchorBoundsOffsetBy(SCREEN_EDGE_INSET - sidebar.width() - PADDING, 0)
|
||||||
|
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_AnchorBottomLeft_UnrestrictedRightSidebar_StayAtAnchor() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.LEFT
|
||||||
|
|
||||||
|
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
|
||||||
|
unrestrictedAreas.add(sidebar)
|
||||||
|
|
||||||
|
val expectedBounds = anchorBoundsOffsetBy(0, 0)
|
||||||
|
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_AnchorBottom_UnrestrictedRightSidebar_StayAtAnchor() {
|
||||||
|
gravity = Gravity.BOTTOM
|
||||||
|
|
||||||
|
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
|
||||||
|
unrestrictedAreas.add(sidebar)
|
||||||
|
|
||||||
|
val expectedBounds = anchorBoundsOffsetBy(0, 0)
|
||||||
|
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExpanded_AnchorBottom_UnrestrictedRightSidebar_StayAtAnchor() {
|
||||||
|
pipSize = EXPANDED_WIDE_PIP_SIZE
|
||||||
|
gravity = Gravity.BOTTOM
|
||||||
|
|
||||||
|
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
|
||||||
|
unrestrictedAreas.add(sidebar)
|
||||||
|
|
||||||
|
val expectedBounds = anchorBoundsOffsetBy(0, 0)
|
||||||
|
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_AnchorBottomRight_RestrictedSmallBottomBar_PushedUp() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
|
||||||
|
val bottomBar = makeBottomBar(96)
|
||||||
|
restrictedAreas.add(bottomBar)
|
||||||
|
|
||||||
|
val expectedBounds = anchorBoundsOffsetBy(0,
|
||||||
|
SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||||
|
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_AnchorBottomRight_RestrictedBottomSheet_StashDownAtAnchor() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
|
||||||
|
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||||
|
restrictedAreas.add(bottomBar)
|
||||||
|
|
||||||
|
val expectedBounds = getExpectedAnchorBounds()
|
||||||
|
expectedBounds.offsetTo(expectedBounds.left, SCREEN_SIZE.height - STASH_OFFSET)
|
||||||
|
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertEquals(STASH_TYPE_BOTTOM, placement.stashType)
|
||||||
|
assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds)
|
||||||
|
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_AnchorBottomRight_UnrestrictedBottomSheet_PushUp() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
|
||||||
|
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||||
|
unrestrictedAreas.add(bottomBar)
|
||||||
|
|
||||||
|
val expectedBounds = anchorBoundsOffsetBy(0,
|
||||||
|
SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||||
|
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_AnchorBottomRight_UnrestrictedBottomSheet_RestrictedSidebar_StashAboveBottomSheet() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
|
||||||
|
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||||
|
unrestrictedAreas.add(bottomBar)
|
||||||
|
|
||||||
|
val maxRestrictedHorizontalPush =
|
||||||
|
(algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt()
|
||||||
|
val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT)
|
||||||
|
restrictedAreas.add(sideBar)
|
||||||
|
|
||||||
|
val expectedUnstashBounds =
|
||||||
|
anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||||
|
|
||||||
|
val expectedBounds = Rect(expectedUnstashBounds)
|
||||||
|
expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top)
|
||||||
|
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||||
|
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||||
|
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_AnchorBottomRight_UnrestrictedBottomSheet_UnrestrictedSidebar_PushUpLeft() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
|
||||||
|
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||||
|
unrestrictedAreas.add(bottomBar)
|
||||||
|
|
||||||
|
val maxRestrictedHorizontalPush =
|
||||||
|
(algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt()
|
||||||
|
val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT)
|
||||||
|
unrestrictedAreas.add(sideBar)
|
||||||
|
|
||||||
|
val expectedBounds = anchorBoundsOffsetBy(
|
||||||
|
SCREEN_EDGE_INSET - sideBar.width() - PADDING,
|
||||||
|
SCREEN_EDGE_INSET - bottomBar.height() - PADDING
|
||||||
|
)
|
||||||
|
|
||||||
|
val placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_Stashed_UnstashBoundsBecomeUnobstructed_Unstashes() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
|
||||||
|
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||||
|
unrestrictedAreas.add(bottomBar)
|
||||||
|
|
||||||
|
val maxRestrictedHorizontalPush =
|
||||||
|
(algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt()
|
||||||
|
val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT)
|
||||||
|
restrictedAreas.add(sideBar)
|
||||||
|
|
||||||
|
val expectedUnstashBounds =
|
||||||
|
anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||||
|
|
||||||
|
val expectedBounds = Rect(expectedUnstashBounds)
|
||||||
|
expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top)
|
||||||
|
|
||||||
|
var placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||||
|
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||||
|
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||||
|
|
||||||
|
currentTime += 1000
|
||||||
|
|
||||||
|
restrictedAreas.remove(sideBar)
|
||||||
|
placement = getActualPlacement()
|
||||||
|
assertEquals(expectedUnstashBounds, placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_Stashed_UnstashBoundsStaysObstructed_UnstashesAfterTimeout() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
|
||||||
|
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||||
|
unrestrictedAreas.add(bottomBar)
|
||||||
|
|
||||||
|
val maxRestrictedHorizontalPush =
|
||||||
|
(algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt()
|
||||||
|
val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT)
|
||||||
|
restrictedAreas.add(sideBar)
|
||||||
|
|
||||||
|
val expectedUnstashBounds =
|
||||||
|
anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||||
|
|
||||||
|
val expectedBounds = Rect(expectedUnstashBounds)
|
||||||
|
expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top)
|
||||||
|
|
||||||
|
var placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||||
|
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||||
|
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||||
|
|
||||||
|
currentTime += algorithm.stashDuration
|
||||||
|
|
||||||
|
placement = getActualPlacement()
|
||||||
|
assertEquals(expectedUnstashBounds, placement.bounds)
|
||||||
|
assertNotStashed(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_Stashed_UnstashBoundsObstructionChanges_UnstashTimeExtended() {
|
||||||
|
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||||
|
|
||||||
|
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||||
|
unrestrictedAreas.add(bottomBar)
|
||||||
|
|
||||||
|
val maxRestrictedHorizontalPush =
|
||||||
|
(algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt()
|
||||||
|
val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT)
|
||||||
|
restrictedAreas.add(sideBar)
|
||||||
|
|
||||||
|
val expectedUnstashBounds =
|
||||||
|
anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING)
|
||||||
|
|
||||||
|
val expectedBounds = Rect(expectedUnstashBounds)
|
||||||
|
expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top)
|
||||||
|
|
||||||
|
var placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||||
|
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||||
|
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||||
|
|
||||||
|
currentTime += 1000
|
||||||
|
|
||||||
|
val newObstruction = Rect(
|
||||||
|
0,
|
||||||
|
expectedUnstashBounds.top,
|
||||||
|
expectedUnstashBounds.right,
|
||||||
|
expectedUnstashBounds.bottom
|
||||||
|
)
|
||||||
|
restrictedAreas.add(newObstruction)
|
||||||
|
|
||||||
|
placement = getActualPlacement()
|
||||||
|
assertEquals(expectedBounds, placement.bounds)
|
||||||
|
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||||
|
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||||
|
assertEquals(currentTime + algorithm.stashDuration, placement.unstashTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeSideBar(width: Int, @Gravity.GravityFlags side: Int): Rect {
|
||||||
|
val sidebar = Rect(0, 0, width, SCREEN_SIZE.height)
|
||||||
|
if (side == Gravity.RIGHT) {
|
||||||
|
sidebar.offsetTo(SCREEN_SIZE.width - width, 0)
|
||||||
|
}
|
||||||
|
return sidebar
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeBottomBar(height: Int): Rect {
|
||||||
|
return Rect(0, SCREEN_SIZE.height - height, SCREEN_SIZE.width, SCREEN_SIZE.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExpectedAnchorBounds(): Rect {
|
||||||
|
val expectedBounds = Rect()
|
||||||
|
Gravity.apply(gravity, pipSize.width, pipSize.height, movementBounds, expectedBounds)
|
||||||
|
return expectedBounds
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun anchorBoundsOffsetBy(dx: Int, dy: Int): Rect {
|
||||||
|
val bounds = getExpectedAnchorBounds()
|
||||||
|
bounds.offset(dx, dy)
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getActualPlacement(): Placement {
|
||||||
|
algorithm.setGravity(gravity)
|
||||||
|
return algorithm.calculatePipPosition(pipSize, restrictedAreas, unrestrictedAreas)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertNotStashed(actual: Placement) {
|
||||||
|
assertEquals(STASH_TYPE_NONE, actual.stashType)
|
||||||
|
assertNull(actual.unstashDestinationBounds)
|
||||||
|
assertEquals(0L, actual.unstashTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user