Respect minimum dimensions for embedded Activities
Before this CL, minimum dimensions of Activity wasn't respected, that said, an Activity could be embedded in a TaskFragment of which bounds are smaller its minimum dimensions. This CL add the minimum dimensions on several places: WM core: 1. Verify minimum dimension requirement before adding an Activity to a TaskFragment. It'll be early return if the requirement is not satisfied. 2. Propagate the minimum dimensions to the client side through TaskFragmentInfo to notify the requirement. 3. If TaskFragmentOrganizer tries to shrink a TaskFragment to the bounds that smaller than minimum dimensions of its children Activity, switch to match the parent bounds. AndroidX Window extensions: 1. Early return if TaskFragment is resized to the bounds that smaller than minimum dimensions which dispatched from the server side. 2. When organizer tries to show Activities side-by-side, verify if minimum dimensions requirement of the primary Activiy. If the requirement is not satisfied, show Activities in fullscreen instead. TODO: Add an API to check if an Activity intent is allowed to embed in a TaskFragment. Bug: 232871351 Test: atest TaskFragmentOrganizerControllerTest Test: atest TaskFragmentOrganizerTest TaskFragmentOrganizerPolicyTest Test: atest SplitActivityLifecycleTest Test: atest CtsWindowManagerJetpackTestCases Test: atest WmJetpackUnitTests Merged-In: Ib46c2cec2a0735b9e3f3420f2cb94754801b86b9 Change-Id: Ib46c2cec2a0735b9e3f3420f2cb94754801b86b9
This commit is contained in:
@@ -8260,6 +8260,11 @@ public class Activity extends ContextThemeWrapper
|
||||
return mMainThread;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public final ActivityInfo getActivityInfo() {
|
||||
return mActivityInfo;
|
||||
}
|
||||
|
||||
final void performCreate(Bundle icicle) {
|
||||
performCreate(icicle, null);
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ import static java.util.Objects.requireNonNull;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.TestApi;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
@@ -66,7 +68,7 @@ public final class TaskFragmentInfo implements Parcelable {
|
||||
private final List<IBinder> mActivities = new ArrayList<>();
|
||||
|
||||
/** Relative position of the fragment's top left corner in the parent container. */
|
||||
private final Point mPositionInParent;
|
||||
private final Point mPositionInParent = new Point();
|
||||
|
||||
/**
|
||||
* Whether the last running activity in the TaskFragment was finished due to clearing task while
|
||||
@@ -80,21 +82,31 @@ public final class TaskFragmentInfo implements Parcelable {
|
||||
*/
|
||||
private final boolean mIsTaskFragmentClearedForPip;
|
||||
|
||||
/**
|
||||
* The maximum {@link ActivityInfo.WindowLayout#minWidth} and
|
||||
* {@link ActivityInfo.WindowLayout#minHeight} aggregated from the TaskFragment's child
|
||||
* activities.
|
||||
*/
|
||||
@NonNull
|
||||
private final Point mMinimumDimensions = new Point();
|
||||
|
||||
/** @hide */
|
||||
public TaskFragmentInfo(
|
||||
@NonNull IBinder fragmentToken, @NonNull WindowContainerToken token,
|
||||
@NonNull Configuration configuration, int runningActivityCount,
|
||||
boolean isVisible, @NonNull List<IBinder> activities, @NonNull Point positionInParent,
|
||||
boolean isTaskClearedForReuse, boolean isTaskFragmentClearedForPip) {
|
||||
boolean isTaskClearedForReuse, boolean isTaskFragmentClearedForPip,
|
||||
@NonNull Point minimumDimensions) {
|
||||
mFragmentToken = requireNonNull(fragmentToken);
|
||||
mToken = requireNonNull(token);
|
||||
mConfiguration.setTo(configuration);
|
||||
mRunningActivityCount = runningActivityCount;
|
||||
mIsVisible = isVisible;
|
||||
mActivities.addAll(activities);
|
||||
mPositionInParent = requireNonNull(positionInParent);
|
||||
mPositionInParent.set(positionInParent);
|
||||
mIsTaskClearedForReuse = isTaskClearedForReuse;
|
||||
mIsTaskFragmentClearedForPip = isTaskFragmentClearedForPip;
|
||||
mMinimumDimensions.set(minimumDimensions);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -153,6 +165,26 @@ public final class TaskFragmentInfo implements Parcelable {
|
||||
return mConfiguration.windowConfiguration.getWindowingMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum width this TaskFragment can be resized to.
|
||||
* Client side must not {@link WindowContainerTransaction#setBounds(WindowContainerToken, Rect)}
|
||||
* that {@link Rect#width()} is shorter than the reported value.
|
||||
* @hide pending unhide
|
||||
*/
|
||||
public int getMinimumWidth() {
|
||||
return mMinimumDimensions.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum width this TaskFragment can be resized to.
|
||||
* Client side must not {@link WindowContainerTransaction#setBounds(WindowContainerToken, Rect)}
|
||||
* that {@link Rect#height()} is shorter than the reported value.
|
||||
* @hide pending unhide
|
||||
*/
|
||||
public int getMinimumHeight() {
|
||||
return mMinimumDimensions.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the parameters that are important for task fragment organizers are
|
||||
* equal between this {@link TaskFragmentInfo} and {@param that}.
|
||||
@@ -170,7 +202,8 @@ public final class TaskFragmentInfo implements Parcelable {
|
||||
&& mActivities.equals(that.mActivities)
|
||||
&& mPositionInParent.equals(that.mPositionInParent)
|
||||
&& mIsTaskClearedForReuse == that.mIsTaskClearedForReuse
|
||||
&& mIsTaskFragmentClearedForPip == that.mIsTaskFragmentClearedForPip;
|
||||
&& mIsTaskFragmentClearedForPip == that.mIsTaskFragmentClearedForPip
|
||||
&& mMinimumDimensions.equals(that.mMinimumDimensions);
|
||||
}
|
||||
|
||||
private TaskFragmentInfo(Parcel in) {
|
||||
@@ -180,9 +213,10 @@ public final class TaskFragmentInfo implements Parcelable {
|
||||
mRunningActivityCount = in.readInt();
|
||||
mIsVisible = in.readBoolean();
|
||||
in.readBinderList(mActivities);
|
||||
mPositionInParent = requireNonNull(in.readTypedObject(Point.CREATOR));
|
||||
mPositionInParent.readFromParcel(in);
|
||||
mIsTaskClearedForReuse = in.readBoolean();
|
||||
mIsTaskFragmentClearedForPip = in.readBoolean();
|
||||
mMinimumDimensions.readFromParcel(in);
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@@ -194,9 +228,10 @@ public final class TaskFragmentInfo implements Parcelable {
|
||||
dest.writeInt(mRunningActivityCount);
|
||||
dest.writeBoolean(mIsVisible);
|
||||
dest.writeBinderList(mActivities);
|
||||
dest.writeTypedObject(mPositionInParent, flags);
|
||||
mPositionInParent.writeToParcel(dest, flags);
|
||||
dest.writeBoolean(mIsTaskClearedForReuse);
|
||||
dest.writeBoolean(mIsTaskFragmentClearedForPip);
|
||||
mMinimumDimensions.writeToParcel(dest, flags);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -224,6 +259,7 @@ public final class TaskFragmentInfo implements Parcelable {
|
||||
+ " positionInParent=" + mPositionInParent
|
||||
+ " isTaskClearedForReuse=" + mIsTaskClearedForReuse
|
||||
+ " isTaskFragmentClearedForPip" + mIsTaskFragmentClearedForPip
|
||||
+ " minimumDimensions" + mMinimumDimensions
|
||||
+ "}";
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,7 @@ public class Point implements Parcelable {
|
||||
}
|
||||
|
||||
public Point(@NonNull Point src) {
|
||||
this.x = src.x;
|
||||
this.y = src.y;
|
||||
set(src);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +47,15 @@ public class Point implements Parcelable {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the point's from {@code src}'s coordinates
|
||||
* @hide
|
||||
*/
|
||||
public void set(@NonNull Point src) {
|
||||
this.x = src.x;
|
||||
this.y = src.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negate the point's coordinates
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,8 @@ package androidx.window.extensions.embedding;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.Activity;
|
||||
import android.util.Pair;
|
||||
import android.util.Size;
|
||||
|
||||
/**
|
||||
* Client-side descriptor of a split that holds two containers.
|
||||
@@ -66,6 +68,13 @@ class SplitContainer {
|
||||
return mSplitRule;
|
||||
}
|
||||
|
||||
/** Returns the minimum dimension pair of primary container and secondary container. */
|
||||
@NonNull
|
||||
Pair<Size, Size> getMinDimensionsPair() {
|
||||
return new Pair<>(mPrimaryContainer.getMinDimensions(),
|
||||
mSecondaryContainer.getMinDimensions());
|
||||
}
|
||||
|
||||
boolean isPlaceholderContainer() {
|
||||
return (mSplitRule instanceof SplitPlaceholderRule);
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ import static androidx.window.extensions.embedding.SplitContainer.getFinishSecon
|
||||
import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule;
|
||||
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent;
|
||||
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
|
||||
import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions;
|
||||
import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair;
|
||||
import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
|
||||
import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityClient;
|
||||
import android.app.ActivityOptions;
|
||||
@@ -43,11 +45,15 @@ import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.util.Size;
|
||||
import android.util.SparseArray;
|
||||
import android.window.TaskFragmentInfo;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
@@ -63,7 +69,7 @@ import java.util.function.Consumer;
|
||||
*/
|
||||
public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
|
||||
ActivityEmbeddingComponent {
|
||||
private static final String TAG = "SplitController";
|
||||
static final String TAG = "SplitController";
|
||||
|
||||
@VisibleForTesting
|
||||
@GuardedBy("mLock")
|
||||
@@ -350,7 +356,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
|
||||
if (!(rule instanceof SplitRule)) {
|
||||
continue;
|
||||
}
|
||||
if (mPresenter.shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) {
|
||||
if (shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -614,12 +620,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
|
||||
// Can launch in the existing secondary container if the rules share the same
|
||||
// presentation.
|
||||
final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
|
||||
if (secondaryContainer == getContainerWithActivity(secondaryActivity)) {
|
||||
if (secondaryContainer == getContainerWithActivity(secondaryActivity)
|
||||
&& !boundsSmallerThanMinDimensions(secondaryContainer.getLastRequestedBounds(),
|
||||
getMinDimensions(secondaryActivity))) {
|
||||
// The activity is already in the target TaskFragment.
|
||||
return true;
|
||||
}
|
||||
secondaryContainer.addPendingAppearedActivity(secondaryActivity);
|
||||
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity,
|
||||
secondaryActivity, null /* secondaryIntent */);
|
||||
wct.reparentActivityToTaskFragment(
|
||||
secondaryContainer.getTaskFragmentToken(),
|
||||
secondaryActivity.getActivityToken());
|
||||
@@ -791,6 +801,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
|
||||
&& (canReuseContainer(splitRule, splitContainer.getSplitRule())
|
||||
// TODO(b/231845476) we should always respect clearTop.
|
||||
|| !respectClearTop)) {
|
||||
mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity,
|
||||
null /* secondaryActivity */, intent);
|
||||
// Can launch in the existing secondary container if the rules share the same
|
||||
// presentation.
|
||||
return splitContainer.getSecondaryContainer();
|
||||
@@ -1117,8 +1129,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
|
||||
|
||||
// Check if there is enough space for launch
|
||||
final SplitPlaceholderRule placeholderRule = getPlaceholderRule(activity);
|
||||
if (placeholderRule == null || !mPresenter.shouldShowSideBySide(
|
||||
mPresenter.getParentContainerBounds(activity), placeholderRule)) {
|
||||
|
||||
if (placeholderRule == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity,
|
||||
placeholderRule.getPlaceholderIntent());
|
||||
if (!shouldShowSideBySide(
|
||||
mPresenter.getParentContainerBounds(activity), placeholderRule,
|
||||
minDimensionsPair)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1161,7 +1181,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mPresenter.shouldShowSideBySide(splitContainer)) {
|
||||
if (shouldShowSideBySide(splitContainer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1233,7 +1253,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
|
||||
// Splits that are not showing side-by-side are reported as having 0 split
|
||||
// ratio, since by definition in the API the primary container occupies no
|
||||
// width of the split when covered by the secondary.
|
||||
mPresenter.shouldShowSideBySide(container)
|
||||
shouldShowSideBySide(container)
|
||||
? container.getSplitRule().getSplitRatio()
|
||||
: 0.0f);
|
||||
splitStates.add(splitState);
|
||||
@@ -1402,7 +1422,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
|
||||
}
|
||||
// Decide whether the associated container should be retained based on the current
|
||||
// presentation mode.
|
||||
if (mPresenter.shouldShowSideBySide(splitContainer)) {
|
||||
if (shouldShowSideBySide(splitContainer)) {
|
||||
return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior);
|
||||
} else {
|
||||
return !shouldFinishAssociatedContainerWhenStacked(finishBehavior);
|
||||
|
||||
@@ -16,15 +16,23 @@
|
||||
|
||||
package androidx.window.extensions.embedding;
|
||||
|
||||
import static android.content.pm.PackageManager.MATCH_ALL;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityThread;
|
||||
import android.app.WindowConfiguration;
|
||||
import android.app.WindowConfiguration.WindowingMode;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.util.LayoutDirection;
|
||||
import android.util.Pair;
|
||||
import android.util.Size;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowMetrics;
|
||||
@@ -34,6 +42,8 @@ import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
@@ -41,9 +51,12 @@ import java.util.concurrent.Executor;
|
||||
* {@link SplitController}.
|
||||
*/
|
||||
class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
private static final int POSITION_START = 0;
|
||||
private static final int POSITION_END = 1;
|
||||
private static final int POSITION_FILL = 2;
|
||||
@VisibleForTesting
|
||||
static final int POSITION_START = 0;
|
||||
@VisibleForTesting
|
||||
static final int POSITION_END = 1;
|
||||
@VisibleForTesting
|
||||
static final int POSITION_FILL = 2;
|
||||
|
||||
@IntDef(value = {
|
||||
POSITION_START,
|
||||
@@ -103,8 +116,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
@NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity,
|
||||
@NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) {
|
||||
final Rect parentBounds = getParentContainerBounds(primaryActivity);
|
||||
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
|
||||
primaryActivity, secondaryIntent);
|
||||
final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
|
||||
isLtr(primaryActivity, rule));
|
||||
primaryActivity, minDimensionsPair);
|
||||
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
|
||||
primaryActivity, primaryRectBounds, null);
|
||||
|
||||
@@ -113,7 +128,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
final TaskFragmentContainer secondaryContainer = mController.newContainer(
|
||||
secondaryIntent, primaryActivity, taskId);
|
||||
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds,
|
||||
rule, isLtr(primaryActivity, rule));
|
||||
rule, primaryActivity, minDimensionsPair);
|
||||
final int windowingMode = mController.getTaskContainer(taskId)
|
||||
.getWindowingModeForSplitTaskFragment(secondaryRectBounds);
|
||||
createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
|
||||
@@ -121,7 +136,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
windowingMode);
|
||||
|
||||
// Set adjacent to each other so that the containers below will be invisible.
|
||||
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule);
|
||||
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
|
||||
minDimensionsPair);
|
||||
|
||||
mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
|
||||
|
||||
@@ -144,13 +160,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
|
||||
final Rect parentBounds = getParentContainerBounds(primaryActivity);
|
||||
final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity,
|
||||
secondaryActivity);
|
||||
final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
|
||||
isLtr(primaryActivity, rule));
|
||||
primaryActivity, minDimensionsPair);
|
||||
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
|
||||
primaryActivity, primaryRectBounds, null);
|
||||
|
||||
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
|
||||
isLtr(primaryActivity, rule));
|
||||
primaryActivity, minDimensionsPair);
|
||||
final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity(
|
||||
secondaryActivity);
|
||||
TaskFragmentContainer containerToAvoid = primaryContainer;
|
||||
@@ -162,7 +180,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
secondaryActivity, secondaryRectBounds, containerToAvoid);
|
||||
|
||||
// Set adjacent to each other so that the containers below will be invisible.
|
||||
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule);
|
||||
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
|
||||
minDimensionsPair);
|
||||
|
||||
mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
|
||||
|
||||
@@ -211,10 +230,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent,
|
||||
@Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) {
|
||||
final Rect parentBounds = getParentContainerBounds(launchingActivity);
|
||||
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
|
||||
launchingActivity, activityIntent);
|
||||
final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
|
||||
isLtr(launchingActivity, rule));
|
||||
launchingActivity, minDimensionsPair);
|
||||
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
|
||||
isLtr(launchingActivity, rule));
|
||||
launchingActivity, minDimensionsPair);
|
||||
|
||||
TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
|
||||
launchingActivity);
|
||||
@@ -258,11 +279,11 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
final boolean isLtr = isLtr(activity, rule);
|
||||
final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair();
|
||||
final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
|
||||
isLtr);
|
||||
activity, minDimensionsPair);
|
||||
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
|
||||
isLtr);
|
||||
activity, minDimensionsPair);
|
||||
final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
|
||||
// Whether the placeholder is becoming side-by-side with the primary from fullscreen.
|
||||
final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer()
|
||||
@@ -273,7 +294,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
// are created again.
|
||||
resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds);
|
||||
resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds);
|
||||
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule);
|
||||
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
|
||||
minDimensionsPair);
|
||||
if (isPlaceholderBecomingSplit) {
|
||||
// When placeholder is shown in split, we should keep the focus on the primary.
|
||||
wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
|
||||
@@ -287,11 +309,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
|
||||
private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
|
||||
@NonNull TaskFragmentContainer primaryContainer,
|
||||
@NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule) {
|
||||
@NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule,
|
||||
@NonNull Pair<Size, Size> minDimensionsPair) {
|
||||
final Rect parentBounds = getParentContainerBounds(primaryContainer);
|
||||
// Clear adjacent TaskFragments if the container is shown in fullscreen, or the
|
||||
// secondaryContainer could not be finished.
|
||||
if (!shouldShowSideBySide(parentBounds, splitRule)) {
|
||||
if (!shouldShowSideBySide(parentBounds, splitRule, minDimensionsPair)) {
|
||||
setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
|
||||
null /* secondary */, null /* splitRule */);
|
||||
} else {
|
||||
@@ -373,41 +396,160 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
super.updateWindowingMode(wct, fragmentToken, windowingMode);
|
||||
}
|
||||
|
||||
boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) {
|
||||
final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer());
|
||||
return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule());
|
||||
/**
|
||||
* Expands the split container if the current split bounds are smaller than the Activity or
|
||||
* Intent that is added to the container.
|
||||
*/
|
||||
void expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct,
|
||||
@NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity,
|
||||
@Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) {
|
||||
if (secondaryActivity == null && secondaryIntent == null) {
|
||||
throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be"
|
||||
+ " non-null.");
|
||||
}
|
||||
final Rect taskBounds = getTaskBoundsFromActivity(primaryActivity);
|
||||
final Pair<Size, Size> minDimensionsPair;
|
||||
if (secondaryActivity != null) {
|
||||
minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity);
|
||||
} else {
|
||||
minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity,
|
||||
secondaryIntent);
|
||||
}
|
||||
// Expand the splitContainer if minimum dimensions are not satisfied.
|
||||
if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) {
|
||||
expandTaskFragment(wct, splitContainer.getPrimaryContainer()
|
||||
.getTaskFragmentToken());
|
||||
expandTaskFragment(wct, splitContainer.getSecondaryContainer()
|
||||
.getTaskFragmentToken());
|
||||
}
|
||||
}
|
||||
|
||||
boolean shouldShowSideBySide(@Nullable Rect parentBounds, @NonNull SplitRule rule) {
|
||||
static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) {
|
||||
return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */);
|
||||
}
|
||||
|
||||
static boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) {
|
||||
final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer());
|
||||
|
||||
return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule(),
|
||||
splitContainer.getMinDimensionsPair());
|
||||
}
|
||||
|
||||
static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule,
|
||||
@Nullable Pair<Size, Size> minDimensionsPair) {
|
||||
// TODO(b/190433398): Supply correct insets.
|
||||
final WindowMetrics parentMetrics = new WindowMetrics(parentBounds,
|
||||
new WindowInsets(new Rect()));
|
||||
return rule.checkParentMetrics(parentMetrics);
|
||||
// Don't show side by side if bounds is not qualified.
|
||||
if (!rule.checkParentMetrics(parentMetrics)) {
|
||||
return false;
|
||||
}
|
||||
final float splitRatio = rule.getSplitRatio();
|
||||
// We only care the size of the bounds regardless of its position.
|
||||
final Rect primaryBounds = getPrimaryBounds(parentBounds, splitRatio, true /* isLtr */);
|
||||
final Rect secondaryBounds = getSecondaryBounds(parentBounds, splitRatio, true /* isLtr */);
|
||||
|
||||
if (minDimensionsPair == null) {
|
||||
return true;
|
||||
}
|
||||
return !boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first)
|
||||
&& !boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds,
|
||||
@NonNull SplitRule rule, boolean isLtr) {
|
||||
if (!shouldShowSideBySide(parentBounds, rule)) {
|
||||
return new Rect();
|
||||
}
|
||||
|
||||
final float splitRatio = rule.getSplitRatio();
|
||||
final float rtlSplitRatio = 1 - splitRatio;
|
||||
switch (position) {
|
||||
case POSITION_START:
|
||||
return isLtr ? getLeftContainerBounds(parentBounds, splitRatio)
|
||||
: getRightContainerBounds(parentBounds, rtlSplitRatio);
|
||||
case POSITION_END:
|
||||
return isLtr ? getRightContainerBounds(parentBounds, splitRatio)
|
||||
: getLeftContainerBounds(parentBounds, rtlSplitRatio);
|
||||
case POSITION_FILL:
|
||||
return parentBounds;
|
||||
}
|
||||
return parentBounds;
|
||||
static Pair<Size, Size> getActivitiesMinDimensionsPair(Activity primaryActivity,
|
||||
Activity secondaryActivity) {
|
||||
return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity));
|
||||
}
|
||||
|
||||
private Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
|
||||
@NonNull
|
||||
static Pair<Size, Size> getActivityIntentMinDimensionsPair(Activity primaryActivity,
|
||||
Intent secondaryIntent) {
|
||||
return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static Size getMinDimensions(@Nullable Activity activity) {
|
||||
if (activity == null) {
|
||||
return null;
|
||||
}
|
||||
final ActivityInfo.WindowLayout windowLayout = activity.getActivityInfo().windowLayout;
|
||||
if (windowLayout == null) {
|
||||
return null;
|
||||
}
|
||||
return new Size(windowLayout.minWidth, windowLayout.minHeight);
|
||||
}
|
||||
|
||||
// TODO(b/232871351): find a light-weight approach for this check.
|
||||
@Nullable
|
||||
static Size getMinDimensions(@Nullable Intent intent) {
|
||||
if (intent == null) {
|
||||
return null;
|
||||
}
|
||||
final PackageManager packageManager = ActivityThread.currentActivityThread()
|
||||
.getApplication().getPackageManager();
|
||||
final ResolveInfo resolveInfo = packageManager.resolveActivity(intent,
|
||||
PackageManager.ResolveInfoFlags.of(MATCH_ALL));
|
||||
if (resolveInfo == null) {
|
||||
return null;
|
||||
}
|
||||
final ActivityInfo activityInfo = resolveInfo.activityInfo;
|
||||
if (activityInfo == null) {
|
||||
return null;
|
||||
}
|
||||
final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout;
|
||||
if (windowLayout == null) {
|
||||
return null;
|
||||
}
|
||||
return new Size(windowLayout.minWidth, windowLayout.minHeight);
|
||||
}
|
||||
|
||||
static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds,
|
||||
@Nullable Size minDimensions) {
|
||||
if (minDimensions == null) {
|
||||
return false;
|
||||
}
|
||||
return bounds.width() < minDimensions.getWidth()
|
||||
|| bounds.height() < minDimensions.getHeight();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@NonNull
|
||||
static Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds,
|
||||
@NonNull SplitRule rule, @NonNull Activity primaryActivity,
|
||||
@Nullable Pair<Size, Size> minDimensionsPair) {
|
||||
if (!shouldShowSideBySide(parentBounds, rule, minDimensionsPair)) {
|
||||
return new Rect();
|
||||
}
|
||||
final boolean isLtr = isLtr(primaryActivity, rule);
|
||||
final float splitRatio = rule.getSplitRatio();
|
||||
|
||||
switch (position) {
|
||||
case POSITION_START:
|
||||
return getPrimaryBounds(parentBounds, splitRatio, isLtr);
|
||||
case POSITION_END:
|
||||
return getSecondaryBounds(parentBounds, splitRatio, isLtr);
|
||||
case POSITION_FILL:
|
||||
default:
|
||||
return new Rect();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Rect getPrimaryBounds(@NonNull Rect parentBounds, float splitRatio,
|
||||
boolean isLtr) {
|
||||
return isLtr ? getLeftContainerBounds(parentBounds, splitRatio)
|
||||
: getRightContainerBounds(parentBounds, 1 - splitRatio);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Rect getSecondaryBounds(@NonNull Rect parentBounds, float splitRatio,
|
||||
boolean isLtr) {
|
||||
return isLtr ? getRightContainerBounds(parentBounds, splitRatio)
|
||||
: getLeftContainerBounds(parentBounds, 1 - splitRatio);
|
||||
}
|
||||
|
||||
private static Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
|
||||
return new Rect(
|
||||
parentBounds.left,
|
||||
parentBounds.top,
|
||||
@@ -415,7 +557,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
parentBounds.bottom);
|
||||
}
|
||||
|
||||
private Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
|
||||
private static Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
|
||||
return new Rect(
|
||||
(int) (parentBounds.left + parentBounds.width() * splitRatio),
|
||||
parentBounds.top,
|
||||
@@ -427,7 +569,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
* Checks if a split with the provided rule should be displays in left-to-right layout
|
||||
* direction, either always or with the current configuration.
|
||||
*/
|
||||
private boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) {
|
||||
private static boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) {
|
||||
switch (rule.getLayoutDirection()) {
|
||||
case LayoutDirection.LOCALE:
|
||||
return context.getResources().getConfiguration().getLayoutDirection()
|
||||
@@ -441,7 +583,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) {
|
||||
static Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) {
|
||||
return container.getTaskContainer().getTaskBounds();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.content.Intent;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.util.Size;
|
||||
import android.window.TaskFragmentInfo;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
@@ -414,6 +415,11 @@ class TaskFragmentContainer {
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
Rect getLastRequestedBounds() {
|
||||
return mLastRequestedBounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if last requested windowing mode is equal to the provided value.
|
||||
*/
|
||||
@@ -439,6 +445,31 @@ class TaskFragmentContainer {
|
||||
return mTaskContainer;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Size getMinDimensions() {
|
||||
if (mInfo == null) {
|
||||
return null;
|
||||
}
|
||||
int maxMinWidth = mInfo.getMinimumWidth();
|
||||
int maxMinHeight = mInfo.getMinimumHeight();
|
||||
for (Activity activity : mPendingAppearedActivities) {
|
||||
final Size minDimensions = SplitPresenter.getMinDimensions(activity);
|
||||
if (minDimensions == null) {
|
||||
continue;
|
||||
}
|
||||
maxMinWidth = Math.max(maxMinWidth, minDimensions.getWidth());
|
||||
maxMinHeight = Math.max(maxMinHeight, minDimensions.getHeight());
|
||||
}
|
||||
if (mPendingAppearedIntent != null) {
|
||||
final Size minDimensions = SplitPresenter.getMinDimensions(mPendingAppearedIntent);
|
||||
if (minDimensions != null) {
|
||||
maxMinWidth = Math.max(maxMinWidth, minDimensions.getWidth());
|
||||
maxMinHeight = Math.max(maxMinHeight, minDimensions.getHeight());
|
||||
}
|
||||
}
|
||||
return new Size(maxMinWidth, maxMinHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(true /* includeContainersToFinishOnExit */);
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
<application android:debuggable="true" android:largeHeap="true">
|
||||
<uses-library android:name="android.test.mock" />
|
||||
<uses-library android:name="android.test.runner" />
|
||||
|
||||
<activity android:name="androidx.window.extensions.embedding.MinimumDimensionActivity">
|
||||
<layout android:minWidth="600px"
|
||||
android:minHeight="1200px"/>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 androidx.window.extensions.embedding;
|
||||
|
||||
import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS;
|
||||
import static androidx.window.extensions.embedding.SplitRule.FINISH_NEVER;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.util.Pair;
|
||||
import android.window.TaskFragmentInfo;
|
||||
import android.window.WindowContainerToken;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public class EmbeddingTestUtils {
|
||||
static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200);
|
||||
static final int TASK_ID = 10;
|
||||
static final float SPLIT_RATIO = 0.5f;
|
||||
/** Default finish behavior in Jetpack. */
|
||||
static final int DEFAULT_FINISH_PRIMARY_WITH_SECONDARY = FINISH_NEVER;
|
||||
static final int DEFAULT_FINISH_SECONDARY_WITH_PRIMARY = FINISH_ALWAYS;
|
||||
|
||||
private EmbeddingTestUtils() {}
|
||||
|
||||
/** Gets the bounds of a TaskFragment that is in split. */
|
||||
static Rect getSplitBounds(boolean isPrimary) {
|
||||
final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO);
|
||||
return isPrimary
|
||||
? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width,
|
||||
TASK_BOUNDS.bottom)
|
||||
: new Rect(
|
||||
TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right,
|
||||
TASK_BOUNDS.bottom);
|
||||
}
|
||||
|
||||
/** Creates a rule to always split the given activity and the given intent. */
|
||||
static SplitRule createSplitRule(@NonNull Activity primaryActivity,
|
||||
@NonNull Intent secondaryIntent) {
|
||||
final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent);
|
||||
return new SplitPairRule.Builder(
|
||||
activityPair -> false,
|
||||
targetPair::equals,
|
||||
w -> true)
|
||||
.setSplitRatio(SPLIT_RATIO)
|
||||
.setShouldClearTop(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Creates a rule to always split the given activities. */
|
||||
static SplitRule createSplitRule(@NonNull Activity primaryActivity,
|
||||
@NonNull Activity secondaryActivity) {
|
||||
return createSplitRule(primaryActivity, secondaryActivity,
|
||||
DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY,
|
||||
true /* clearTop */);
|
||||
}
|
||||
|
||||
/** Creates a rule to always split the given activities with the given finish behaviors. */
|
||||
static SplitRule createSplitRule(@NonNull Activity primaryActivity,
|
||||
@NonNull Activity secondaryActivity, int finishPrimaryWithSecondary,
|
||||
int finishSecondaryWithPrimary, boolean clearTop) {
|
||||
final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity);
|
||||
return new SplitPairRule.Builder(
|
||||
targetPair::equals,
|
||||
activityIntentPair -> false,
|
||||
w -> true)
|
||||
.setSplitRatio(SPLIT_RATIO)
|
||||
.setFinishPrimaryWithSecondary(finishPrimaryWithSecondary)
|
||||
.setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
|
||||
.setShouldClearTop(clearTop)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Creates a mock TaskFragmentInfo for the given TaskFragment. */
|
||||
static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container,
|
||||
@NonNull Activity activity) {
|
||||
return new TaskFragmentInfo(container.getTaskFragmentToken(),
|
||||
mock(WindowContainerToken.class),
|
||||
new Configuration(),
|
||||
1,
|
||||
true /* isVisible */,
|
||||
Collections.singletonList(activity.getActivityToken()),
|
||||
new Point(),
|
||||
false /* isTaskClearedForReuse */,
|
||||
false /* isTaskFragmentClearedForPip */,
|
||||
new Point());
|
||||
}
|
||||
|
||||
static ActivityInfo createActivityInfoWithMinDimensions() {
|
||||
ActivityInfo aInfo = new ActivityInfo();
|
||||
final Rect primaryBounds = getSplitBounds(true /* isPrimary */);
|
||||
aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0,
|
||||
primaryBounds.width() + 1, primaryBounds.height() + 1);
|
||||
return aInfo;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ package androidx.window.extensions.embedding;
|
||||
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
|
||||
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
|
||||
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
|
||||
@@ -58,8 +60,6 @@ import java.util.ArrayList;
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class JetpackTaskFragmentOrganizerTest {
|
||||
private static final int TASK_ID = 10;
|
||||
|
||||
@Mock
|
||||
private WindowContainerTransaction mTransaction;
|
||||
@Mock
|
||||
@@ -131,6 +131,7 @@ public class JetpackTaskFragmentOrganizerTest {
|
||||
return new TaskFragmentInfo(container.getTaskFragmentToken(),
|
||||
mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */,
|
||||
false /* isVisible */, new ArrayList<>(), new Point(),
|
||||
false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */);
|
||||
false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */,
|
||||
new Point());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 androidx.window.extensions.embedding;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
/**
|
||||
* Activity that declares minWidth and minHeight in
|
||||
* {@link android.content.pm.ActivityInfo.WindowLayout}
|
||||
*/
|
||||
public class MinimumDimensionActivity extends Activity {}
|
||||
@@ -19,8 +19,14 @@ package androidx.window.extensions.embedding;
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
|
||||
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
|
||||
import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS;
|
||||
import static androidx.window.extensions.embedding.SplitRule.FINISH_NEVER;
|
||||
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
|
||||
@@ -49,20 +55,19 @@ import android.app.Activity;
|
||||
import android.app.ActivityOptions;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
import android.util.Pair;
|
||||
import android.window.TaskFragmentInfo;
|
||||
import android.window.WindowContainerToken;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
@@ -86,16 +91,9 @@ import java.util.List;
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SplitControllerTest {
|
||||
private static final int TASK_ID = 10;
|
||||
private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200);
|
||||
private static final float SPLIT_RATIO = 0.5f;
|
||||
private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent(
|
||||
new ComponentName("test", "placeholder"));
|
||||
|
||||
/** Default finish behavior in Jetpack. */
|
||||
private static final int DEFAULT_FINISH_PRIMARY_WITH_SECONDARY = FINISH_NEVER;
|
||||
private static final int DEFAULT_FINISH_SECONDARY_WITH_PRIMARY = FINISH_ALWAYS;
|
||||
|
||||
private Activity mActivity;
|
||||
@Mock
|
||||
private Resources mActivityResources;
|
||||
@@ -419,6 +417,25 @@ public class SplitControllerTest {
|
||||
assertSplitPair(primaryContainer, container);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveStartActivityIntent_shouldLaunchInFullscreen() {
|
||||
final Intent intent = new Intent().setComponent(
|
||||
new ComponentName(ApplicationProvider.getApplicationContext(),
|
||||
MinimumDimensionActivity.class));
|
||||
setupSplitRule(mActivity, intent);
|
||||
final Activity primaryActivity = createMockActivity();
|
||||
addSplitTaskFragments(primaryActivity, mActivity);
|
||||
|
||||
final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
|
||||
mTransaction, TASK_ID, intent, null /* launchingActivity */);
|
||||
final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
|
||||
mActivity);
|
||||
|
||||
assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container));
|
||||
assertTrue(primaryContainer.areLastRequestedBoundsEqual(null));
|
||||
assertTrue(container.areLastRequestedBoundsEqual(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlaceActivityInTopContainer() {
|
||||
mSplitController.placeActivityInTopContainer(mActivity);
|
||||
@@ -766,6 +783,48 @@ public class SplitControllerTest {
|
||||
assertSplitPair(mActivity, primaryActivity);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveActivityToContainer_primaryActivityMinDimensionsNotSatisfied() {
|
||||
final Activity activityBelow = createMockActivity();
|
||||
setupSplitRule(mActivity, activityBelow);
|
||||
|
||||
doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo();
|
||||
|
||||
final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
|
||||
TASK_ID);
|
||||
container.addPendingAppearedActivity(mActivity);
|
||||
|
||||
// Allow to split as primary.
|
||||
boolean result = mSplitController.resolveActivityToContainer(mActivity,
|
||||
true /* isOnReparent */);
|
||||
|
||||
assertTrue(result);
|
||||
assertSplitPair(mActivity, activityBelow, true /* matchParentBounds */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveActivityToContainer_secondaryActivityMinDimensionsNotSatisfied() {
|
||||
final Activity activityBelow = createMockActivity();
|
||||
setupSplitRule(activityBelow, mActivity);
|
||||
|
||||
ActivityInfo aInfo = new ActivityInfo();
|
||||
final Rect secondaryBounds = getSplitBounds(false /* isPrimary */);
|
||||
aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0,
|
||||
secondaryBounds.width() + 1, secondaryBounds.height() + 1);
|
||||
doReturn(aInfo).when(mActivity).getActivityInfo();
|
||||
|
||||
final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
|
||||
TASK_ID);
|
||||
container.addPendingAppearedActivity(mActivity);
|
||||
|
||||
// Allow to split as primary.
|
||||
boolean result = mSplitController.resolveActivityToContainer(mActivity,
|
||||
false /* isOnReparent */);
|
||||
|
||||
assertTrue(result);
|
||||
assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveActivityToContainer_inUnknownTaskFragment() {
|
||||
doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity);
|
||||
@@ -835,23 +894,10 @@ public class SplitControllerTest {
|
||||
doReturn(activityToken).when(activity).getActivityToken();
|
||||
doReturn(activity).when(mSplitController).getActivity(activityToken);
|
||||
doReturn(TASK_ID).when(activity).getTaskId();
|
||||
doReturn(new ActivityInfo()).when(activity).getActivityInfo();
|
||||
return activity;
|
||||
}
|
||||
|
||||
/** Creates a mock TaskFragmentInfo for the given TaskFragment. */
|
||||
private TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container,
|
||||
@NonNull Activity activity) {
|
||||
return new TaskFragmentInfo(container.getTaskFragmentToken(),
|
||||
mock(WindowContainerToken.class),
|
||||
new Configuration(),
|
||||
1,
|
||||
true /* isVisible */,
|
||||
Collections.singletonList(activity.getActivityToken()),
|
||||
new Point(),
|
||||
false /* isTaskClearedForReuse */,
|
||||
false /* isTaskFragmentClearedForPip */);
|
||||
}
|
||||
|
||||
/** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
|
||||
private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) {
|
||||
final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID);
|
||||
@@ -902,49 +948,10 @@ public class SplitControllerTest {
|
||||
/** Setups a rule to always split the given activities. */
|
||||
private void setupSplitRule(@NonNull Activity primaryActivity,
|
||||
@NonNull Activity secondaryActivity) {
|
||||
final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity,
|
||||
DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY,
|
||||
true /* clearTop */);
|
||||
final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity);
|
||||
mSplitController.setEmbeddingRules(Collections.singleton(splitRule));
|
||||
}
|
||||
|
||||
/** Creates a rule to always split the given activity and the given intent. */
|
||||
private SplitRule createSplitRule(@NonNull Activity primaryActivity,
|
||||
@NonNull Intent secondaryIntent) {
|
||||
final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent);
|
||||
return new SplitPairRule.Builder(
|
||||
activityPair -> false,
|
||||
targetPair::equals,
|
||||
w -> true)
|
||||
.setSplitRatio(SPLIT_RATIO)
|
||||
.setShouldClearTop(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Creates a rule to always split the given activities. */
|
||||
private SplitRule createSplitRule(@NonNull Activity primaryActivity,
|
||||
@NonNull Activity secondaryActivity) {
|
||||
return createSplitRule(primaryActivity, secondaryActivity,
|
||||
DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY,
|
||||
true /* clearTop */);
|
||||
}
|
||||
|
||||
/** Creates a rule to always split the given activities with the given finish behaviors. */
|
||||
private SplitRule createSplitRule(@NonNull Activity primaryActivity,
|
||||
@NonNull Activity secondaryActivity, int finishPrimaryWithSecondary,
|
||||
int finishSecondaryWithPrimary, boolean clearTop) {
|
||||
final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity);
|
||||
return new SplitPairRule.Builder(
|
||||
targetPair::equals,
|
||||
activityIntentPair -> false,
|
||||
w -> true)
|
||||
.setSplitRatio(SPLIT_RATIO)
|
||||
.setFinishPrimaryWithSecondary(finishPrimaryWithSecondary)
|
||||
.setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
|
||||
.setShouldClearTop(clearTop)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Adds a pair of TaskFragments as split for the given activities. */
|
||||
private void addSplitTaskFragments(@NonNull Activity primaryActivity,
|
||||
@NonNull Activity secondaryActivity) {
|
||||
@@ -973,39 +980,42 @@ public class SplitControllerTest {
|
||||
secondaryContainer.setLastRequestedBounds(getSplitBounds(false /* isPrimary */));
|
||||
}
|
||||
|
||||
/** Gets the bounds of a TaskFragment that is in split. */
|
||||
private Rect getSplitBounds(boolean isPrimary) {
|
||||
final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO);
|
||||
return isPrimary
|
||||
? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width,
|
||||
TASK_BOUNDS.bottom)
|
||||
: new Rect(TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right,
|
||||
TASK_BOUNDS.bottom);
|
||||
/** Asserts that the two given activities are in split. */
|
||||
private void assertSplitPair(@NonNull Activity primaryActivity,
|
||||
@NonNull Activity secondaryActivity) {
|
||||
assertSplitPair(primaryActivity, secondaryActivity, false /* matchParentBounds */);
|
||||
}
|
||||
|
||||
/** Asserts that the two given activities are in split. */
|
||||
private void assertSplitPair(@NonNull Activity primaryActivity,
|
||||
@NonNull Activity secondaryActivity) {
|
||||
@NonNull Activity secondaryActivity, boolean matchParentBounds) {
|
||||
assertSplitPair(mSplitController.getContainerWithActivity(primaryActivity),
|
||||
mSplitController.getContainerWithActivity(secondaryActivity));
|
||||
mSplitController.getContainerWithActivity(secondaryActivity), matchParentBounds);
|
||||
}
|
||||
|
||||
private void assertSplitPair(@NonNull TaskFragmentContainer primaryContainer,
|
||||
@NonNull TaskFragmentContainer secondaryContainer) {
|
||||
assertSplitPair(primaryContainer, secondaryContainer, false /* matchParentBounds*/);
|
||||
}
|
||||
|
||||
/** Asserts that the two given TaskFragments are in split. */
|
||||
private void assertSplitPair(@NonNull TaskFragmentContainer primaryContainer,
|
||||
@NonNull TaskFragmentContainer secondaryContainer) {
|
||||
@NonNull TaskFragmentContainer secondaryContainer, boolean matchParentBounds) {
|
||||
assertNotNull(primaryContainer);
|
||||
assertNotNull(secondaryContainer);
|
||||
assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer,
|
||||
secondaryContainer));
|
||||
if (primaryContainer.mInfo != null) {
|
||||
assertTrue(primaryContainer.areLastRequestedBoundsEqual(
|
||||
getSplitBounds(true /* isPrimary */)));
|
||||
final Rect primaryBounds = matchParentBounds ? new Rect()
|
||||
: getSplitBounds(true /* isPrimary */);
|
||||
assertTrue(primaryContainer.areLastRequestedBoundsEqual(primaryBounds));
|
||||
assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(
|
||||
WINDOWING_MODE_MULTI_WINDOW));
|
||||
}
|
||||
if (secondaryContainer.mInfo != null) {
|
||||
assertTrue(secondaryContainer.areLastRequestedBoundsEqual(
|
||||
getSplitBounds(false /* isPrimary */)));
|
||||
final Rect secondaryBounds = matchParentBounds ? new Rect()
|
||||
: getSplitBounds(false /* isPrimary */);
|
||||
assertTrue(secondaryContainer.areLastRequestedBoundsEqual(secondaryBounds));
|
||||
assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(
|
||||
WINDOWING_MODE_MULTI_WINDOW));
|
||||
}
|
||||
|
||||
@@ -18,25 +18,46 @@ package androidx.window.extensions.embedding;
|
||||
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
|
||||
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
|
||||
import static androidx.window.extensions.embedding.SplitPresenter.POSITION_END;
|
||||
import static androidx.window.extensions.embedding.SplitPresenter.POSITION_FILL;
|
||||
import static androidx.window.extensions.embedding.SplitPresenter.POSITION_START;
|
||||
import static androidx.window.extensions.embedding.SplitPresenter.getBoundsForPosition;
|
||||
import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
|
||||
import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide;
|
||||
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
import android.util.Pair;
|
||||
import android.util.Size;
|
||||
import android.window.TaskFragmentInfo;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
@@ -56,8 +77,6 @@ import org.mockito.MockitoAnnotations;
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SplitPresenterTest {
|
||||
private static final int TASK_ID = 10;
|
||||
private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200);
|
||||
|
||||
@Mock
|
||||
private Activity mActivity;
|
||||
@@ -77,11 +96,7 @@ public class SplitPresenterTest {
|
||||
mPresenter = mController.mPresenter;
|
||||
spyOn(mController);
|
||||
spyOn(mPresenter);
|
||||
final Configuration activityConfig = new Configuration();
|
||||
activityConfig.windowConfiguration.setBounds(TASK_BOUNDS);
|
||||
activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS);
|
||||
doReturn(mActivityResources).when(mActivity).getResources();
|
||||
doReturn(activityConfig).when(mActivityResources).getConfiguration();
|
||||
mActivity = createMockActivity();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -129,4 +144,108 @@ public class SplitPresenterTest {
|
||||
verify(mTransaction, never()).setWindowingMode(any(), anyInt());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMinDimensionsForIntent() {
|
||||
final Intent intent = new Intent(ApplicationProvider.getApplicationContext(),
|
||||
MinimumDimensionActivity.class);
|
||||
assertEquals(new Size(600, 1200), getMinDimensions(intent));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldShowSideBySide() {
|
||||
Activity secondaryActivity = createMockActivity();
|
||||
final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity);
|
||||
|
||||
assertTrue(shouldShowSideBySide(TASK_BOUNDS, splitRule));
|
||||
|
||||
// Set minDimensions of primary container to larger than primary bounds.
|
||||
final Rect primaryBounds = getSplitBounds(true /* isPrimary */);
|
||||
Pair<Size, Size> minDimensionsPair = new Pair<>(
|
||||
new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null);
|
||||
|
||||
assertFalse(shouldShowSideBySide(TASK_BOUNDS, splitRule, minDimensionsPair));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBoundsForPosition() {
|
||||
Activity secondaryActivity = createMockActivity();
|
||||
final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity);
|
||||
final Rect primaryBounds = getSplitBounds(true /* isPrimary */);
|
||||
final Rect secondaryBounds = getSplitBounds(false /* isPrimary */);
|
||||
|
||||
assertEquals("Primary bounds must be reported.",
|
||||
primaryBounds,
|
||||
getBoundsForPosition(POSITION_START, TASK_BOUNDS, splitRule,
|
||||
mActivity, null /* miniDimensionsPair */));
|
||||
|
||||
assertEquals("Secondary bounds must be reported.",
|
||||
secondaryBounds,
|
||||
getBoundsForPosition(POSITION_END, TASK_BOUNDS, splitRule,
|
||||
mActivity, null /* miniDimensionsPair */));
|
||||
assertEquals("Task bounds must be reported.",
|
||||
new Rect(),
|
||||
getBoundsForPosition(POSITION_FILL, TASK_BOUNDS, splitRule,
|
||||
mActivity, null /* miniDimensionsPair */));
|
||||
|
||||
Pair<Size, Size> minDimensionsPair = new Pair<>(
|
||||
new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null);
|
||||
|
||||
assertEquals("Fullscreen bounds must be reported because of min dimensions.",
|
||||
new Rect(),
|
||||
getBoundsForPosition(POSITION_START, TASK_BOUNDS,
|
||||
splitRule, mActivity, minDimensionsPair));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpandSplitContainerIfNeeded() {
|
||||
SplitContainer splitContainer = mock(SplitContainer.class);
|
||||
Activity secondaryActivity = createMockActivity();
|
||||
SplitRule splitRule = createSplitRule(mActivity, secondaryActivity);
|
||||
TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID);
|
||||
TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID);
|
||||
doReturn(splitRule).when(splitContainer).getSplitRule();
|
||||
doReturn(primaryTf).when(splitContainer).getPrimaryContainer();
|
||||
doReturn(secondaryTf).when(splitContainer).getSecondaryContainer();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity,
|
||||
null /* secondaryActivity */, null /* secondaryIntent */));
|
||||
|
||||
mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity,
|
||||
secondaryActivity, null /* secondaryIntent */);
|
||||
|
||||
verify(mPresenter, never()).expandTaskFragment(any(), any());
|
||||
|
||||
doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo();
|
||||
|
||||
mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity,
|
||||
secondaryActivity, null /* secondaryIntent */);
|
||||
|
||||
verify(mPresenter).expandTaskFragment(eq(mTransaction),
|
||||
eq(primaryTf.getTaskFragmentToken()));
|
||||
verify(mPresenter).expandTaskFragment(eq(mTransaction),
|
||||
eq(secondaryTf.getTaskFragmentToken()));
|
||||
|
||||
clearInvocations(mPresenter);
|
||||
mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity,
|
||||
null /* secondaryActivity */, new Intent(ApplicationProvider
|
||||
.getApplicationContext(), MinimumDimensionActivity.class));
|
||||
|
||||
verify(mPresenter).expandTaskFragment(eq(mTransaction),
|
||||
eq(primaryTf.getTaskFragmentToken()));
|
||||
verify(mPresenter).expandTaskFragment(eq(mTransaction),
|
||||
eq(secondaryTf.getTaskFragmentToken()));
|
||||
}
|
||||
|
||||
private Activity createMockActivity() {
|
||||
final Activity activity = mock(Activity.class);
|
||||
final Configuration activityConfig = new Configuration();
|
||||
activityConfig.windowConfiguration.setBounds(TASK_BOUNDS);
|
||||
activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS);
|
||||
doReturn(mActivityResources).when(activity).getResources();
|
||||
doReturn(activityConfig).when(mActivityResources).getConfiguration();
|
||||
doReturn(new ActivityInfo()).when(activity).getActivityInfo();
|
||||
return activity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
|
||||
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
@@ -53,9 +56,6 @@ import org.mockito.MockitoAnnotations;
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TaskContainerTest {
|
||||
private static final int TASK_ID = 10;
|
||||
private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200);
|
||||
|
||||
@Mock
|
||||
private SplitController mController;
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package androidx.window.extensions.embedding;
|
||||
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
|
||||
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
@@ -43,8 +45,6 @@ import org.mockito.MockitoAnnotations;
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TaskFragmentAnimationControllerTest {
|
||||
private static final int TASK_ID = 10;
|
||||
|
||||
@Mock
|
||||
private TaskFragmentOrganizer mOrganizer;
|
||||
private TaskFragmentAnimationController mAnimationController;
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package androidx.window.extensions.embedding;
|
||||
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
|
||||
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
|
||||
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -30,17 +33,13 @@ import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
import android.os.Binder;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
import android.window.TaskFragmentInfo;
|
||||
import android.window.WindowContainerToken;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
@@ -55,7 +54,6 @@ import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -68,8 +66,6 @@ import java.util.List;
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TaskFragmentContainerTest {
|
||||
private static final int TASK_ID = 10;
|
||||
|
||||
@Mock
|
||||
private SplitPresenter mPresenter;
|
||||
@Mock
|
||||
@@ -311,18 +307,4 @@ public class TaskFragmentContainerTest {
|
||||
doReturn(activity).when(mController).getActivity(activityToken);
|
||||
return activity;
|
||||
}
|
||||
|
||||
/** Creates a mock TaskFragmentInfo for the given TaskFragment. */
|
||||
private TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container,
|
||||
@NonNull Activity activity) {
|
||||
return new TaskFragmentInfo(container.getTaskFragmentToken(),
|
||||
mock(WindowContainerToken.class),
|
||||
new Configuration(),
|
||||
1,
|
||||
true /* isVisible */,
|
||||
Collections.singletonList(activity.getActivityToken()),
|
||||
new Point(),
|
||||
false /* isTaskClearedForReuse */,
|
||||
false /* isTaskFragmentClearedForPip */);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
|
||||
private static final int DESTROY_TIMEOUT = 10 * 1000;
|
||||
|
||||
final ActivityTaskManagerService mAtmService;
|
||||
@NonNull
|
||||
final ActivityInfo info; // activity info provided by developer in AndroidManifest
|
||||
// Which user is this running for?
|
||||
final int mUserId;
|
||||
@@ -9707,6 +9708,15 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Point getMinDimensions() {
|
||||
final ActivityInfo.WindowLayout windowLayout = info.windowLayout;
|
||||
if (windowLayout == null) {
|
||||
return null;
|
||||
}
|
||||
return new Point(windowLayout.minWidth, windowLayout.minHeight);
|
||||
}
|
||||
|
||||
static class Builder {
|
||||
private final ActivityTaskManagerService mAtmService;
|
||||
private WindowProcessController mCallerApp;
|
||||
|
||||
@@ -508,7 +508,8 @@ public class ActivityStartController {
|
||||
*/
|
||||
int startActivityInTaskFragment(@NonNull TaskFragment taskFragment,
|
||||
@NonNull Intent activityIntent, @Nullable Bundle activityOptions,
|
||||
@Nullable IBinder resultTo, int callingUid, int callingPid) {
|
||||
@Nullable IBinder resultTo, int callingUid, int callingPid,
|
||||
@Nullable IBinder errorCallbackToken) {
|
||||
final ActivityRecord caller =
|
||||
resultTo != null ? ActivityRecord.forTokenLocked(resultTo) : null;
|
||||
return obtainStarter(activityIntent, "startActivityInTaskFragment")
|
||||
@@ -521,6 +522,7 @@ public class ActivityStartController {
|
||||
.setRealCallingUid(callingUid)
|
||||
.setRealCallingPid(callingPid)
|
||||
.setUserId(caller != null ? caller.mUserId : mService.getCurrentUserId())
|
||||
.setErrorCallbackToken(errorCallbackToken)
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@ import android.app.PendingIntent;
|
||||
import android.app.ProfilerInfo;
|
||||
import android.app.WaitResult;
|
||||
import android.app.WindowConfiguration;
|
||||
import android.app.compat.CompatChanges;
|
||||
import android.compat.annotation.ChangeId;
|
||||
import android.compat.annotation.EnabledSince;
|
||||
import android.content.ComponentName;
|
||||
@@ -369,6 +368,13 @@ class ActivityStarter {
|
||||
int filterCallingUid;
|
||||
PendingIntentRecord originatingPendingIntent;
|
||||
boolean allowBackgroundActivityStart;
|
||||
/**
|
||||
* The error callback token passed in {@link android.window.WindowContainerTransaction}
|
||||
* for TaskFragment operation error handling via
|
||||
* {@link android.window.TaskFragmentOrganizer#onTaskFragmentError(IBinder, Throwable)}.
|
||||
*/
|
||||
@Nullable
|
||||
IBinder errorCallbackToken;
|
||||
|
||||
/**
|
||||
* If set to {@code true}, allows this activity start to look into
|
||||
@@ -422,6 +428,7 @@ class ActivityStarter {
|
||||
filterCallingUid = UserHandle.USER_NULL;
|
||||
originatingPendingIntent = null;
|
||||
allowBackgroundActivityStart = false;
|
||||
errorCallbackToken = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -464,6 +471,7 @@ class ActivityStarter {
|
||||
filterCallingUid = request.filterCallingUid;
|
||||
originatingPendingIntent = request.originatingPendingIntent;
|
||||
allowBackgroundActivityStart = request.allowBackgroundActivityStart;
|
||||
errorCallbackToken = request.errorCallbackToken;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2889,6 +2897,7 @@ class ActivityStarter {
|
||||
private void addOrReparentStartingActivity(@NonNull Task task, String reason) {
|
||||
TaskFragment newParent = task;
|
||||
if (mInTaskFragment != null) {
|
||||
// TODO(b/234351413): remove remaining embedded Task logic.
|
||||
// mInTaskFragment is created and added to the leaf task by task fragment organizer's
|
||||
// request. If the task was resolved and different than mInTaskFragment, reparent the
|
||||
// task to mInTaskFragment for embedding.
|
||||
@@ -2915,7 +2924,14 @@ class ActivityStarter {
|
||||
newParent = candidateTf;
|
||||
}
|
||||
}
|
||||
|
||||
// Start Activity to the Task if mStartActivity's min dimensions are not satisfied.
|
||||
if (newParent.isEmbedded() && newParent.smallerThanMinDimension(mStartActivity)) {
|
||||
reason += " - MinimumDimensionViolation";
|
||||
mService.mWindowOrganizerController.sendMinimumDimensionViolation(
|
||||
newParent, mStartActivity.getMinDimensions(), mRequest.errorCallbackToken,
|
||||
reason);
|
||||
newParent = task;
|
||||
}
|
||||
if (mStartActivity.getTaskFragment() == null
|
||||
|| mStartActivity.getTaskFragment() == newParent) {
|
||||
newParent.addChild(mStartActivity, POSITION_TOP);
|
||||
@@ -3200,6 +3216,11 @@ class ActivityStarter {
|
||||
return this;
|
||||
}
|
||||
|
||||
ActivityStarter setErrorCallbackToken(@Nullable IBinder errorCallbackToken) {
|
||||
mRequest.errorCallbackToken = errorCallbackToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
void dump(PrintWriter pw, String prefix) {
|
||||
pw.print(prefix);
|
||||
pw.print("mCurrentUser=");
|
||||
|
||||
@@ -152,6 +152,7 @@ class TaskFragment extends WindowContainer<WindowContainer> {
|
||||
final RootWindowContainer mRootWindowContainer;
|
||||
private final TaskFragmentOrganizerController mTaskFragmentOrganizerController;
|
||||
|
||||
// TODO(b/233177466): Move mMinWidth and mMinHeight to Task and remove usages in TaskFragment
|
||||
/**
|
||||
* Minimal width of this task fragment when it's resizeable. {@link #INVALID_MIN_SIZE} means it
|
||||
* should use the default minimal width.
|
||||
@@ -535,6 +536,25 @@ class TaskFragment extends WindowContainer<WindowContainer> {
|
||||
|| isAllowedToEmbedActivityInTrustedMode(a, uid);
|
||||
}
|
||||
|
||||
boolean smallerThanMinDimension(@NonNull ActivityRecord activity) {
|
||||
final Rect taskFragBounds = getBounds();
|
||||
final Task task = getTask();
|
||||
// Don't need to check if the bounds match parent Task bounds because the fallback mechanism
|
||||
// is to reparent the Activity to parent if minimum dimensions are not satisfied.
|
||||
if (task == null || taskFragBounds.equals(task.getBounds())) {
|
||||
return false;
|
||||
}
|
||||
final Point minDimensions = activity.getMinDimensions();
|
||||
if (minDimensions == null) {
|
||||
return false;
|
||||
}
|
||||
final int minWidth = minDimensions.x;
|
||||
final int minHeight = minDimensions.y;
|
||||
final boolean smaller = taskFragBounds.width() < minWidth
|
||||
|| taskFragBounds.height() < minHeight;
|
||||
return smaller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the organized task fragment is allowed to embed activity in untrusted mode.
|
||||
*/
|
||||
@@ -1755,7 +1775,8 @@ class TaskFragment extends WindowContainer<WindowContainer> {
|
||||
mClearedTaskForReuse = false;
|
||||
mClearedTaskFragmentForPip = false;
|
||||
|
||||
boolean isAddingActivity = child.asActivityRecord() != null;
|
||||
final ActivityRecord addingActivity = child.asActivityRecord();
|
||||
final boolean isAddingActivity = addingActivity != null;
|
||||
final Task task = isAddingActivity ? getTask() : null;
|
||||
|
||||
// If this task had any activity before we added this one.
|
||||
@@ -1780,7 +1801,7 @@ class TaskFragment extends WindowContainer<WindowContainer> {
|
||||
mBackScreenshots.put(r.mActivityComponent.flattenToString(), backBuffer);
|
||||
}
|
||||
child.asActivityRecord().inHistory = true;
|
||||
task.onDescendantActivityAdded(taskHadActivity, activityType, child.asActivityRecord());
|
||||
task.onDescendantActivityAdded(taskHadActivity, activityType, addingActivity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2294,7 +2315,7 @@ class TaskFragment extends WindowContainer<WindowContainer> {
|
||||
TaskFragmentInfo getTaskFragmentInfo() {
|
||||
List<IBinder> childActivities = new ArrayList<>();
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
final WindowContainer wc = getChildAt(i);
|
||||
final WindowContainer<?> wc = getChildAt(i);
|
||||
final ActivityRecord ar = wc.asActivityRecord();
|
||||
if (mTaskFragmentOrganizerUid != INVALID_UID && ar != null
|
||||
&& ar.info.processName.equals(mTaskFragmentOrganizerProcessName)
|
||||
@@ -2314,7 +2335,31 @@ class TaskFragment extends WindowContainer<WindowContainer> {
|
||||
childActivities,
|
||||
positionInParent,
|
||||
mClearedTaskForReuse,
|
||||
mClearedTaskFragmentForPip);
|
||||
mClearedTaskFragmentForPip,
|
||||
calculateMinDimension());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the minimum dimensions that this TaskFragment can be resized.
|
||||
* @see TaskFragmentInfo#getMinimumWidth()
|
||||
* @see TaskFragmentInfo#getMinimumHeight()
|
||||
*/
|
||||
Point calculateMinDimension() {
|
||||
final int[] maxMinWidth = new int[1];
|
||||
final int[] maxMinHeight = new int[1];
|
||||
|
||||
forAllActivities(a -> {
|
||||
if (a.finishing) {
|
||||
return;
|
||||
}
|
||||
final Point minDimensions = a.getMinDimensions();
|
||||
if (minDimensions == null) {
|
||||
return;
|
||||
}
|
||||
maxMinWidth[0] = Math.max(maxMinWidth[0], minDimensions.x);
|
||||
maxMinHeight[0] = Math.max(maxMinHeight[0], minDimensions.y);
|
||||
});
|
||||
return new Point(maxMinWidth[0], maxMinHeight[0]);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.android.server.wm;
|
||||
|
||||
import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
|
||||
import static android.app.ActivityManager.isStartResultSuccessful;
|
||||
import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER;
|
||||
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT;
|
||||
@@ -56,6 +57,7 @@ import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
@@ -115,7 +117,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
static final int CONTROLLABLE_CONFIGS = ActivityInfo.CONFIG_WINDOW_CONFIGURATION
|
||||
| ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE | ActivityInfo.CONFIG_SCREEN_SIZE
|
||||
| ActivityInfo.CONFIG_LAYOUT_DIRECTION;
|
||||
static final int CONTROLLABLE_WINDOW_CONFIGS = WindowConfiguration.WINDOW_CONFIG_BOUNDS
|
||||
static final int CONTROLLABLE_WINDOW_CONFIGS = WINDOW_CONFIG_BOUNDS
|
||||
| WindowConfiguration.WINDOW_CONFIG_APP_BOUNDS;
|
||||
|
||||
private final ActivityTaskManagerService mService;
|
||||
@@ -421,7 +423,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
}
|
||||
}
|
||||
|
||||
int containerEffect = applyWindowContainerChange(wc, entry.getValue());
|
||||
int containerEffect = applyWindowContainerChange(wc, entry.getValue(),
|
||||
t.getErrorCallbackToken());
|
||||
effects |= containerEffect;
|
||||
|
||||
// Lifecycle changes will trigger ensureConfig for everything.
|
||||
@@ -503,7 +506,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
}
|
||||
}
|
||||
|
||||
private int applyChanges(WindowContainer container, WindowContainerTransaction.Change change) {
|
||||
private int applyChanges(WindowContainer<?> container,
|
||||
WindowContainerTransaction.Change change, @Nullable IBinder errorCallbackToken) {
|
||||
// The "client"-facing API should prevent bad changes; however, just in case, sanitize
|
||||
// masks here.
|
||||
final int configMask = change.getConfigSetMask() & CONTROLLABLE_CONFIGS;
|
||||
@@ -511,6 +515,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
int effects = 0;
|
||||
final int windowingMode = change.getWindowingMode();
|
||||
if (configMask != 0) {
|
||||
|
||||
adjustBoundsForMinDimensionsIfNeeded(container, change, errorCallbackToken);
|
||||
|
||||
if (windowingMode > -1 && windowingMode != container.getWindowingMode()) {
|
||||
// Special handling for when we are setting a windowingMode in the same transaction.
|
||||
// Setting the windowingMode is going to call onConfigurationChanged so we don't
|
||||
@@ -556,6 +563,26 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
return effects;
|
||||
}
|
||||
|
||||
private void adjustBoundsForMinDimensionsIfNeeded(WindowContainer<?> container,
|
||||
WindowContainerTransaction.Change change, @Nullable IBinder errorCallbackToken) {
|
||||
final TaskFragment taskFragment = container.asTaskFragment();
|
||||
if (taskFragment == null || !taskFragment.isEmbedded()) {
|
||||
return;
|
||||
}
|
||||
if ((change.getWindowSetMask() & WINDOW_CONFIG_BOUNDS) == 0) {
|
||||
return;
|
||||
}
|
||||
final WindowConfiguration winConfig = change.getConfiguration().windowConfiguration;
|
||||
final Rect bounds = winConfig.getBounds();
|
||||
final Point minDimensions = taskFragment.calculateMinDimension();
|
||||
if (bounds.width() < minDimensions.x || bounds.height() < minDimensions.y) {
|
||||
sendMinimumDimensionViolation(taskFragment, minDimensions, errorCallbackToken,
|
||||
"setBounds:" + bounds);
|
||||
// Sets the bounds to match parent bounds.
|
||||
winConfig.setBounds(new Rect());
|
||||
}
|
||||
}
|
||||
|
||||
private int applyTaskChanges(Task tr, WindowContainerTransaction.Change c) {
|
||||
int effects = 0;
|
||||
final SurfaceControl.Transaction t = c.getBoundsChangeTransaction();
|
||||
@@ -696,7 +723,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
final Bundle activityOptions = hop.getLaunchOptions();
|
||||
final int result = mService.getActivityStartController()
|
||||
.startActivityInTaskFragment(tf, activityIntent, activityOptions,
|
||||
hop.getCallingActivity(), caller.mUid, caller.mPid);
|
||||
hop.getCallingActivity(), caller.mUid, caller.mPid,
|
||||
errorCallbackToken);
|
||||
if (!isStartResultSuccessful(result)) {
|
||||
sendTaskFragmentOperationFailure(organizer, errorCallbackToken,
|
||||
convertStartFailureToThrowable(result, activityIntent));
|
||||
@@ -740,6 +768,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
|
||||
break;
|
||||
}
|
||||
if (parent.smallerThanMinDimension(activity)) {
|
||||
sendMinimumDimensionViolation(parent, activity.getMinDimensions(),
|
||||
errorCallbackToken, "reparentActivityToTask");
|
||||
break;
|
||||
}
|
||||
|
||||
activity.reparent(parent, POSITION_TOP);
|
||||
effects |= TRANSACT_EFFECTS_LIFECYCLE;
|
||||
break;
|
||||
@@ -953,6 +987,19 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
return effects;
|
||||
}
|
||||
|
||||
/** A helper method to send minimum dimension violation error to the client. */
|
||||
void sendMinimumDimensionViolation(TaskFragment taskFragment, Point minDimensions,
|
||||
IBinder errorCallbackToken, String reason) {
|
||||
if (taskFragment == null || taskFragment.getTaskFragmentOrganizer() == null) {
|
||||
return;
|
||||
}
|
||||
final Throwable exception = new SecurityException("The task fragment's bounds:"
|
||||
+ taskFragment.getBounds() + " does not satisfy minimum dimensions:"
|
||||
+ minDimensions + " " + reason);
|
||||
sendTaskFragmentOperationFailure(taskFragment.getTaskFragmentOrganizer(),
|
||||
errorCallbackToken, exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post and wait for the result of the activity start to prevent potential deadlock against
|
||||
* {@link WindowManagerGlobalLock}.
|
||||
@@ -1164,14 +1211,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
}
|
||||
|
||||
private int applyWindowContainerChange(WindowContainer wc,
|
||||
WindowContainerTransaction.Change c) {
|
||||
WindowContainerTransaction.Change c, @Nullable IBinder errorCallbackToken) {
|
||||
sanitizeWindowContainer(wc);
|
||||
if (wc.asTaskFragment() != null && wc.asTaskFragment().isEmbeddedTaskFragmentInPip()) {
|
||||
// No override from organizer for embedded TaskFragment in a PIP Task.
|
||||
return 0;
|
||||
}
|
||||
|
||||
int effects = applyChanges(wc, c);
|
||||
int effects = applyChanges(wc, c, errorCallbackToken);
|
||||
|
||||
if (wc instanceof DisplayArea) {
|
||||
effects |= applyDisplayAreaChanges(wc.asDisplayArea(), c);
|
||||
@@ -1515,8 +1562,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
mLaunchTaskFragments.put(creationParams.getFragmentToken(), taskFragment);
|
||||
}
|
||||
|
||||
void reparentTaskFragment(@NonNull TaskFragment oldParent, @Nullable WindowContainer newParent,
|
||||
@Nullable ITaskFragmentOrganizer organizer, @Nullable IBinder errorCallbackToken) {
|
||||
void reparentTaskFragment(@NonNull TaskFragment oldParent,
|
||||
@Nullable WindowContainer<?> newParent, @Nullable ITaskFragmentOrganizer organizer,
|
||||
@Nullable IBinder errorCallbackToken) {
|
||||
final TaskFragment newParentTF;
|
||||
if (newParent == null) {
|
||||
// Use the old parent's parent if the caller doesn't specify the new parent.
|
||||
@@ -1554,6 +1602,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
|
||||
sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
|
||||
return;
|
||||
}
|
||||
final Point minDimensions = oldParent.calculateMinDimension();
|
||||
final Rect newParentBounds = newParentTF.getBounds();
|
||||
if (newParentBounds.width() < minDimensions.x
|
||||
|| newParentBounds.height() < minDimensions.y) {
|
||||
sendMinimumDimensionViolation(newParentTF, minDimensions, errorCallbackToken,
|
||||
"reparentTaskFragment");
|
||||
return;
|
||||
}
|
||||
while (oldParent.hasChild()) {
|
||||
oldParent.getChildAt(0).reparent(newParentTF, POSITION_TOP);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
|
||||
import static com.android.server.wm.WindowContainer.POSITION_TOP;
|
||||
import static com.android.server.wm.testing.Assert.assertThrows;
|
||||
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
@@ -43,6 +45,7 @@ import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Binder;
|
||||
@@ -78,6 +81,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
private static final int TASK_ID = 10;
|
||||
|
||||
private TaskFragmentOrganizerController mController;
|
||||
private WindowOrganizerController mWindowOrganizerController;
|
||||
private TaskFragmentOrganizer mOrganizer;
|
||||
private TaskFragmentOrganizerToken mOrganizerToken;
|
||||
private ITaskFragmentOrganizer mIOrganizer;
|
||||
@@ -87,10 +91,13 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
private WindowContainerTransaction mTransaction;
|
||||
private WindowContainerToken mFragmentWindowToken;
|
||||
private RemoteAnimationDefinition mDefinition;
|
||||
private IBinder mErrorToken;
|
||||
private Rect mTaskFragBounds;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
mController = mAtm.mWindowOrganizerController.mTaskFragmentOrganizerController;
|
||||
mWindowOrganizerController = mAtm.mWindowOrganizerController;
|
||||
mController = mWindowOrganizerController.mTaskFragmentOrganizerController;
|
||||
mOrganizer = new TaskFragmentOrganizer(Runnable::run);
|
||||
mOrganizerToken = mOrganizer.getOrganizerToken();
|
||||
mIOrganizer = ITaskFragmentOrganizer.Stub.asInterface(mOrganizerToken.asBinder());
|
||||
@@ -101,6 +108,10 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
mTransaction = new WindowContainerTransaction();
|
||||
mFragmentWindowToken = mTaskFragment.mRemoteToken.toWindowContainerToken();
|
||||
mDefinition = new RemoteAnimationDefinition();
|
||||
mErrorToken = new Binder();
|
||||
final Rect displayBounds = mDisplayContent.getBounds();
|
||||
mTaskFragBounds = new Rect(displayBounds.left, displayBounds.top, displayBounds.centerX(),
|
||||
displayBounds.centerY());
|
||||
|
||||
spyOn(mController);
|
||||
spyOn(mOrganizer);
|
||||
@@ -221,16 +232,15 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnTaskFragmentError() throws RemoteException {
|
||||
final IBinder errorCallbackToken = new Binder();
|
||||
public void testOnTaskFragmentError() {
|
||||
final Throwable exception = new IllegalArgumentException("Test exception");
|
||||
|
||||
mController.registerOrganizer(mIOrganizer);
|
||||
mController.onTaskFragmentError(mTaskFragment.getTaskFragmentOrganizer(),
|
||||
errorCallbackToken, exception);
|
||||
mErrorToken, exception);
|
||||
mController.dispatchPendingEvents();
|
||||
|
||||
verify(mOrganizer).onTaskFragmentError(eq(errorCallbackToken), eq(exception));
|
||||
verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), eq(exception));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -280,7 +290,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
final int uid = Binder.getCallingUid();
|
||||
mTaskFragment.setTaskFragmentOrganizer(mOrganizer.getOrganizerToken(), uid,
|
||||
DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME);
|
||||
mAtm.mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
|
||||
mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
|
||||
mController.registerOrganizer(mIOrganizer);
|
||||
mOrganizer.applyTransaction(mTransaction);
|
||||
final Task task = createTask(mDisplayContent);
|
||||
@@ -305,7 +315,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
final IBinder temporaryToken = token.getValue();
|
||||
assertNotEquals(activity.token, temporaryToken);
|
||||
mTransaction.reparentActivityToTaskFragment(mFragmentToken, temporaryToken);
|
||||
mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
assertEquals(mTaskFragment, activity.getTaskFragment());
|
||||
// The temporary token can only be used once.
|
||||
@@ -395,8 +405,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
// No lifecycle update when the TaskFragment is not recorded.
|
||||
verify(mAtm.mRootWindowContainer, never()).resumeFocusedTasksTopActivities();
|
||||
|
||||
mAtm.mWindowOrganizerController.mLaunchTaskFragments
|
||||
.put(mFragmentToken, mTaskFragment);
|
||||
mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
|
||||
assertApplyTransactionAllowed(mTransaction);
|
||||
|
||||
verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities();
|
||||
@@ -466,23 +475,23 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
// Fail to create TaskFragment when the task uid is different from caller.
|
||||
activity.info.applicationInfo.uid = uid;
|
||||
activity.getTask().effectiveUid = uid + 1;
|
||||
mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
assertNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken));
|
||||
assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken));
|
||||
|
||||
// Fail to create TaskFragment when the task uid is different from owner activity.
|
||||
activity.info.applicationInfo.uid = uid + 1;
|
||||
activity.getTask().effectiveUid = uid;
|
||||
mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
assertNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken));
|
||||
assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken));
|
||||
|
||||
// Successfully created a TaskFragment for same uid.
|
||||
activity.info.applicationInfo.uid = uid;
|
||||
activity.getTask().effectiveUid = uid;
|
||||
mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
assertNotNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken));
|
||||
assertNotNull(mWindowOrganizerController.getTaskFragment(fragmentToken));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -519,7 +528,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
.setParentTask(task)
|
||||
.setFragmentToken(mFragmentToken)
|
||||
.build();
|
||||
mAtm.mWindowOrganizerController.mLaunchTaskFragments
|
||||
mWindowOrganizerController.mLaunchTaskFragments
|
||||
.put(mFragmentToken, mTaskFragment);
|
||||
mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token);
|
||||
doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity);
|
||||
@@ -549,8 +558,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
.setOrganizer(mOrganizer)
|
||||
.createActivityCount(1)
|
||||
.build();
|
||||
mAtm.mWindowOrganizerController.mLaunchTaskFragments.put(token0, tf0);
|
||||
mAtm.mWindowOrganizerController.mLaunchTaskFragments.put(token1, tf1);
|
||||
mWindowOrganizerController.mLaunchTaskFragments.put(token0, tf0);
|
||||
mWindowOrganizerController.mLaunchTaskFragments.put(token1, tf1);
|
||||
final ActivityRecord activity0 = tf0.getTopMostActivity();
|
||||
final ActivityRecord activity1 = tf1.getTopMostActivity();
|
||||
|
||||
@@ -558,7 +567,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
final ActivityRecord activityInOtherTask = createActivityRecord(mDefaultDisplay);
|
||||
mDisplayContent.setFocusedApp(activityInOtherTask);
|
||||
mTransaction.requestFocusOnTaskFragment(token0);
|
||||
mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
assertEquals(activityInOtherTask, mDisplayContent.mFocusedApp);
|
||||
|
||||
@@ -566,7 +575,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
activity0.setState(ActivityRecord.State.PAUSED, "test");
|
||||
activity1.setState(ActivityRecord.State.RESUMED, "test");
|
||||
mDisplayContent.setFocusedApp(activity1);
|
||||
mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
assertEquals(activity1, mDisplayContent.mFocusedApp);
|
||||
|
||||
@@ -574,7 +583,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
// has a resumed activity.
|
||||
activity0.setState(ActivityRecord.State.RESUMED, "test");
|
||||
mDisplayContent.setFocusedApp(activity1);
|
||||
mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
assertEquals(activity0, mDisplayContent.mFocusedApp);
|
||||
}
|
||||
@@ -583,53 +592,50 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
public void testTaskFragmentInPip_startActivityInTaskFragment() {
|
||||
setupTaskFragmentInPip();
|
||||
final ActivityRecord activity = mTaskFragment.getTopMostActivity();
|
||||
final IBinder errorToken = new Binder();
|
||||
spyOn(mAtm.getActivityStartController());
|
||||
spyOn(mAtm.mWindowOrganizerController);
|
||||
spyOn(mWindowOrganizerController);
|
||||
|
||||
// Not allow to start activity in a TaskFragment that is in a PIP Task.
|
||||
mTransaction.startActivityInTaskFragment(
|
||||
mFragmentToken, activity.token, new Intent(), null /* activityOptions */)
|
||||
.setErrorCallbackToken(errorToken);
|
||||
mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
.setErrorCallbackToken(mErrorToken);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
verify(mAtm.getActivityStartController(), never()).startActivityInTaskFragment(any(), any(),
|
||||
any(), any(), anyInt(), anyInt());
|
||||
any(), any(), anyInt(), anyInt(), any());
|
||||
verify(mAtm.mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
|
||||
eq(errorToken), any(IllegalArgumentException.class));
|
||||
eq(mErrorToken), any(IllegalArgumentException.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTaskFragmentInPip_reparentActivityToTaskFragment() {
|
||||
setupTaskFragmentInPip();
|
||||
final ActivityRecord activity = createActivityRecord(mDisplayContent);
|
||||
final IBinder errorToken = new Binder();
|
||||
spyOn(mAtm.mWindowOrganizerController);
|
||||
spyOn(mWindowOrganizerController);
|
||||
|
||||
// Not allow to reparent activity to a TaskFragment that is in a PIP Task.
|
||||
mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token)
|
||||
.setErrorCallbackToken(errorToken);
|
||||
mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
.setErrorCallbackToken(mErrorToken);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
verify(mAtm.mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
|
||||
eq(errorToken), any(IllegalArgumentException.class));
|
||||
verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
|
||||
eq(mErrorToken), any(IllegalArgumentException.class));
|
||||
assertNull(activity.getOrganizedTaskFragment());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTaskFragmentInPip_setAdjacentTaskFragment() {
|
||||
setupTaskFragmentInPip();
|
||||
final IBinder errorToken = new Binder();
|
||||
spyOn(mAtm.mWindowOrganizerController);
|
||||
spyOn(mWindowOrganizerController);
|
||||
|
||||
// Not allow to set adjacent on a TaskFragment that is in a PIP Task.
|
||||
mTransaction.setAdjacentTaskFragments(mFragmentToken, null /* fragmentToken2 */,
|
||||
null /* options */)
|
||||
.setErrorCallbackToken(errorToken);
|
||||
mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
.setErrorCallbackToken(mErrorToken);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
verify(mAtm.mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
|
||||
eq(errorToken), any(IllegalArgumentException.class));
|
||||
verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
|
||||
eq(mErrorToken), any(IllegalArgumentException.class));
|
||||
verify(mTaskFragment, never()).setAdjacentTaskFragment(any(), anyBoolean());
|
||||
}
|
||||
|
||||
@@ -640,47 +646,45 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
ACTIVITY_TYPE_STANDARD);
|
||||
final ActivityRecord activity = createActivityRecord(pipTask);
|
||||
final IBinder fragmentToken = new Binder();
|
||||
final IBinder errorToken = new Binder();
|
||||
spyOn(mAtm.mWindowOrganizerController);
|
||||
spyOn(mWindowOrganizerController);
|
||||
|
||||
// Not allow to create TaskFragment in a PIP Task.
|
||||
createTaskFragmentFromOrganizer(mTransaction, activity, fragmentToken);
|
||||
mTransaction.setErrorCallbackToken(errorToken);
|
||||
mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
mTransaction.setErrorCallbackToken(mErrorToken);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
verify(mAtm.mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
|
||||
eq(errorToken), any(IllegalArgumentException.class));
|
||||
assertNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken));
|
||||
verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
|
||||
eq(mErrorToken), any(IllegalArgumentException.class));
|
||||
assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTaskFragmentInPip_deleteTaskFragment() {
|
||||
setupTaskFragmentInPip();
|
||||
final IBinder errorToken = new Binder();
|
||||
spyOn(mAtm.mWindowOrganizerController);
|
||||
spyOn(mWindowOrganizerController);
|
||||
|
||||
// Not allow to delete a TaskFragment that is in a PIP Task.
|
||||
mTransaction.deleteTaskFragment(mFragmentWindowToken)
|
||||
.setErrorCallbackToken(errorToken);
|
||||
mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
.setErrorCallbackToken(mErrorToken);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
verify(mAtm.mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
|
||||
eq(errorToken), any(IllegalArgumentException.class));
|
||||
assertNotNull(mAtm.mWindowOrganizerController.getTaskFragment(mFragmentToken));
|
||||
verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
|
||||
eq(mErrorToken), any(IllegalArgumentException.class));
|
||||
assertNotNull(mWindowOrganizerController.getTaskFragment(mFragmentToken));
|
||||
|
||||
// Allow organizer to delete empty TaskFragment for cleanup.
|
||||
final Task task = mTaskFragment.getTask();
|
||||
mTaskFragment.removeChild(mTaskFragment.getTopMostActivity());
|
||||
mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
assertNull(mAtm.mWindowOrganizerController.getTaskFragment(mFragmentToken));
|
||||
assertNull(mWindowOrganizerController.getTaskFragment(mFragmentToken));
|
||||
assertNull(task.getTopChild());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTaskFragmentInPip_setConfig() {
|
||||
setupTaskFragmentInPip();
|
||||
spyOn(mAtm.mWindowOrganizerController);
|
||||
spyOn(mWindowOrganizerController);
|
||||
|
||||
// Set bounds is ignored on a TaskFragment that is in a PIP Task.
|
||||
mTransaction.setBounds(mFragmentWindowToken, new Rect(0, 0, 100, 100));
|
||||
@@ -832,14 +836,13 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
final IBinder fragmentToken = new Binder();
|
||||
createTaskFragmentFromOrganizer(mTransaction, ownerActivity, fragmentToken);
|
||||
mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
|
||||
final TaskFragment taskFragment = mAtm.mWindowOrganizerController
|
||||
.getTaskFragment(fragmentToken);
|
||||
final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(fragmentToken);
|
||||
|
||||
assertNotNull(taskFragment);
|
||||
|
||||
taskFragment.removeImmediately();
|
||||
|
||||
assertNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken));
|
||||
assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -902,6 +905,101 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
|
||||
assertApplyTransactionDisallowed(mTransaction);
|
||||
}
|
||||
|
||||
// TODO(b/232871351): add test for minimum dimension violation in startActivityInTaskFragment
|
||||
@Test
|
||||
public void testMinDimensionViolation_ReparentActivityToTaskFragment() {
|
||||
final Task task = createTask(mDisplayContent);
|
||||
final ActivityRecord activity = createActivityRecord(task);
|
||||
// Make minWidth/minHeight exceeds the TaskFragment bounds.
|
||||
activity.info.windowLayout = new ActivityInfo.WindowLayout(
|
||||
0, 0, 0, 0, 0, mTaskFragBounds.width() + 10, mTaskFragBounds.height() + 10);
|
||||
mOrganizer.applyTransaction(mTransaction);
|
||||
mController.registerOrganizer(mIOrganizer);
|
||||
mTaskFragment = new TaskFragmentBuilder(mAtm)
|
||||
.setParentTask(task)
|
||||
.setFragmentToken(mFragmentToken)
|
||||
.setOrganizer(mOrganizer)
|
||||
.setBounds(mTaskFragBounds)
|
||||
.build();
|
||||
doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity);
|
||||
mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
|
||||
clearInvocations(mAtm.mRootWindowContainer);
|
||||
|
||||
// Reparent activity to mTaskFragment, which is smaller than activity's
|
||||
// minimum dimensions.
|
||||
mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token)
|
||||
.setErrorCallbackToken(mErrorToken);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), any(SecurityException.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMinDimensionViolation_ReparentChildren() {
|
||||
final Task task = createTask(mDisplayContent);
|
||||
mOrganizer.applyTransaction(mTransaction);
|
||||
mController.registerOrganizer(mIOrganizer);
|
||||
final IBinder oldFragToken = new Binder();
|
||||
final TaskFragment oldTaskFrag = new TaskFragmentBuilder(mAtm)
|
||||
.setParentTask(task)
|
||||
.createActivityCount(1)
|
||||
.setFragmentToken(oldFragToken)
|
||||
.setOrganizer(mOrganizer)
|
||||
.build();
|
||||
final ActivityRecord activity = oldTaskFrag.getTopMostActivity();
|
||||
// Make minWidth/minHeight exceeds mTaskFragment bounds.
|
||||
activity.info.windowLayout = new ActivityInfo.WindowLayout(
|
||||
0, 0, 0, 0, 0, mTaskFragBounds.width() + 10, mTaskFragBounds.height() + 10);
|
||||
mTaskFragment = new TaskFragmentBuilder(mAtm)
|
||||
.setParentTask(task)
|
||||
.setFragmentToken(mFragmentToken)
|
||||
.setOrganizer(mOrganizer)
|
||||
.setBounds(mTaskFragBounds)
|
||||
.build();
|
||||
doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity);
|
||||
mWindowOrganizerController.mLaunchTaskFragments.put(oldFragToken, oldTaskFrag);
|
||||
mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
|
||||
clearInvocations(mAtm.mRootWindowContainer);
|
||||
|
||||
// Reparent oldTaskFrag's children to mTaskFragment, which is smaller than activity's
|
||||
// minimum dimensions.
|
||||
mTransaction.reparentChildren(oldTaskFrag.mRemoteToken.toWindowContainerToken(),
|
||||
mTaskFragment.mRemoteToken.toWindowContainerToken())
|
||||
.setErrorCallbackToken(mErrorToken);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), any(SecurityException.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMinDimensionViolation_SetBounds() {
|
||||
final Task task = createTask(mDisplayContent);
|
||||
mOrganizer.applyTransaction(mTransaction);
|
||||
mController.registerOrganizer(mIOrganizer);
|
||||
mTaskFragment = new TaskFragmentBuilder(mAtm)
|
||||
.setParentTask(task)
|
||||
.createActivityCount(1)
|
||||
.setFragmentToken(mFragmentToken)
|
||||
.setOrganizer(mOrganizer)
|
||||
.setBounds(new Rect(0, 0, mTaskFragBounds.right * 2, mTaskFragBounds.bottom * 2))
|
||||
.build();
|
||||
final ActivityRecord activity = mTaskFragment.getTopMostActivity();
|
||||
// Make minWidth/minHeight exceeds the TaskFragment bounds.
|
||||
activity.info.windowLayout = new ActivityInfo.WindowLayout(
|
||||
0, 0, 0, 0, 0, mTaskFragBounds.width() + 10, mTaskFragBounds.height() + 10);
|
||||
mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
|
||||
clearInvocations(mAtm.mRootWindowContainer);
|
||||
|
||||
// Shrink the TaskFragment to mTaskFragBounds to make its bounds smaller than activity's
|
||||
// minimum dimensions.
|
||||
mTransaction.setBounds(mTaskFragment.mRemoteToken.toWindowContainerToken(), mTaskFragBounds)
|
||||
.setErrorCallbackToken(mErrorToken);
|
||||
mWindowOrganizerController.applyTransaction(mTransaction);
|
||||
|
||||
assertWithMessage("setBounds must not be performed.")
|
||||
.that(mTaskFragment.getBounds()).isEqualTo(task.getBounds());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link TaskFragment} with the {@link WindowContainerTransaction}. Calls
|
||||
* {@link WindowOrganizerController#applyTransaction} to apply the transaction,
|
||||
|
||||
@@ -1199,6 +1199,7 @@ class WindowTestsBase extends SystemServiceTestsBase {
|
||||
@Nullable
|
||||
private TaskFragmentOrganizer mOrganizer;
|
||||
private IBinder mFragmentToken;
|
||||
private Rect mBounds;
|
||||
|
||||
TaskFragmentBuilder(ActivityTaskManagerService service) {
|
||||
mAtm = service;
|
||||
@@ -1235,6 +1236,11 @@ class WindowTestsBase extends SystemServiceTestsBase {
|
||||
return this;
|
||||
}
|
||||
|
||||
TaskFragmentBuilder setBounds(@Nullable Rect bounds) {
|
||||
mBounds = bounds;
|
||||
return this;
|
||||
}
|
||||
|
||||
TaskFragment build() {
|
||||
SystemServicesTestRule.checkHoldsLock(mAtm.mGlobalLock);
|
||||
|
||||
@@ -1262,6 +1268,9 @@ class WindowTestsBase extends SystemServiceTestsBase {
|
||||
mOrganizer.getOrganizerToken(), DEFAULT_TASK_FRAGMENT_ORGANIZER_UID,
|
||||
DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME);
|
||||
}
|
||||
if (mBounds != null && !mBounds.isEmpty()) {
|
||||
taskFragment.setBounds(mBounds);
|
||||
}
|
||||
spyOn(taskFragment);
|
||||
return taskFragment;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user