Merge "Fix crash in getParentContainerBounds" into tm-dev

This commit is contained in:
Chris Li
2022-04-20 13:04:17 +00:00
committed by Android (Google) Code Review
5 changed files with 257 additions and 87 deletions

View File

@@ -40,6 +40,7 @@ import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import android.util.ArraySet; import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.window.TaskFragmentInfo; import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction; import android.window.WindowContainerTransaction;
@@ -59,6 +60,7 @@ import java.util.function.Consumer;
*/ */
public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
ActivityEmbeddingComponent { ActivityEmbeddingComponent {
private static final String TAG = "SplitController";
@VisibleForTesting @VisibleForTesting
final SplitPresenter mPresenter; final SplitPresenter mPresenter;
@@ -229,8 +231,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
} }
if (taskContainer.isEmpty()) { if (taskContainer.isEmpty()) {
// Cleanup the TaskContainer if it becomes empty. // Cleanup the TaskContainer if it becomes empty.
mPresenter.stopOverrideSplitAnimation(taskContainer.mTaskId); mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId());
mTaskContainers.remove(taskContainer.mTaskId); mTaskContainers.remove(taskContainer.getTaskId());
} }
return; return;
} }
@@ -241,13 +243,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
if (taskContainer == null) { if (taskContainer == null) {
return; return;
} }
final boolean wasInPip = isInPictureInPicture(taskContainer.mConfiguration); final boolean wasInPip = isInPictureInPicture(taskContainer.getConfiguration());
final boolean isInPIp = isInPictureInPicture(config); final boolean isInPIp = isInPictureInPicture(config);
taskContainer.mConfiguration = config; taskContainer.setConfiguration(config);
// We need to check the animation override when enter/exit PIP or has bounds changed. // We need to check the animation override when enter/exit PIP or has bounds changed.
boolean shouldUpdateAnimationOverride = wasInPip != isInPIp; boolean shouldUpdateAnimationOverride = wasInPip != isInPIp;
if (onTaskBoundsMayChange(taskContainer, config.windowConfiguration.getBounds()) if (taskContainer.setTaskBounds(config.windowConfiguration.getBounds())
&& !isInPIp) { && !isInPIp) {
// We don't care the bounds change when it has already entered PIP. // We don't care the bounds change when it has already entered PIP.
shouldUpdateAnimationOverride = true; shouldUpdateAnimationOverride = true;
@@ -257,16 +259,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
} }
} }
/** Returns {@code true} if the bounds is changed. */
private boolean onTaskBoundsMayChange(@NonNull TaskContainer taskContainer,
@NonNull Rect taskBounds) {
if (!taskBounds.isEmpty() && !taskContainer.mTaskBounds.equals(taskBounds)) {
taskContainer.mTaskBounds.set(taskBounds);
return true;
}
return false;
}
/** /**
* Updates if we should override transition animation. We only want to override if the Task * Updates if we should override transition animation. We only want to override if the Task
* bounds is large enough for at least one split rule. * bounds is large enough for at least one split rule.
@@ -279,15 +271,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// We only want to override if it supports split. // We only want to override if it supports split.
if (supportSplit(taskContainer)) { if (supportSplit(taskContainer)) {
mPresenter.startOverrideSplitAnimation(taskContainer.mTaskId); mPresenter.startOverrideSplitAnimation(taskContainer.getTaskId());
} else { } else {
mPresenter.stopOverrideSplitAnimation(taskContainer.mTaskId); mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId());
} }
} }
private boolean supportSplit(@NonNull TaskContainer taskContainer) { private boolean supportSplit(@NonNull TaskContainer taskContainer) {
// No split inside PIP. // No split inside PIP.
if (isInPictureInPicture(taskContainer.mConfiguration)) { if (isInPictureInPicture(taskContainer.getConfiguration())) {
return false; return false;
} }
// Check if the parent container bounds can support any split rule. // Check if the parent container bounds can support any split rule.
@@ -295,7 +287,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
if (!(rule instanceof SplitRule)) { if (!(rule instanceof SplitRule)) {
continue; continue;
} }
if (mPresenter.shouldShowSideBySide(taskContainer.mTaskBounds, (SplitRule) rule)) { if (mPresenter.shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) {
return true; return true;
} }
} }
@@ -425,21 +417,36 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return null; return null;
} }
TaskFragmentContainer newContainer(@NonNull Activity activity, int taskId) {
return newContainer(activity, activity, taskId);
}
/** /**
* Creates and registers a new organized container with an optional activity that will be * Creates and registers a new organized container with an optional activity that will be
* re-parented to it in a WCT. * re-parented to it in a WCT.
*
* @param activity the activity that will be reparented to the TaskFragment.
* @param activityInTask activity in the same Task so that we can get the Task bounds if
* needed.
* @param taskId parent Task of the new TaskFragment.
*/ */
TaskFragmentContainer newContainer(@Nullable Activity activity, int taskId) { TaskFragmentContainer newContainer(@Nullable Activity activity,
@NonNull Activity activityInTask, int taskId) {
if (activityInTask == null) {
throw new IllegalArgumentException("activityInTask must not be null,");
}
final TaskFragmentContainer container = new TaskFragmentContainer(activity, taskId); final TaskFragmentContainer container = new TaskFragmentContainer(activity, taskId);
if (!mTaskContainers.contains(taskId)) { if (!mTaskContainers.contains(taskId)) {
mTaskContainers.put(taskId, new TaskContainer(taskId)); mTaskContainers.put(taskId, new TaskContainer(taskId));
} }
final TaskContainer taskContainer = mTaskContainers.get(taskId); final TaskContainer taskContainer = mTaskContainers.get(taskId);
taskContainer.mContainers.add(container); taskContainer.mContainers.add(container);
if (activity != null && !taskContainer.isTaskBoundsInitialized() if (!taskContainer.isTaskBoundsInitialized()) {
&& onTaskBoundsMayChange(taskContainer, // Get the initial bounds before the TaskFragment has appeared.
SplitPresenter.getTaskBoundsFromActivity(activity))) { final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask);
// Initial check before any TaskFragment has appeared. if (!taskContainer.setTaskBounds(taskBounds)) {
Log.w(TAG, "Can't find bounds from activity=" + activityInTask);
}
updateAnimationOverride(taskContainer); updateAnimationOverride(taskContainer);
} }
return container; return container;
@@ -887,6 +894,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return null; return null;
} }
@Nullable
TaskContainer getTaskContainer(int taskId) {
return mTaskContainers.get(taskId);
}
/** /**
* Returns {@code true} if an Activity with the provided component name should always be * Returns {@code true} if an Activity with the provided component name should always be
* expanded to occupy full task bounds. Such activity must not be put in a split. * expanded to occupy full task bounds. Such activity must not be put in a split.
@@ -1211,37 +1223,4 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return configuration != null return configuration != null
&& configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED; && configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED;
} }
/** Represents TaskFragments and split pairs below a Task. */
@VisibleForTesting
static class TaskContainer {
/** The unique task id. */
final int mTaskId;
/** Active TaskFragments in this Task. */
final List<TaskFragmentContainer> mContainers = new ArrayList<>();
/** Active split pairs in this Task. */
final List<SplitContainer> mSplitContainers = new ArrayList<>();
/**
* TaskFragments that the organizer has requested to be closed. They should be removed when
* the organizer receives {@link #onTaskFragmentVanished(TaskFragmentInfo)} event for them.
*/
final Set<IBinder> mFinishedContainer = new ArraySet<>();
/** Available window bounds of this Task. */
final Rect mTaskBounds = new Rect();
/** Configuration of the Task. */
@Nullable
Configuration mConfiguration;
TaskContainer(int taskId) {
mTaskId = taskId;
}
boolean isEmpty() {
return mContainers.isEmpty() && mFinishedContainer.isEmpty();
}
boolean isTaskBoundsInitialized() {
return !mTaskBounds.isEmpty();
}
}
} }

View File

@@ -19,9 +19,9 @@ package androidx.window.extensions.embedding;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import android.app.Activity; import android.app.Activity;
import android.app.WindowConfiguration;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
@@ -111,8 +111,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
primaryActivity, primaryRectBounds, null); primaryActivity, primaryRectBounds, null);
// Create new empty task fragment // Create new empty task fragment
final TaskFragmentContainer secondaryContainer = mController.newContainer(null, final TaskFragmentContainer secondaryContainer = mController.newContainer(
primaryContainer.getTaskId()); null /* activity */, primaryActivity, primaryContainer.getTaskId());
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds,
rule, isLtr(primaryActivity, rule)); rule, isLtr(primaryActivity, rule));
createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
@@ -168,8 +168,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
* Creates a new expanded container. * Creates a new expanded container.
*/ */
TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) { TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) {
final TaskFragmentContainer newContainer = mController.newContainer(null, final TaskFragmentContainer newContainer = mController.newContainer(null /* activity */,
launchingActivity.getTaskId()); launchingActivity, launchingActivity.getTaskId());
final WindowContainerTransaction wct = new WindowContainerTransaction(); final WindowContainerTransaction wct = new WindowContainerTransaction();
createTaskFragment(wct, newContainer.getTaskFragmentToken(), createTaskFragment(wct, newContainer.getTaskFragmentToken(),
@@ -236,8 +236,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
launchingActivity.getTaskId()); launchingActivity.getTaskId());
} }
TaskFragmentContainer secondaryContainer = mController.newContainer(null, TaskFragmentContainer secondaryContainer = mController.newContainer(null /* activity */,
primaryContainer.getTaskId()); launchingActivity, primaryContainer.getTaskId());
final WindowContainerTransaction wct = new WindowContainerTransaction(); final WindowContainerTransaction wct = new WindowContainerTransaction();
mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
rule); rule);
@@ -398,20 +398,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull @NonNull
Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) {
final Configuration parentConfig = mFragmentParentConfigs.get( final int taskId = container.getTaskId();
container.getTaskFragmentToken()); final TaskContainer taskContainer = mController.getTaskContainer(taskId);
if (parentConfig != null) { if (taskContainer == null) {
return parentConfig.windowConfiguration.getBounds(); throw new IllegalStateException("Can't find TaskContainer taskId=" + taskId);
} }
return taskContainer.getTaskBounds();
// If there is no parent yet - then assuming that activities are running in full task bounds
final Activity topActivity = container.getTopNonFinishingActivity();
final Rect bounds = topActivity != null ? getParentContainerBounds(topActivity) : null;
if (bounds == null) {
throw new IllegalStateException("Unknown parent bounds");
}
return bounds;
} }
@NonNull @NonNull
@@ -419,22 +411,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
final TaskFragmentContainer container = mController.getContainerWithActivity( final TaskFragmentContainer container = mController.getContainerWithActivity(
activity.getActivityToken()); activity.getActivityToken());
if (container != null) { if (container != null) {
final Configuration parentConfig = mFragmentParentConfigs.get( return getParentContainerBounds(container);
container.getTaskFragmentToken());
if (parentConfig != null) {
return parentConfig.windowConfiguration.getBounds();
}
} }
return getTaskBoundsFromActivity(activity); return getTaskBoundsFromActivity(activity);
} }
@NonNull @NonNull
static Rect getTaskBoundsFromActivity(@NonNull Activity activity) { static Rect getTaskBoundsFromActivity(@NonNull Activity activity) {
final WindowConfiguration windowConfiguration =
activity.getResources().getConfiguration().windowConfiguration;
if (!activity.isInMultiWindowMode()) { if (!activity.isInMultiWindowMode()) {
// In fullscreen mode the max bounds should correspond to the task bounds. // In fullscreen mode the max bounds should correspond to the task bounds.
return activity.getResources().getConfiguration().windowConfiguration.getMaxBounds(); return windowConfiguration.getMaxBounds();
} }
return activity.getResources().getConfiguration().windowConfiguration.getBounds(); return windowConfiguration.getBounds();
} }
} }

View File

@@ -0,0 +1,97 @@
/*
* 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.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.IBinder;
import android.util.ArraySet;
import android.window.TaskFragmentInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/** Represents TaskFragments and split pairs below a Task. */
class TaskContainer {
/** The unique task id. */
private final int mTaskId;
/** Available window bounds of this Task. */
private final Rect mTaskBounds = new Rect();
/** Configuration of the Task. */
@Nullable
private Configuration mConfiguration;
/** Active TaskFragments in this Task. */
final List<TaskFragmentContainer> mContainers = new ArrayList<>();
/** Active split pairs in this Task. */
final List<SplitContainer> mSplitContainers = new ArrayList<>();
/**
* TaskFragments that the organizer has requested to be closed. They should be removed when
* the organizer receives {@link SplitController#onTaskFragmentVanished(TaskFragmentInfo)} event
* for them.
*/
final Set<IBinder> mFinishedContainer = new ArraySet<>();
TaskContainer(int taskId) {
mTaskId = taskId;
}
int getTaskId() {
return mTaskId;
}
@NonNull
Rect getTaskBounds() {
return mTaskBounds;
}
/** Returns {@code true} if the bounds is changed. */
boolean setTaskBounds(@NonNull Rect taskBounds) {
if (!taskBounds.isEmpty() && !mTaskBounds.equals(taskBounds)) {
mTaskBounds.set(taskBounds);
return true;
}
return false;
}
/** Whether the Task bounds has been initialized. */
boolean isTaskBoundsInitialized() {
return !mTaskBounds.isEmpty();
}
@Nullable
Configuration getConfiguration() {
return mConfiguration;
}
void setConfiguration(@Nullable Configuration configuration) {
mConfiguration = configuration;
}
/** Whether there is any {@link TaskFragmentContainer} below this Task. */
boolean isEmpty() {
return mContainers.isEmpty() && mFinishedContainer.isEmpty();
}
}

View File

@@ -21,6 +21,9 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@@ -29,12 +32,12 @@ import static org.mockito.Mockito.never;
import android.app.Activity; import android.app.Activity;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo; import android.window.TaskFragmentInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import androidx.window.extensions.embedding.SplitController.TaskContainer;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -53,6 +56,7 @@ import org.mockito.MockitoAnnotations;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class SplitControllerTest { public class SplitControllerTest {
private static final int TASK_ID = 10; private static final int TASK_ID = 10;
private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200);
@Mock @Mock
private Activity mActivity; private Activity mActivity;
@@ -70,8 +74,11 @@ public class SplitControllerTest {
mSplitPresenter = mSplitController.mPresenter; mSplitPresenter = mSplitController.mPresenter;
spyOn(mSplitController); spyOn(mSplitController);
spyOn(mSplitPresenter); spyOn(mSplitPresenter);
final Configuration activityConfig = new Configuration();
activityConfig.windowConfiguration.setBounds(TASK_BOUNDS);
activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS);
doReturn(mActivityResources).when(mActivity).getResources(); doReturn(mActivityResources).when(mActivity).getResources();
doReturn(new Configuration()).when(mActivityResources).getConfiguration(); doReturn(activityConfig).when(mActivityResources).getConfiguration();
} }
@Test @Test
@@ -117,4 +124,20 @@ public class SplitControllerTest {
verify(mSplitController).removeContainer(tf); verify(mSplitController).removeContainer(tf);
verify(mActivity, never()).finish(); verify(mActivity, never()).finish();
} }
@Test
public void testNewContainer() {
// Must pass in a valid activity.
assertThrows(IllegalArgumentException.class, () ->
mSplitController.newContainer(null /* activity */, TASK_ID));
assertThrows(IllegalArgumentException.class, () ->
mSplitController.newContainer(mActivity, null /* launchingActivity */, TASK_ID));
final TaskFragmentContainer tf = mSplitController.newContainer(null, mActivity, TASK_ID);
final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
assertNotNull(tf);
assertNotNull(taskContainer);
assertEquals(TASK_BOUNDS, taskContainer.getTaskBounds());
}
} }

View File

@@ -0,0 +1,82 @@
/*
* 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 org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Test class for {@link TaskContainer}.
*
* Build/Install/Run:
* atest WMJetpackUnitTests:TaskContainerTest
*/
@Presubmit
@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);
@Test
public void testIsTaskBoundsInitialized() {
final TaskContainer taskContainer = new TaskContainer(TASK_ID);
assertFalse(taskContainer.isTaskBoundsInitialized());
taskContainer.setTaskBounds(TASK_BOUNDS);
assertTrue(taskContainer.isTaskBoundsInitialized());
}
@Test
public void testSetTaskBounds() {
final TaskContainer taskContainer = new TaskContainer(TASK_ID);
assertFalse(taskContainer.setTaskBounds(new Rect()));
assertTrue(taskContainer.setTaskBounds(TASK_BOUNDS));
assertFalse(taskContainer.setTaskBounds(TASK_BOUNDS));
}
@Test
public void testIsEmpty() {
final TaskContainer taskContainer = new TaskContainer(TASK_ID);
assertTrue(taskContainer.isEmpty());
final TaskFragmentContainer tf = new TaskFragmentContainer(null, TASK_ID);
taskContainer.mContainers.add(tf);
assertFalse(taskContainer.isEmpty());
taskContainer.mFinishedContainer.add(tf.getTaskFragmentToken());
taskContainer.mContainers.clear();
assertFalse(taskContainer.isEmpty());
}
}