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:
committed by
Android (Google) Code Review
commit
d338520c9f
@@ -18,6 +18,7 @@ package com.android.wm.shell.dagger;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
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.PipUiEventLogger;
|
||||
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.TvPipController;
|
||||
import com.android.wm.shell.pip.tv.TvPipMenuController;
|
||||
@@ -64,6 +66,7 @@ public abstract class TvPipModule {
|
||||
Context context,
|
||||
TvPipBoundsState tvPipBoundsState,
|
||||
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
|
||||
TvPipBoundsController tvPipBoundsController,
|
||||
PipAppOpsListener pipAppOpsListener,
|
||||
PipTaskOrganizer pipTaskOrganizer,
|
||||
TvPipMenuController tvPipMenuController,
|
||||
@@ -74,13 +77,13 @@ public abstract class TvPipModule {
|
||||
PipParamsChangedForwarder pipParamsChangedForwarder,
|
||||
DisplayController displayController,
|
||||
WindowManagerShellWrapper windowManagerShellWrapper,
|
||||
@ShellMainThread ShellExecutor mainExecutor,
|
||||
@ShellMainThread Handler mainHandler) {
|
||||
@ShellMainThread ShellExecutor mainExecutor) {
|
||||
return Optional.of(
|
||||
TvPipController.create(
|
||||
context,
|
||||
tvPipBoundsState,
|
||||
tvPipBoundsAlgorithm,
|
||||
tvPipBoundsController,
|
||||
pipAppOpsListener,
|
||||
pipTaskOrganizer,
|
||||
pipTransitionController,
|
||||
@@ -91,8 +94,22 @@ public abstract class TvPipModule {
|
||||
pipParamsChangedForwarder,
|
||||
displayController,
|
||||
windowManagerShellWrapper,
|
||||
mainExecutor,
|
||||
mainHandler));
|
||||
mainExecutor));
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
@Provides
|
||||
static TvPipBoundsController provideTvPipBoundsController(
|
||||
Context context,
|
||||
@ShellMainThread Handler mainHandler,
|
||||
TvPipBoundsState tvPipBoundsState,
|
||||
TvPipBoundsAlgorithm tvPipBoundsAlgorithm) {
|
||||
return new TvPipBoundsController(
|
||||
context,
|
||||
SystemClock::uptimeMillis,
|
||||
mainHandler,
|
||||
tvPipBoundsState,
|
||||
tvPipBoundsAlgorithm);
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
|
||||
@@ -29,7 +29,6 @@ import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Insets;
|
||||
import android.graphics.Rect;
|
||||
import android.os.SystemClock;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Size;
|
||||
import android.view.Gravity;
|
||||
@@ -66,7 +65,7 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
||||
@NonNull PipSnapAlgorithm pipSnapAlgorithm) {
|
||||
super(context, tvPipBoundsState, pipSnapAlgorithm);
|
||||
this.mTvPipBoundsState = tvPipBoundsState;
|
||||
this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(SystemClock::uptimeMillis);
|
||||
this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm();
|
||||
reloadResources(context);
|
||||
}
|
||||
|
||||
@@ -80,7 +79,6 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
||||
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
|
||||
@@ -104,7 +102,7 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
||||
updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true);
|
||||
}
|
||||
mTvPipBoundsState.setTvPipExpanded(isPipExpanded);
|
||||
return getTvPipBounds().getBounds();
|
||||
return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds());
|
||||
}
|
||||
|
||||
/** 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,
|
||||
"%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.
|
||||
*/
|
||||
public Placement getTvPipBounds() {
|
||||
@NonNull
|
||||
public Placement getTvPipPlacement() {
|
||||
final Size pipSize = getPipSize();
|
||||
final Rect displayBounds = mTvPipBoundsState.getDisplayBounds();
|
||||
final Size screenSize = new Size(displayBounds.width(), displayBounds.height());
|
||||
@@ -153,8 +165,6 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
||||
mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset());
|
||||
mKeepClearAlgorithm.setPipPermanentDecorInsets(
|
||||
mTvPipBoundsState.getPipMenuPermanentDecorInsets());
|
||||
mKeepClearAlgorithm.setPipTemporaryDecorInsets(
|
||||
mTvPipBoundsState.getPipMenuTemporaryDecorInsets());
|
||||
|
||||
final Placement placement = mKeepClearAlgorithm.calculatePipPosition(
|
||||
pipSize,
|
||||
@@ -407,8 +417,4 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
|
||||
TAG, expandedSize.getWidth(), expandedSize.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
void keepUnstashedForCurrentKeepClearAreas() {
|
||||
mKeepClearAlgorithm.keepUnstashedForCurrentKeepClearAreas();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,10 @@ import android.app.ActivityTaskManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.RemoteAction;
|
||||
import android.app.TaskInfo;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteException;
|
||||
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.PipAnimationController;
|
||||
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.PipParamsChangedForwarder;
|
||||
import com.android.wm.shell.pip.PipTaskOrganizer;
|
||||
import com.android.wm.shell.pip.PipTransitionController;
|
||||
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
|
||||
import com.android.wm.shell.protolog.ShellProtoLogGroup;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Manages the picture-in-picture (PIP) UI and states.
|
||||
*/
|
||||
public class TvPipController implements PipTransitionController.PipTransitionCallback,
|
||||
TvPipMenuController.Delegate, TvPipNotificationController.Delegate,
|
||||
DisplayController.OnDisplaysChangedListener {
|
||||
TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate,
|
||||
TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener {
|
||||
private static final String TAG = "TvPipController";
|
||||
static final boolean DEBUG = false;
|
||||
|
||||
@@ -98,19 +95,18 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
|
||||
private final TvPipBoundsState mTvPipBoundsState;
|
||||
private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
|
||||
private final TvPipBoundsController mTvPipBoundsController;
|
||||
private final PipAppOpsListener mAppOpsListener;
|
||||
private final PipTaskOrganizer mPipTaskOrganizer;
|
||||
private final PipMediaController mPipMediaController;
|
||||
private final TvPipNotificationController mPipNotificationController;
|
||||
private final TvPipMenuController mTvPipMenuController;
|
||||
private final ShellExecutor mMainExecutor;
|
||||
private final Handler mMainHandler;
|
||||
private final TvPipImpl mImpl = new TvPipImpl();
|
||||
|
||||
private @State int mState = STATE_NO_PIP;
|
||||
private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY;
|
||||
private int mPinnedTaskId = NONEXISTENT_TASK_ID;
|
||||
private Runnable mUnstashRunnable;
|
||||
|
||||
private RemoteAction mCloseAction;
|
||||
// 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,
|
||||
TvPipBoundsState tvPipBoundsState,
|
||||
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
|
||||
TvPipBoundsController tvPipBoundsController,
|
||||
PipAppOpsListener pipAppOpsListener,
|
||||
PipTaskOrganizer pipTaskOrganizer,
|
||||
PipTransitionController pipTransitionController,
|
||||
@@ -133,12 +130,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
PipParamsChangedForwarder pipParamsChangedForwarder,
|
||||
DisplayController displayController,
|
||||
WindowManagerShellWrapper wmShell,
|
||||
ShellExecutor mainExecutor,
|
||||
Handler mainHandler) {
|
||||
ShellExecutor mainExecutor) {
|
||||
return new TvPipController(
|
||||
context,
|
||||
tvPipBoundsState,
|
||||
tvPipBoundsAlgorithm,
|
||||
tvPipBoundsController,
|
||||
pipAppOpsListener,
|
||||
pipTaskOrganizer,
|
||||
pipTransitionController,
|
||||
@@ -149,14 +146,14 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
pipParamsChangedForwarder,
|
||||
displayController,
|
||||
wmShell,
|
||||
mainExecutor,
|
||||
mainHandler).mImpl;
|
||||
mainExecutor).mImpl;
|
||||
}
|
||||
|
||||
private TvPipController(
|
||||
Context context,
|
||||
TvPipBoundsState tvPipBoundsState,
|
||||
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
|
||||
TvPipBoundsController tvPipBoundsController,
|
||||
PipAppOpsListener pipAppOpsListener,
|
||||
PipTaskOrganizer pipTaskOrganizer,
|
||||
PipTransitionController pipTransitionController,
|
||||
@@ -167,16 +164,16 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
PipParamsChangedForwarder pipParamsChangedForwarder,
|
||||
DisplayController displayController,
|
||||
WindowManagerShellWrapper wmShell,
|
||||
ShellExecutor mainExecutor,
|
||||
Handler mainHandler) {
|
||||
ShellExecutor mainExecutor) {
|
||||
mContext = context;
|
||||
mMainExecutor = mainExecutor;
|
||||
mMainHandler = mainHandler;
|
||||
|
||||
mTvPipBoundsState = tvPipBoundsState;
|
||||
mTvPipBoundsState.setDisplayId(context.getDisplayId());
|
||||
mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay()));
|
||||
mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
|
||||
mTvPipBoundsController = tvPipBoundsController;
|
||||
mTvPipBoundsController.setListener(this);
|
||||
|
||||
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
|
||||
* 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
|
||||
public void showPictureInPictureMenu() {
|
||||
@@ -256,7 +253,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
"%s: closeMenu(), state before=%s", TAG, stateToName(mState));
|
||||
}
|
||||
setState(STATE_PIP);
|
||||
mTvPipBoundsAlgorithm.keepUnstashedForCurrentKeepClearAreas();
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
|
||||
@@ -331,68 +327,35 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
|
||||
Set<Rect> unrestricted) {
|
||||
if (mTvPipBoundsState.getDisplayId() == displayId) {
|
||||
boolean unrestrictedAreasChanged = !Objects.equals(unrestricted,
|
||||
mTvPipBoundsState.getUnrestrictedKeepClearAreas());
|
||||
mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted);
|
||||
updatePinnedStackBounds();
|
||||
updatePinnedStackBounds(mResizeAnimationDuration, unrestrictedAreasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePinnedStackBounds() {
|
||||
updatePinnedStackBounds(mResizeAnimationDuration);
|
||||
updatePinnedStackBounds(mResizeAnimationDuration, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean stayAtAnchorPosition = mTvPipMenuController.isInMoveMode();
|
||||
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(), animationDuration);
|
||||
};
|
||||
mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime());
|
||||
}
|
||||
mTvPipBoundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing,
|
||||
animationDuration, immediate);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
@Override
|
||||
public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) {
|
||||
mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds,
|
||||
animationDuration, rect -> mTvPipMenuController.updateExpansionState());
|
||||
mTvPipMenuController.onPipTransitionStarted(newTargetBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -431,7 +394,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
|
||||
@Override
|
||||
public void closeEduText() {
|
||||
updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs);
|
||||
updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false);
|
||||
}
|
||||
|
||||
private void registerSessionListenerForCurrentUser() {
|
||||
@@ -473,6 +436,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
mPipNotificationController.dismiss();
|
||||
mTvPipMenuController.closeMenu();
|
||||
mTvPipBoundsState.resetTvPipState();
|
||||
mTvPipBoundsController.onPipDismissed();
|
||||
setState(STATE_NO_PIP);
|
||||
mPinnedTaskId = NONEXISTENT_TASK_ID;
|
||||
}
|
||||
|
||||
@@ -33,17 +33,14 @@ 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) {
|
||||
class TvPipKeepClearAlgorithm() {
|
||||
/**
|
||||
* Result of the positioning algorithm.
|
||||
*
|
||||
@@ -51,17 +48,17 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
* @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]
|
||||
* @param unstashDestinationBounds If stashed, the PiP should move to this position when
|
||||
* unstashing.
|
||||
* @param triggerStash Whether this placement should trigger the PiP to stash, or extend
|
||||
* the unstash timeout if already stashed.
|
||||
*/
|
||||
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
|
||||
val triggerStash: Boolean = false
|
||||
) {
|
||||
/** Bounds to use if the PiP should not be stashed. */
|
||||
fun getUnstashedBounds() = unstashDestinationBounds ?: bounds
|
||||
@@ -79,12 +76,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
/** 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
|
||||
|
||||
@@ -93,14 +84,10 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
private var transformedMovementBounds = Rect()
|
||||
|
||||
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
|
||||
* decorations are relevant for calculating intersecting keep clear areas */
|
||||
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
|
||||
@@ -113,8 +100,8 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
* 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].
|
||||
* as [Placement.bounds], along with a position to move to when the PiP unstashes
|
||||
* as [Placement.unstashDestinationBounds].
|
||||
*
|
||||
* @param pipSize The size of the PiP window
|
||||
* @param restrictedAreas The restricted keep clear areas
|
||||
@@ -130,13 +117,11 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas)
|
||||
|
||||
val pipSizeWithAllDecors = addDecors(pipSize)
|
||||
val pipAnchorBoundsWithAllDecors =
|
||||
val pipAnchorBoundsWithDecors =
|
||||
getNormalPipAnchorBounds(pipSizeWithAllDecors, transformedMovementBounds)
|
||||
|
||||
val pipAnchorBoundsWithPermanentDecors =
|
||||
removeTemporaryDecorsTransformed(pipAnchorBoundsWithAllDecors)
|
||||
val result = calculatePipPositionTransformed(
|
||||
pipAnchorBoundsWithPermanentDecors,
|
||||
pipAnchorBoundsWithDecors,
|
||||
transformedRestrictedAreas,
|
||||
transformedUnrestrictedAreas
|
||||
)
|
||||
@@ -152,7 +137,7 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
anchorBounds,
|
||||
getStashType(pipBounds, unstashedDestBounds),
|
||||
unstashedDestBounds,
|
||||
result.unstashTime
|
||||
result.triggerStash
|
||||
)
|
||||
}
|
||||
|
||||
@@ -213,26 +198,13 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
!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, unstashBounds),
|
||||
unstashBounds,
|
||||
unstashTime
|
||||
areasOverlappingUnstashPositionChanged
|
||||
)
|
||||
}
|
||||
|
||||
@@ -438,14 +410,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
} ?: 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.
|
||||
*
|
||||
@@ -492,10 +456,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
pipPermanentDecorInsets = insets
|
||||
}
|
||||
|
||||
fun setPipTemporaryDecorInsets(insets: Insets) {
|
||||
pipTemporaryDecorInsets = insets
|
||||
}
|
||||
|
||||
/**
|
||||
* @param open Whether this event marks the opening of an occupied segment
|
||||
* @param pos The coordinate of this event
|
||||
@@ -790,7 +750,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
private fun addDecors(size: Size): Size {
|
||||
val bounds = Rect(0, 0, size.width, size.height)
|
||||
bounds.inset(pipPermanentDecorInsets)
|
||||
bounds.inset(pipTemporaryDecorInsets)
|
||||
|
||||
return Size(bounds.width(), bounds.height())
|
||||
}
|
||||
@@ -805,19 +764,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
|
||||
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.intersectsX(other: Rect) = right >= other.left && left <= other.right
|
||||
private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom
|
||||
|
||||
@@ -209,7 +209,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: showMovementMenuOnly()", TAG);
|
||||
}
|
||||
mInMoveMode = true;
|
||||
setInMoveMode(true);
|
||||
mCloseAfterExitMoveMenu = true;
|
||||
showMenuInternal();
|
||||
}
|
||||
@@ -219,7 +219,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
if (DEBUG) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG);
|
||||
}
|
||||
mInMoveMode = false;
|
||||
setInMoveMode(false);
|
||||
mCloseAfterExitMoveMenu = false;
|
||||
showMenuInternal();
|
||||
}
|
||||
@@ -293,6 +293,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
return mInMoveMode;
|
||||
}
|
||||
|
||||
private void setInMoveMode(boolean moveMode) {
|
||||
if (mInMoveMode == moveMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
mInMoveMode = moveMode;
|
||||
if (mDelegate != null) {
|
||||
mDelegate.onInMoveModeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnterMoveMode() {
|
||||
if (DEBUG) {
|
||||
@@ -300,7 +311,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
"%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode,
|
||||
mCloseAfterExitMoveMenu);
|
||||
}
|
||||
mInMoveMode = true;
|
||||
setInMoveMode(true);
|
||||
mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
|
||||
}
|
||||
|
||||
@@ -312,13 +323,13 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
mCloseAfterExitMoveMenu);
|
||||
}
|
||||
if (mCloseAfterExitMoveMenu) {
|
||||
mInMoveMode = false;
|
||||
setInMoveMode(false);
|
||||
mCloseAfterExitMoveMenu = false;
|
||||
closeMenu();
|
||||
return true;
|
||||
}
|
||||
if (mInMoveMode) {
|
||||
mInMoveMode = false;
|
||||
setInMoveMode(false);
|
||||
mPipMenuView.showButtonsMenu();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ android_test {
|
||||
"androidx.test.ext.junit",
|
||||
"androidx.dynamicanimation_dynamicanimation",
|
||||
"dagger2",
|
||||
"frameworks-base-testutils",
|
||||
"kotlinx-coroutines-android",
|
||||
"kotlinx-coroutines-core",
|
||||
"mockito-target-extended-minus-junit4",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,9 @@ 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.assertFalse
|
||||
import junit.framework.Assert.assertNull
|
||||
import junit.framework.Assert.assertTrue
|
||||
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class TvPipKeepClearAlgorithmTest {
|
||||
@@ -46,7 +48,6 @@ class TvPipKeepClearAlgorithmTest {
|
||||
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
|
||||
@@ -58,16 +59,14 @@ class TvPipKeepClearAlgorithmTest {
|
||||
|
||||
restrictedAreas.clear()
|
||||
unrestrictedAreas.clear()
|
||||
currentTime = 0L
|
||||
pipSize = DEFAULT_PIP_SIZE
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
algorithm = TvPipKeepClearAlgorithm({ currentTime })
|
||||
algorithm = TvPipKeepClearAlgorithm()
|
||||
algorithm.setScreenSize(SCREEN_SIZE)
|
||||
algorithm.setMovementBounds(movementBounds)
|
||||
algorithm.pipAreaPadding = PADDING
|
||||
algorithm.stashOffset = STASH_OFFSET
|
||||
algorithm.stashDuration = 5000L
|
||||
algorithm.setGravity(gravity)
|
||||
algorithm.maxRestrictedDistanceFraction = 0.3
|
||||
}
|
||||
@@ -265,7 +264,7 @@ class TvPipKeepClearAlgorithmTest {
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_BOTTOM, placement.stashType)
|
||||
assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds)
|
||||
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||
assertTrue(placement.triggerStash)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -305,7 +304,7 @@ class TvPipKeepClearAlgorithmTest {
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||
assertTrue(placement.triggerStash)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -352,9 +351,7 @@ class TvPipKeepClearAlgorithmTest {
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||
|
||||
currentTime += 1000
|
||||
assertTrue(placement.triggerStash)
|
||||
|
||||
restrictedAreas.remove(sideBar)
|
||||
placement = getActualPlacement()
|
||||
@@ -363,7 +360,7 @@ class TvPipKeepClearAlgorithmTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_Stashed_UnstashBoundsStaysObstructed_UnstashesAfterTimeout() {
|
||||
fun test_Stashed_UnstashBoundsStaysObstructed_DoesNotTriggerStash() {
|
||||
gravity = Gravity.BOTTOM or Gravity.RIGHT
|
||||
|
||||
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
|
||||
@@ -384,13 +381,13 @@ class TvPipKeepClearAlgorithmTest {
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||
|
||||
currentTime += algorithm.stashDuration
|
||||
assertTrue(placement.triggerStash)
|
||||
|
||||
placement = getActualPlacement()
|
||||
assertEquals(expectedUnstashBounds, placement.bounds)
|
||||
assertNotStashed(placement)
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||
assertFalse(placement.triggerStash)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -415,9 +412,7 @@ class TvPipKeepClearAlgorithmTest {
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||
assertEquals(algorithm.stashDuration, placement.unstashTime)
|
||||
|
||||
currentTime += 1000
|
||||
assertTrue(placement.triggerStash)
|
||||
|
||||
val newObstruction = Rect(
|
||||
0,
|
||||
@@ -431,7 +426,7 @@ class TvPipKeepClearAlgorithmTest {
|
||||
assertEquals(expectedBounds, placement.bounds)
|
||||
assertEquals(STASH_TYPE_RIGHT, placement.stashType)
|
||||
assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
|
||||
assertEquals(currentTime + algorithm.stashDuration, placement.unstashTime)
|
||||
assertTrue(placement.triggerStash)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -458,21 +453,9 @@ class TvPipKeepClearAlgorithmTest {
|
||||
|
||||
@Test
|
||||
fun test_PipInsets() {
|
||||
val permInsets = Insets.of(-1, -2, -3, -4)
|
||||
algorithm.setPipPermanentDecorInsets(permInsets)
|
||||
testInsetsForAllPositions(permInsets)
|
||||
val insets = Insets.of(-1, -2, -3, -4)
|
||||
algorithm.setPipPermanentDecorInsets(insets)
|
||||
|
||||
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
|
||||
testAnchorPositionWithInsets(insets)
|
||||
|
||||
@@ -546,6 +529,6 @@ class TvPipKeepClearAlgorithmTest {
|
||||
private fun assertNotStashed(actual: Placement) {
|
||||
assertEquals(STASH_TYPE_NONE, actual.stashType)
|
||||
assertNull(actual.unstashDestinationBounds)
|
||||
assertEquals(0L, actual.unstashTime)
|
||||
assertFalse(actual.triggerStash)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user