Merge changes I49d32c18,I8e8736fc into tm-dev

* changes:
  Apply temporary PiP decor separately from overall placement
  Debounce TV PiP movements
This commit is contained in:
Robert Horvath
2022-05-11 07:59:53 +00:00
committed by Android (Google) Code Review
9 changed files with 620 additions and 184 deletions

View File

@@ -18,6 +18,7 @@ package com.android.wm.shell.dagger;
import android.content.Context; import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock;
import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.WindowManagerShellWrapper;
@@ -39,6 +40,7 @@ import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.pip.PipTransitionState;
import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.PipUiEventLogger;
import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm; import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm;
import com.android.wm.shell.pip.tv.TvPipBoundsController;
import com.android.wm.shell.pip.tv.TvPipBoundsState; import com.android.wm.shell.pip.tv.TvPipBoundsState;
import com.android.wm.shell.pip.tv.TvPipController; import com.android.wm.shell.pip.tv.TvPipController;
import com.android.wm.shell.pip.tv.TvPipMenuController; import com.android.wm.shell.pip.tv.TvPipMenuController;
@@ -64,6 +66,7 @@ public abstract class TvPipModule {
Context context, Context context,
TvPipBoundsState tvPipBoundsState, TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
PipAppOpsListener pipAppOpsListener, PipAppOpsListener pipAppOpsListener,
PipTaskOrganizer pipTaskOrganizer, PipTaskOrganizer pipTaskOrganizer,
TvPipMenuController tvPipMenuController, TvPipMenuController tvPipMenuController,
@@ -74,13 +77,13 @@ public abstract class TvPipModule {
PipParamsChangedForwarder pipParamsChangedForwarder, PipParamsChangedForwarder pipParamsChangedForwarder,
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,
tvPipBoundsState, tvPipBoundsState,
tvPipBoundsAlgorithm, tvPipBoundsAlgorithm,
tvPipBoundsController,
pipAppOpsListener, pipAppOpsListener,
pipTaskOrganizer, pipTaskOrganizer,
pipTransitionController, pipTransitionController,
@@ -91,8 +94,22 @@ public abstract class TvPipModule {
pipParamsChangedForwarder, pipParamsChangedForwarder,
displayController, displayController,
windowManagerShellWrapper, windowManagerShellWrapper,
mainExecutor, mainExecutor));
mainHandler)); }
@WMSingleton
@Provides
static TvPipBoundsController provideTvPipBoundsController(
Context context,
@ShellMainThread Handler mainHandler,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm) {
return new TvPipBoundsController(
context,
SystemClock::uptimeMillis,
mainHandler,
tvPipBoundsState,
tvPipBoundsAlgorithm);
} }
@WMSingleton @WMSingleton

View File

@@ -29,7 +29,6 @@ import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Insets; import android.graphics.Insets;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.SystemClock;
import android.util.ArraySet; import android.util.ArraySet;
import android.util.Size; import android.util.Size;
import android.view.Gravity; import android.view.Gravity;
@@ -66,7 +65,7 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
@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); this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm();
reloadResources(context); reloadResources(context);
} }
@@ -80,7 +79,6 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
res.getDimensionPixelSize(R.dimen.pip_keep_clear_area_padding)); res.getDimensionPixelSize(R.dimen.pip_keep_clear_area_padding));
mKeepClearAlgorithm.setMaxRestrictedDistanceFraction( mKeepClearAlgorithm.setMaxRestrictedDistanceFraction(
res.getFraction(R.fraction.config_pipMaxRestrictedMoveDistance, 1, 1)); res.getFraction(R.fraction.config_pipMaxRestrictedMoveDistance, 1, 1));
mKeepClearAlgorithm.setStashDuration(res.getInteger(R.integer.config_pipStashDuration));
} }
@Override @Override
@@ -104,7 +102,7 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true); updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true);
} }
mTvPipBoundsState.setTvPipExpanded(isPipExpanded); mTvPipBoundsState.setTvPipExpanded(isPipExpanded);
return getTvPipBounds().getBounds(); return adjustBoundsForTemporaryDecor(getTvPipPlacement().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. */
@@ -114,13 +112,27 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: getAdjustedDestinationBounds: %f", TAG, newAspectRatio); "%s: getAdjustedDestinationBounds: %f", TAG, newAspectRatio);
} }
return getTvPipBounds().getBounds(); return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds());
}
Rect adjustBoundsForTemporaryDecor(Rect bounds) {
Rect boundsWithDecor = new Rect(bounds);
Insets decorInset = mTvPipBoundsState.getPipMenuTemporaryDecorInsets();
Insets pipDecorReverseInsets = Insets.subtract(Insets.NONE, decorInset);
boundsWithDecor.inset(decorInset);
Gravity.apply(mTvPipBoundsState.getTvPipGravity(),
boundsWithDecor.width(), boundsWithDecor.height(), bounds, boundsWithDecor);
// remove temporary decoration again
boundsWithDecor.inset(pipDecorReverseInsets);
return boundsWithDecor;
} }
/** /**
* Calculates the PiP bounds. * Calculates the PiP bounds.
*/ */
public Placement getTvPipBounds() { @NonNull
public Placement getTvPipPlacement() {
final Size pipSize = getPipSize(); final Size pipSize = getPipSize();
final Rect displayBounds = mTvPipBoundsState.getDisplayBounds(); final Rect displayBounds = mTvPipBoundsState.getDisplayBounds();
final Size screenSize = new Size(displayBounds.width(), displayBounds.height()); final Size screenSize = new Size(displayBounds.width(), displayBounds.height());
@@ -153,8 +165,6 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset()); mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset());
mKeepClearAlgorithm.setPipPermanentDecorInsets( mKeepClearAlgorithm.setPipPermanentDecorInsets(
mTvPipBoundsState.getPipMenuPermanentDecorInsets()); mTvPipBoundsState.getPipMenuPermanentDecorInsets());
mKeepClearAlgorithm.setPipTemporaryDecorInsets(
mTvPipBoundsState.getPipMenuTemporaryDecorInsets());
final Placement placement = mKeepClearAlgorithm.calculatePipPosition( final Placement placement = mKeepClearAlgorithm.calculatePipPosition(
pipSize, pipSize,
@@ -407,8 +417,4 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
TAG, expandedSize.getWidth(), expandedSize.getHeight()); TAG, expandedSize.getWidth(), expandedSize.getHeight());
} }
} }
void keepUnstashedForCurrentKeepClearAreas() {
mKeepClearAlgorithm.keepUnstashedForCurrentKeepClearAreas();
}
} }

View File

@@ -0,0 +1,253 @@
/*
* 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 static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Handler;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Controller managing the PiP's position.
* Manages debouncing of PiP movements and scheduling of unstashing.
*/
public class TvPipBoundsController {
private static final boolean DEBUG = false;
private static final String TAG = "TvPipBoundsController";
/**
* Time the calculated PiP position needs to be stable before PiP is moved there,
* to avoid erratic movement.
* Some changes will cause the PiP to be repositioned immediately, such as changes to
* unrestricted keep clear areas.
*/
@VisibleForTesting
static final long POSITION_DEBOUNCE_TIMEOUT_MILLIS = 300L;
private final Context mContext;
private final Supplier<Long> mClock;
private final Handler mMainHandler;
private final TvPipBoundsState mTvPipBoundsState;
private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
@Nullable
private PipBoundsListener mListener;
private int mResizeAnimationDuration;
private int mStashDurationMs;
private Rect mCurrentPlacementBounds;
private Rect mPipTargetBounds;
private final Runnable mApplyPendingPlacementRunnable = this::applyPendingPlacement;
private boolean mPendingStash;
private Placement mPendingPlacement;
private int mPendingPlacementAnimationDuration;
private Runnable mUnstashRunnable;
public TvPipBoundsController(
Context context,
Supplier<Long> clock,
Handler mainHandler,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm) {
mContext = context;
mClock = clock;
mMainHandler = mainHandler;
mTvPipBoundsState = tvPipBoundsState;
mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
loadConfigurations();
}
private void loadConfigurations() {
final Resources res = mContext.getResources();
mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
mStashDurationMs = res.getInteger(R.integer.config_pipStashDuration);
}
void setListener(PipBoundsListener listener) {
mListener = listener;
}
/**
* Update the PiP bounds based on the state of the PiP, decors, and keep clear areas.
* Unless {@code immediate} is {@code true}, the PiP does not move immediately to avoid
* keep clear areas, but waits for a new position to stay uncontested for
* {@link #POSITION_DEBOUNCE_TIMEOUT_MILLIS} before moving to it.
* Temporary decor changes are applied immediately.
*
* @param stayAtAnchorPosition If true, PiP will be placed at the anchor position
* @param disallowStashing If true, PiP will not be placed off-screen in a stashed position
* @param animationDuration Duration of the animation to the new position
* @param immediate If true, PiP will move immediately to avoid keep clear areas
*/
@VisibleForTesting
void recalculatePipBounds(boolean stayAtAnchorPosition, boolean disallowStashing,
int animationDuration, boolean immediate) {
final Placement placement = mTvPipBoundsAlgorithm.getTvPipPlacement();
final int stashType = disallowStashing ? STASH_TYPE_NONE : placement.getStashType();
mTvPipBoundsState.setStashed(stashType);
if (stayAtAnchorPosition) {
cancelScheduledPlacement();
applyPlacementBounds(placement.getAnchorBounds(), animationDuration);
} else if (disallowStashing) {
cancelScheduledPlacement();
applyPlacementBounds(placement.getUnstashedBounds(), animationDuration);
} else if (immediate) {
cancelScheduledPlacement();
applyPlacementBounds(placement.getBounds(), animationDuration);
scheduleUnstashIfNeeded(placement);
} else {
applyPlacementBounds(mCurrentPlacementBounds, animationDuration);
schedulePinnedStackPlacement(placement, animationDuration);
}
}
private void schedulePinnedStackPlacement(@NonNull final Placement placement,
int animationDuration) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: schedulePinnedStackPlacement() - pip bounds: %s",
TAG, placement.getBounds().toShortString());
}
if (mPendingPlacement != null && Objects.equals(mPendingPlacement.getBounds(),
placement.getBounds())) {
mPendingStash = mPendingStash || placement.getTriggerStash();
return;
}
mPendingStash = placement.getStashType() != STASH_TYPE_NONE
&& (mPendingStash || placement.getTriggerStash());
mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable);
mPendingPlacement = placement;
mPendingPlacementAnimationDuration = animationDuration;
mMainHandler.postAtTime(mApplyPendingPlacementRunnable,
mClock.get() + POSITION_DEBOUNCE_TIMEOUT_MILLIS);
}
private void scheduleUnstashIfNeeded(final Placement placement) {
if (mUnstashRunnable != null) {
mMainHandler.removeCallbacks(mUnstashRunnable);
mUnstashRunnable = null;
}
if (placement.getUnstashDestinationBounds() != null) {
mUnstashRunnable = () -> {
applyPlacementBounds(placement.getUnstashDestinationBounds(),
mResizeAnimationDuration);
mUnstashRunnable = null;
};
mMainHandler.postAtTime(mUnstashRunnable, mClock.get() + mStashDurationMs);
}
}
private void applyPendingPlacement() {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: applyPendingPlacement()", TAG);
}
if (mPendingPlacement != null) {
if (mPendingStash) {
mPendingStash = false;
scheduleUnstashIfNeeded(mPendingPlacement);
}
if (mUnstashRunnable != null) {
// currently stashed, use stashed pos
applyPlacementBounds(mPendingPlacement.getBounds(),
mPendingPlacementAnimationDuration);
} else {
applyPlacementBounds(mPendingPlacement.getUnstashedBounds(),
mPendingPlacementAnimationDuration);
}
}
mPendingPlacement = null;
}
void onPipDismissed() {
mCurrentPlacementBounds = null;
mPipTargetBounds = null;
cancelScheduledPlacement();
}
private void cancelScheduledPlacement() {
mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable);
mPendingPlacement = null;
if (mUnstashRunnable != null) {
mMainHandler.removeCallbacks(mUnstashRunnable);
mUnstashRunnable = null;
}
}
private void applyPlacementBounds(Rect bounds, int animationDuration) {
if (bounds == null) {
return;
}
mCurrentPlacementBounds = bounds;
Rect adjustedBounds = mTvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(bounds);
movePipTo(adjustedBounds, animationDuration);
}
/** Animates the PiP to the given bounds with the given animation duration. */
private void movePipTo(Rect bounds, int animationDuration) {
if (Objects.equals(mPipTargetBounds, bounds)) {
return;
}
mPipTargetBounds = bounds;
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString());
}
if (mListener != null) {
mListener.onPipTargetBoundsChange(bounds, animationDuration);
}
}
/**
* Interface being notified of changes to the PiP bounds as calculated by
* @link TvPipBoundsController}.
*/
public interface PipBoundsListener {
/**
* Called when the calculated PiP bounds are changing.
*
* @param newTargetBounds The new bounds of the PiP.
* @param animationDuration The animation duration for the PiP movement.
*/
void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration);
}
}

View File

@@ -25,12 +25,10 @@ import android.app.ActivityTaskManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.RemoteAction; import android.app.RemoteAction;
import android.app.TaskInfo; import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
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.view.Gravity; import android.view.Gravity;
@@ -46,25 +44,24 @@ 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.PipAppOpsListener; import com.android.wm.shell.pip.PipAppOpsListener;
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.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipParamsChangedForwarder;
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 com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
/** /**
* Manages the picture-in-picture (PIP) UI and states. * Manages the picture-in-picture (PIP) UI and states.
*/ */
public class TvPipController implements PipTransitionController.PipTransitionCallback, public class TvPipController implements PipTransitionController.PipTransitionCallback,
TvPipMenuController.Delegate, TvPipNotificationController.Delegate, TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate,
DisplayController.OnDisplaysChangedListener { TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener {
private static final String TAG = "TvPipController"; private static final String TAG = "TvPipController";
static final boolean DEBUG = false; static final boolean DEBUG = false;
@@ -98,19 +95,18 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private final TvPipBoundsState mTvPipBoundsState; private final TvPipBoundsState mTvPipBoundsState;
private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
private final TvPipBoundsController mTvPipBoundsController;
private final PipAppOpsListener mAppOpsListener; private final PipAppOpsListener mAppOpsListener;
private final PipTaskOrganizer mPipTaskOrganizer; private final PipTaskOrganizer mPipTaskOrganizer;
private final PipMediaController mPipMediaController; private final PipMediaController mPipMediaController;
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 RemoteAction mCloseAction; private RemoteAction mCloseAction;
// How long the shell will wait for the app to close the PiP if a custom action is set. // How long the shell will wait for the app to close the PiP if a custom action is set.
@@ -123,6 +119,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
Context context, Context context,
TvPipBoundsState tvPipBoundsState, TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
PipAppOpsListener pipAppOpsListener, PipAppOpsListener pipAppOpsListener,
PipTaskOrganizer pipTaskOrganizer, PipTaskOrganizer pipTaskOrganizer,
PipTransitionController pipTransitionController, PipTransitionController pipTransitionController,
@@ -133,12 +130,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
PipParamsChangedForwarder pipParamsChangedForwarder, PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController, DisplayController displayController,
WindowManagerShellWrapper wmShell, WindowManagerShellWrapper wmShell,
ShellExecutor mainExecutor, ShellExecutor mainExecutor) {
Handler mainHandler) {
return new TvPipController( return new TvPipController(
context, context,
tvPipBoundsState, tvPipBoundsState,
tvPipBoundsAlgorithm, tvPipBoundsAlgorithm,
tvPipBoundsController,
pipAppOpsListener, pipAppOpsListener,
pipTaskOrganizer, pipTaskOrganizer,
pipTransitionController, pipTransitionController,
@@ -149,14 +146,14 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
pipParamsChangedForwarder, pipParamsChangedForwarder,
displayController, displayController,
wmShell, wmShell,
mainExecutor, mainExecutor).mImpl;
mainHandler).mImpl;
} }
private TvPipController( private TvPipController(
Context context, Context context,
TvPipBoundsState tvPipBoundsState, TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
PipAppOpsListener pipAppOpsListener, PipAppOpsListener pipAppOpsListener,
PipTaskOrganizer pipTaskOrganizer, PipTaskOrganizer pipTaskOrganizer,
PipTransitionController pipTransitionController, PipTransitionController pipTransitionController,
@@ -167,16 +164,16 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
PipParamsChangedForwarder pipParamsChangedForwarder, PipParamsChangedForwarder pipParamsChangedForwarder,
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());
mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay())); mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay()));
mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm; mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
mTvPipBoundsController = tvPipBoundsController;
mTvPipBoundsController.setListener(this);
mPipMediaController = pipMediaController; mPipMediaController = pipMediaController;
@@ -227,7 +224,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
/** /**
* Starts the process if bringing up the Pip menu if by issuing a command to move Pip * Starts the process if bringing up the Pip menu if by issuing a command to move Pip
* task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip * task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip
* task/window is properly positioned in {@link #onPipTransitionFinished(ComponentName, int)}. * task/window is properly positioned in {@link #onPipTransitionFinished(int)}.
*/ */
@Override @Override
public void showPictureInPictureMenu() { public void showPictureInPictureMenu() {
@@ -256,7 +253,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
"%s: closeMenu(), state before=%s", TAG, stateToName(mState)); "%s: closeMenu(), state before=%s", TAG, stateToName(mState));
} }
setState(STATE_PIP); setState(STATE_PIP);
mTvPipBoundsAlgorithm.keepUnstashedForCurrentKeepClearAreas();
updatePinnedStackBounds(); updatePinnedStackBounds();
} }
@@ -331,68 +327,35 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
Set<Rect> unrestricted) { Set<Rect> unrestricted) {
if (mTvPipBoundsState.getDisplayId() == displayId) { if (mTvPipBoundsState.getDisplayId() == displayId) {
boolean unrestrictedAreasChanged = !Objects.equals(unrestricted,
mTvPipBoundsState.getUnrestrictedKeepClearAreas());
mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted); mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted);
updatePinnedStackBounds(); updatePinnedStackBounds(mResizeAnimationDuration, unrestrictedAreasChanged);
} }
} }
private void updatePinnedStackBounds() { private void updatePinnedStackBounds() {
updatePinnedStackBounds(mResizeAnimationDuration); updatePinnedStackBounds(mResizeAnimationDuration, true);
} }
/** /**
* Update the PiP bounds based on the state of the PiP and keep clear areas. * 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 updatePinnedStackBounds(int animationDuration) { private void updatePinnedStackBounds(int animationDuration, boolean immediate) {
if (mState == STATE_NO_PIP) { if (mState == STATE_NO_PIP) {
return; return;
} }
final boolean stayAtAnchorPosition = mTvPipMenuController.isInMoveMode(); final boolean stayAtAnchorPosition = mTvPipMenuController.isInMoveMode();
final boolean disallowStashing = mState == STATE_PIP_MENU || stayAtAnchorPosition; final boolean disallowStashing = mState == STATE_PIP_MENU || stayAtAnchorPosition;
final Placement placement = mTvPipBoundsAlgorithm.getTvPipBounds(); mTvPipBoundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing,
animationDuration, immediate);
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) { @Override
mMainHandler.removeCallbacks(mUnstashRunnable); public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) {
mUnstashRunnable = null; mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds,
} animationDuration, rect -> mTvPipMenuController.updateExpansionState());
if (!disallowStashing && placement.getUnstashDestinationBounds() != null) { mTvPipMenuController.onPipTransitionStarted(newTargetBounds);
mUnstashRunnable = () -> {
movePinnedStackTo(placement.getUnstashDestinationBounds(), animationDuration);
};
mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime());
}
}
/** Animates the PiP to the given bounds. */
private void movePinnedStackTo(Rect bounds) {
movePinnedStackTo(bounds, mResizeAnimationDuration);
}
/** Animates the PiP to the given bounds with the given animation duration. */
private void movePinnedStackTo(Rect bounds, int animationDuration) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: movePinnedStack() - new pip bounds: %s", TAG, bounds.toShortString());
}
mPipTaskOrganizer.scheduleAnimateResizePip(bounds,
animationDuration, rect -> {
mTvPipMenuController.updateExpansionState();
});
mTvPipMenuController.onPipTransitionStarted(bounds);
} }
/** /**
@@ -431,7 +394,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
@Override @Override
public void closeEduText() { public void closeEduText() {
updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs); updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false);
} }
private void registerSessionListenerForCurrentUser() { private void registerSessionListenerForCurrentUser() {
@@ -473,6 +436,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
mPipNotificationController.dismiss(); mPipNotificationController.dismiss();
mTvPipMenuController.closeMenu(); mTvPipMenuController.closeMenu();
mTvPipBoundsState.resetTvPipState(); mTvPipBoundsState.resetTvPipState();
mTvPipBoundsController.onPipDismissed();
setState(STATE_NO_PIP); setState(STATE_NO_PIP);
mPinnedTaskId = NONEXISTENT_TASK_ID; mPinnedTaskId = NONEXISTENT_TASK_ID;
} }

View File

@@ -33,17 +33,14 @@ import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
private const val DEFAULT_PIP_MARGINS = 48 private const val DEFAULT_PIP_MARGINS = 48
private const val DEFAULT_STASH_DURATION = 5000L
private const val RELAX_DEPTH = 1 private const val RELAX_DEPTH = 1
private const val DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION = 0.15 private const val DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION = 0.15
/** /**
* This class calculates an appropriate position for a Picture-In-Picture (PiP) window, taking * This class calculates an appropriate position for a Picture-In-Picture (PiP) window, taking
* into account app defined keep clear areas. * into account app defined keep clear areas.
*
* @param clock A function returning a current timestamp (in milliseconds)
*/ */
class TvPipKeepClearAlgorithm(private val clock: () -> Long) { class TvPipKeepClearAlgorithm() {
/** /**
* Result of the positioning algorithm. * Result of the positioning algorithm.
* *
@@ -51,17 +48,17 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
* @param anchorBounds The bounds of the PiP anchor position * @param anchorBounds The bounds of the PiP anchor position
* (where the PiP would be placed if there were no keep clear areas) * (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 stashType Where the PiP has been stashed, if at all
* @param unstashDestinationBounds If stashed, the PiP should move to this position after * @param unstashDestinationBounds If stashed, the PiP should move to this position when
* [stashDuration] has passed. * unstashing.
* @param unstashTime If stashed, the time at which the PiP should move * @param triggerStash Whether this placement should trigger the PiP to stash, or extend
* to [unstashDestinationBounds] * the unstash timeout if already stashed.
*/ */
data class Placement( data class Placement(
val bounds: Rect, val bounds: Rect,
val anchorBounds: Rect, val anchorBounds: Rect,
@PipBoundsState.StashType val stashType: Int = STASH_TYPE_NONE, @PipBoundsState.StashType val stashType: Int = STASH_TYPE_NONE,
val unstashDestinationBounds: Rect? = null, val unstashDestinationBounds: Rect? = null,
val unstashTime: Long = 0L val triggerStash: Boolean = false
) { ) {
/** Bounds to use if the PiP should not be stashed. */ /** Bounds to use if the PiP should not be stashed. */
fun getUnstashedBounds() = unstashDestinationBounds ?: bounds fun getUnstashedBounds() = unstashDestinationBounds ?: bounds
@@ -79,12 +76,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
/** The distance the PiP peeks into the screen when stashed */ /** The distance the PiP peeks into the screen when stashed */
var stashOffset = DEFAULT_PIP_MARGINS 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 */ /** The fraction of screen width/height restricted keep clear areas can move the PiP */
var maxRestrictedDistanceFraction = DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION var maxRestrictedDistanceFraction = DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION
@@ -93,14 +84,10 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
private var transformedMovementBounds = Rect() private var transformedMovementBounds = Rect()
private var lastAreasOverlappingUnstashPosition: Set<Rect> = emptySet() private var lastAreasOverlappingUnstashPosition: Set<Rect> = emptySet()
private var lastStashTime: Long = Long.MIN_VALUE
/** Spaces around the PiP that we should leave space for when placing the PiP. Permanent PiP /** Spaces around the PiP that we should leave space for when placing the PiP. Permanent PiP
* decorations are relevant for calculating intersecting keep clear areas */ * decorations are relevant for calculating intersecting keep clear areas */
private var pipPermanentDecorInsets = Insets.NONE private var pipPermanentDecorInsets = Insets.NONE
/** Spaces around the PiP that we should leave space for when placing the PiP. Temporary PiP
* decorations are not relevant for calculating intersecting keep clear areas */
private var pipTemporaryDecorInsets = Insets.NONE
/** /**
* Calculates the position the PiP should be placed at, taking into consideration the * Calculates the position the PiP should be placed at, taking into consideration the
@@ -113,8 +100,8 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
* always try to respect these areas. * 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 * 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 * as [Placement.bounds], along with a position to move to when the PiP unstashes
* passed as [Placement.unstashDestinationBounds]. * as [Placement.unstashDestinationBounds].
* *
* @param pipSize The size of the PiP window * @param pipSize The size of the PiP window
* @param restrictedAreas The restricted keep clear areas * @param restrictedAreas The restricted keep clear areas
@@ -130,13 +117,11 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas) val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas)
val pipSizeWithAllDecors = addDecors(pipSize) val pipSizeWithAllDecors = addDecors(pipSize)
val pipAnchorBoundsWithAllDecors = val pipAnchorBoundsWithDecors =
getNormalPipAnchorBounds(pipSizeWithAllDecors, transformedMovementBounds) getNormalPipAnchorBounds(pipSizeWithAllDecors, transformedMovementBounds)
val pipAnchorBoundsWithPermanentDecors =
removeTemporaryDecorsTransformed(pipAnchorBoundsWithAllDecors)
val result = calculatePipPositionTransformed( val result = calculatePipPositionTransformed(
pipAnchorBoundsWithPermanentDecors, pipAnchorBoundsWithDecors,
transformedRestrictedAreas, transformedRestrictedAreas,
transformedUnrestrictedAreas transformedUnrestrictedAreas
) )
@@ -152,7 +137,7 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
anchorBounds, anchorBounds,
getStashType(pipBounds, unstashedDestBounds), getStashType(pipBounds, unstashedDestBounds),
unstashedDestBounds, unstashedDestBounds,
result.unstashTime result.triggerStash
) )
} }
@@ -213,26 +198,13 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
!lastAreasOverlappingUnstashPosition.containsAll(areasOverlappingUnstashPosition) !lastAreasOverlappingUnstashPosition.containsAll(areasOverlappingUnstashPosition)
lastAreasOverlappingUnstashPosition = 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) val stashedBounds = getNearbyStashedPosition(unstashBounds, keepClearAreas)
return Placement( return Placement(
stashedBounds, stashedBounds,
pipAnchorBounds, pipAnchorBounds,
getStashType(stashedBounds, unstashBounds), getStashType(stashedBounds, unstashBounds),
unstashBounds, unstashBounds,
unstashTime areasOverlappingUnstashPositionChanged
) )
} }
@@ -438,14 +410,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
} ?: bounds } ?: bounds
} }
/**
* 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. * Updates the size of the screen.
* *
@@ -492,10 +456,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
pipPermanentDecorInsets = insets pipPermanentDecorInsets = insets
} }
fun setPipTemporaryDecorInsets(insets: Insets) {
pipTemporaryDecorInsets = insets
}
/** /**
* @param open Whether this event marks the opening of an occupied segment * @param open Whether this event marks the opening of an occupied segment
* @param pos The coordinate of this event * @param pos The coordinate of this event
@@ -790,7 +750,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
private fun addDecors(size: Size): Size { private fun addDecors(size: Size): Size {
val bounds = Rect(0, 0, size.width, size.height) val bounds = Rect(0, 0, size.width, size.height)
bounds.inset(pipPermanentDecorInsets) bounds.inset(pipPermanentDecorInsets)
bounds.inset(pipTemporaryDecorInsets)
return Size(bounds.width(), bounds.height()) return Size(bounds.width(), bounds.height())
} }
@@ -805,19 +764,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
return bounds return bounds
} }
/**
* Removes the space that was reserved for temporary decorations around the PiP
* @param bounds the bounds (in base case) to remove the insets from
*/
private fun removeTemporaryDecorsTransformed(bounds: Rect): Rect {
if (pipTemporaryDecorInsets == Insets.NONE) return bounds
val reverseInsets = Insets.subtract(Insets.NONE, pipTemporaryDecorInsets)
val boundsInScreenSpace = fromTransformedSpace(bounds)
boundsInScreenSpace.inset(reverseInsets)
return toTransformedSpace(boundsInScreenSpace)
}
private fun Rect.offsetCopy(dx: Int, dy: Int) = Rect(this).apply { offset(dx, dy) } private fun Rect.offsetCopy(dx: Int, dy: Int) = Rect(this).apply { offset(dx, dy) }
private fun Rect.intersectsX(other: Rect) = right >= other.left && left <= other.right private fun Rect.intersectsX(other: Rect) = right >= other.left && left <= other.right
private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom

View File

@@ -209,7 +209,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showMovementMenuOnly()", TAG); "%s: showMovementMenuOnly()", TAG);
} }
mInMoveMode = true; setInMoveMode(true);
mCloseAfterExitMoveMenu = true; mCloseAfterExitMoveMenu = true;
showMenuInternal(); showMenuInternal();
} }
@@ -219,7 +219,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
if (DEBUG) { if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG);
} }
mInMoveMode = false; setInMoveMode(false);
mCloseAfterExitMoveMenu = false; mCloseAfterExitMoveMenu = false;
showMenuInternal(); showMenuInternal();
} }
@@ -293,6 +293,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
return mInMoveMode; return mInMoveMode;
} }
private void setInMoveMode(boolean moveMode) {
if (mInMoveMode == moveMode) {
return;
}
mInMoveMode = moveMode;
if (mDelegate != null) {
mDelegate.onInMoveModeChanged();
}
}
@Override @Override
public void onEnterMoveMode() { public void onEnterMoveMode() {
if (DEBUG) { if (DEBUG) {
@@ -300,7 +311,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
"%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode, "%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode,
mCloseAfterExitMoveMenu); mCloseAfterExitMoveMenu);
} }
mInMoveMode = true; setInMoveMode(true);
mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
} }
@@ -312,13 +323,13 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
mCloseAfterExitMoveMenu); mCloseAfterExitMoveMenu);
} }
if (mCloseAfterExitMoveMenu) { if (mCloseAfterExitMoveMenu) {
mInMoveMode = false; setInMoveMode(false);
mCloseAfterExitMoveMenu = false; mCloseAfterExitMoveMenu = false;
closeMenu(); closeMenu();
return true; return true;
} }
if (mInMoveMode) { if (mInMoveMode) {
mInMoveMode = false; setInMoveMode(false);
mPipMenuView.showButtonsMenu(); mPipMenuView.showButtonsMenu();
return true; return true;
} }

View File

@@ -37,6 +37,7 @@ android_test {
"androidx.test.ext.junit", "androidx.test.ext.junit",
"androidx.dynamicanimation_dynamicanimation", "androidx.dynamicanimation_dynamicanimation",
"dagger2", "dagger2",
"frameworks-base-testutils",
"kotlinx-coroutines-android", "kotlinx-coroutines-android",
"kotlinx-coroutines-core", "kotlinx-coroutines-core",
"mockito-target-extended-minus-junit4", "mockito-target-extended-minus-junit4",

View File

@@ -0,0 +1,255 @@
/*
* 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.content.Context
import android.content.res.Resources
import android.graphics.Rect
import android.os.Handler
import android.os.test.TestLooper
import android.testing.AndroidTestingRunner
import com.android.wm.shell.R
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT
import com.android.wm.shell.pip.tv.TvPipBoundsController.POSITION_DEBOUNCE_TIMEOUT_MILLIS
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.AdditionalAnswers.returnsFirstArg
import org.mockito.Mock
import org.mockito.Mockito.`when` as whenever
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.eq
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
class TvPipBoundsControllerTest {
val ANIMATION_DURATION = 100
val STASH_DURATION = 5000
val FAR_FUTURE = 60 * 60000L
val ANCHOR_BOUNDS = Rect(90, 90, 100, 100)
val STASHED_BOUNDS = Rect(99, 90, 109, 100)
val MOVED_BOUNDS = Rect(90, 80, 100, 90)
val STASHED_MOVED_BOUNDS = Rect(99, 80, 109, 90)
val ANCHOR_PLACEMENT = Placement(ANCHOR_BOUNDS, ANCHOR_BOUNDS)
val STASHED_PLACEMENT = Placement(STASHED_BOUNDS, ANCHOR_BOUNDS,
STASH_TYPE_RIGHT, ANCHOR_BOUNDS, false)
val STASHED_PLACEMENT_RESTASH = Placement(STASHED_BOUNDS, ANCHOR_BOUNDS,
STASH_TYPE_RIGHT, ANCHOR_BOUNDS, true)
val MOVED_PLACEMENT = Placement(MOVED_BOUNDS, ANCHOR_BOUNDS)
val STASHED_MOVED_PLACEMENT = Placement(STASHED_MOVED_BOUNDS, ANCHOR_BOUNDS,
STASH_TYPE_RIGHT, MOVED_BOUNDS, false)
val STASHED_MOVED_PLACEMENT_RESTASH = Placement(STASHED_MOVED_BOUNDS, ANCHOR_BOUNDS,
STASH_TYPE_RIGHT, MOVED_BOUNDS, true)
lateinit var boundsController: TvPipBoundsController
var time = 0L
lateinit var testLooper: TestLooper
lateinit var mainHandler: Handler
var inMenu = false
var inMoveMode = false
@Mock
lateinit var context: Context
@Mock
lateinit var resources: Resources
@Mock
lateinit var tvPipBoundsState: TvPipBoundsState
@Mock
lateinit var tvPipBoundsAlgorithm: TvPipBoundsAlgorithm
@Mock
lateinit var listener: TvPipBoundsController.PipBoundsListener
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
time = 0L
inMenu = false
inMoveMode = false
testLooper = TestLooper { time }
mainHandler = Handler(testLooper.getLooper())
whenever(context.resources).thenReturn(resources)
whenever(resources.getInteger(R.integer.config_pipStashDuration)).thenReturn(STASH_DURATION)
whenever(tvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(any()))
.then(returnsFirstArg<Rect>())
boundsController = TvPipBoundsController(
context,
{ time },
mainHandler,
tvPipBoundsState,
tvPipBoundsAlgorithm)
boundsController.setListener(listener)
}
@Test
fun testPlacement_MovedAfterDebounceTimeout() {
triggerPlacement(MOVED_PLACEMENT)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS)
assertNoMovementUpTo(time + FAR_FUTURE)
}
@Test
fun testStashedPlacement_MovedAfterDebounceTimeout_Unstashes() {
triggerPlacement(STASHED_PLACEMENT_RESTASH)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS)
}
@Test
fun testDebounceSamePlacement_MovesDebounceTimeoutAfterFirstPlacement() {
triggerPlacement(MOVED_PLACEMENT)
advanceTimeTo(POSITION_DEBOUNCE_TIMEOUT_MILLIS / 2)
triggerPlacement(MOVED_PLACEMENT)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS)
}
@Test
fun testNoMovementUntilPlacementStabilizes() {
triggerPlacement(ANCHOR_PLACEMENT)
advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10)
triggerPlacement(MOVED_PLACEMENT)
advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10)
triggerPlacement(ANCHOR_PLACEMENT)
advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10)
triggerPlacement(MOVED_PLACEMENT)
assertMovementAt(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS)
}
@Test
fun testUnstashIfStashNoLongerNecessary() {
triggerPlacement(STASHED_PLACEMENT_RESTASH)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
triggerPlacement(ANCHOR_PLACEMENT)
assertMovementAt(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS, ANCHOR_BOUNDS)
}
@Test
fun testRestashingPlacementDelaysUnstash() {
triggerPlacement(STASHED_PLACEMENT_RESTASH)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
assertNoMovementUpTo(time + STASH_DURATION / 2)
triggerPlacement(STASHED_PLACEMENT_RESTASH)
assertNoMovementUpTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS)
assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS)
}
@Test
fun testNonRestashingPlacementDoesNotDelayUnstash() {
triggerPlacement(STASHED_PLACEMENT_RESTASH)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
assertNoMovementUpTo(time + STASH_DURATION / 2)
triggerPlacement(STASHED_PLACEMENT)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS)
}
@Test
fun testImmediatePlacement() {
triggerImmediatePlacement(STASHED_PLACEMENT_RESTASH)
assertMovement(STASHED_BOUNDS)
assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS)
}
@Test
fun testInMoveMode_KeepAtAnchor() {
startMoveMode()
triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH)
assertMovement(ANCHOR_BOUNDS)
assertNoMovementUpTo(time + FAR_FUTURE)
}
@Test
fun testInMenu_Unstashed() {
openPipMenu()
triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH)
assertMovement(MOVED_BOUNDS)
assertNoMovementUpTo(time + FAR_FUTURE)
}
@Test
fun testCloseMenu_DoNotRestash() {
openPipMenu()
triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH)
assertMovement(MOVED_BOUNDS)
closePipMenu()
triggerPlacement(STASHED_MOVED_PLACEMENT)
assertNoMovementUpTo(time + FAR_FUTURE)
}
fun assertMovement(bounds: Rect) {
verify(listener).onPipTargetBoundsChange(eq(bounds), anyInt())
reset(listener)
}
fun assertMovementAt(timeMs: Long, bounds: Rect) {
assertNoMovementUpTo(timeMs - 1)
advanceTimeTo(timeMs)
assertMovement(bounds)
}
fun assertNoMovementUpTo(timeMs: Long) {
advanceTimeTo(timeMs)
verify(listener, never()).onPipTargetBoundsChange(any(), anyInt())
}
fun triggerPlacement(placement: Placement, immediate: Boolean = false) {
whenever(tvPipBoundsAlgorithm.getTvPipPlacement()).thenReturn(placement)
val stayAtAnchorPosition = inMoveMode
val disallowStashing = inMenu || stayAtAnchorPosition
boundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing,
ANIMATION_DURATION, immediate)
}
fun triggerImmediatePlacement(placement: Placement) {
triggerPlacement(placement, true)
}
fun openPipMenu() {
inMenu = true
inMoveMode = false
}
fun closePipMenu() {
inMenu = false
inMoveMode = false
}
fun startMoveMode() {
inMenu = true
inMoveMode = true
}
fun advanceTimeTo(ms: Long) {
time = ms
testLooper.dispatchAll()
}
}

View File

@@ -30,7 +30,9 @@ import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import junit.framework.Assert.assertEquals import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertNull import junit.framework.Assert.assertNull
import junit.framework.Assert.assertTrue
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
class TvPipKeepClearAlgorithmTest { class TvPipKeepClearAlgorithmTest {
@@ -46,7 +48,6 @@ class TvPipKeepClearAlgorithmTest {
private lateinit var pipSize: Size private lateinit var pipSize: Size
private lateinit var movementBounds: Rect private lateinit var movementBounds: Rect
private lateinit var algorithm: TvPipKeepClearAlgorithm private lateinit var algorithm: TvPipKeepClearAlgorithm
private var currentTime = 0L
private var restrictedAreas = mutableSetOf<Rect>() private var restrictedAreas = mutableSetOf<Rect>()
private var unrestrictedAreas = mutableSetOf<Rect>() private var unrestrictedAreas = mutableSetOf<Rect>()
private var gravity: Int = 0 private var gravity: Int = 0
@@ -58,16 +59,14 @@ class TvPipKeepClearAlgorithmTest {
restrictedAreas.clear() restrictedAreas.clear()
unrestrictedAreas.clear() unrestrictedAreas.clear()
currentTime = 0L
pipSize = DEFAULT_PIP_SIZE pipSize = DEFAULT_PIP_SIZE
gravity = Gravity.BOTTOM or Gravity.RIGHT gravity = Gravity.BOTTOM or Gravity.RIGHT
algorithm = TvPipKeepClearAlgorithm({ currentTime }) algorithm = TvPipKeepClearAlgorithm()
algorithm.setScreenSize(SCREEN_SIZE) algorithm.setScreenSize(SCREEN_SIZE)
algorithm.setMovementBounds(movementBounds) algorithm.setMovementBounds(movementBounds)
algorithm.pipAreaPadding = PADDING algorithm.pipAreaPadding = PADDING
algorithm.stashOffset = STASH_OFFSET algorithm.stashOffset = STASH_OFFSET
algorithm.stashDuration = 5000L
algorithm.setGravity(gravity) algorithm.setGravity(gravity)
algorithm.maxRestrictedDistanceFraction = 0.3 algorithm.maxRestrictedDistanceFraction = 0.3
} }
@@ -265,7 +264,7 @@ class TvPipKeepClearAlgorithmTest {
assertEquals(expectedBounds, placement.bounds) assertEquals(expectedBounds, placement.bounds)
assertEquals(STASH_TYPE_BOTTOM, placement.stashType) assertEquals(STASH_TYPE_BOTTOM, placement.stashType)
assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds) assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds)
assertEquals(algorithm.stashDuration, placement.unstashTime) assertTrue(placement.triggerStash)
} }
@Test @Test
@@ -305,7 +304,7 @@ class TvPipKeepClearAlgorithmTest {
assertEquals(expectedBounds, placement.bounds) assertEquals(expectedBounds, placement.bounds)
assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(STASH_TYPE_RIGHT, placement.stashType)
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
assertEquals(algorithm.stashDuration, placement.unstashTime) assertTrue(placement.triggerStash)
} }
@Test @Test
@@ -352,9 +351,7 @@ class TvPipKeepClearAlgorithmTest {
assertEquals(expectedBounds, placement.bounds) assertEquals(expectedBounds, placement.bounds)
assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(STASH_TYPE_RIGHT, placement.stashType)
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
assertEquals(algorithm.stashDuration, placement.unstashTime) assertTrue(placement.triggerStash)
currentTime += 1000
restrictedAreas.remove(sideBar) restrictedAreas.remove(sideBar)
placement = getActualPlacement() placement = getActualPlacement()
@@ -363,7 +360,7 @@ class TvPipKeepClearAlgorithmTest {
} }
@Test @Test
fun test_Stashed_UnstashBoundsStaysObstructed_UnstashesAfterTimeout() { fun test_Stashed_UnstashBoundsStaysObstructed_DoesNotTriggerStash() {
gravity = Gravity.BOTTOM or Gravity.RIGHT gravity = Gravity.BOTTOM or Gravity.RIGHT
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
@@ -384,13 +381,13 @@ class TvPipKeepClearAlgorithmTest {
assertEquals(expectedBounds, placement.bounds) assertEquals(expectedBounds, placement.bounds)
assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(STASH_TYPE_RIGHT, placement.stashType)
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
assertEquals(algorithm.stashDuration, placement.unstashTime) assertTrue(placement.triggerStash)
currentTime += algorithm.stashDuration
placement = getActualPlacement() placement = getActualPlacement()
assertEquals(expectedUnstashBounds, placement.bounds) assertEquals(expectedBounds, placement.bounds)
assertNotStashed(placement) assertEquals(STASH_TYPE_RIGHT, placement.stashType)
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
assertFalse(placement.triggerStash)
} }
@Test @Test
@@ -415,9 +412,7 @@ class TvPipKeepClearAlgorithmTest {
assertEquals(expectedBounds, placement.bounds) assertEquals(expectedBounds, placement.bounds)
assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(STASH_TYPE_RIGHT, placement.stashType)
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
assertEquals(algorithm.stashDuration, placement.unstashTime) assertTrue(placement.triggerStash)
currentTime += 1000
val newObstruction = Rect( val newObstruction = Rect(
0, 0,
@@ -431,7 +426,7 @@ class TvPipKeepClearAlgorithmTest {
assertEquals(expectedBounds, placement.bounds) assertEquals(expectedBounds, placement.bounds)
assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(STASH_TYPE_RIGHT, placement.stashType)
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
assertEquals(currentTime + algorithm.stashDuration, placement.unstashTime) assertTrue(placement.triggerStash)
} }
@Test @Test
@@ -458,21 +453,9 @@ class TvPipKeepClearAlgorithmTest {
@Test @Test
fun test_PipInsets() { fun test_PipInsets() {
val permInsets = Insets.of(-1, -2, -3, -4) val insets = Insets.of(-1, -2, -3, -4)
algorithm.setPipPermanentDecorInsets(permInsets) algorithm.setPipPermanentDecorInsets(insets)
testInsetsForAllPositions(permInsets)
val tempInsets = Insets.of(-4, -3, -2, -1)
algorithm.setPipPermanentDecorInsets(Insets.NONE)
algorithm.setPipTemporaryDecorInsets(tempInsets)
testInsetsForAllPositions(tempInsets)
algorithm.setPipPermanentDecorInsets(permInsets)
algorithm.setPipTemporaryDecorInsets(tempInsets)
testInsetsForAllPositions(Insets.add(permInsets, tempInsets))
}
private fun testInsetsForAllPositions(insets: Insets) {
gravity = Gravity.BOTTOM or Gravity.RIGHT gravity = Gravity.BOTTOM or Gravity.RIGHT
testAnchorPositionWithInsets(insets) testAnchorPositionWithInsets(insets)
@@ -546,6 +529,6 @@ class TvPipKeepClearAlgorithmTest {
private fun assertNotStashed(actual: Placement) { private fun assertNotStashed(actual: Placement) {
assertEquals(STASH_TYPE_NONE, actual.stashType) assertEquals(STASH_TYPE_NONE, actual.stashType)
assertNull(actual.unstashDestinationBounds) assertNull(actual.unstashDestinationBounds)
assertEquals(0L, actual.unstashTime) assertFalse(actual.triggerStash)
} }
} }