Fix crash in getParentContainerBounds

Before, before TaskFragment appeared, the getTopNonFinishingActivity may
be null, and throw exception for no bounds found.
Now, make sure we always have Task bounds stored.

Bug: 228885031
Test: atest WMJetpackUnitTests:SplitControllerTest
Test: atest WMJetpackUnitTests:TaskContainerTest
Change-Id: I3744813a85f24941563c8ff8f284b2e45396f906
This commit is contained in:
Chris Li
2022-04-18 23:30:04 +08:00
parent d41df35243
commit e382326e3d
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.Looper;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;
@@ -59,6 +60,7 @@ import java.util.function.Consumer;
*/
public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
ActivityEmbeddingComponent {
private static final String TAG = "SplitController";
@VisibleForTesting
final SplitPresenter mPresenter;
@@ -229,8 +231,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
if (taskContainer.isEmpty()) {
// Cleanup the TaskContainer if it becomes empty.
mPresenter.stopOverrideSplitAnimation(taskContainer.mTaskId);
mTaskContainers.remove(taskContainer.mTaskId);
mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId());
mTaskContainers.remove(taskContainer.getTaskId());
}
return;
}
@@ -241,13 +243,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
if (taskContainer == null) {
return;
}
final boolean wasInPip = isInPictureInPicture(taskContainer.mConfiguration);
final boolean wasInPip = isInPictureInPicture(taskContainer.getConfiguration());
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.
boolean shouldUpdateAnimationOverride = wasInPip != isInPIp;
if (onTaskBoundsMayChange(taskContainer, config.windowConfiguration.getBounds())
if (taskContainer.setTaskBounds(config.windowConfiguration.getBounds())
&& !isInPIp) {
// We don't care the bounds change when it has already entered PIP.
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
* 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.
if (supportSplit(taskContainer)) {
mPresenter.startOverrideSplitAnimation(taskContainer.mTaskId);
mPresenter.startOverrideSplitAnimation(taskContainer.getTaskId());
} else {
mPresenter.stopOverrideSplitAnimation(taskContainer.mTaskId);
mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId());
}
}
private boolean supportSplit(@NonNull TaskContainer taskContainer) {
// No split inside PIP.
if (isInPictureInPicture(taskContainer.mConfiguration)) {
if (isInPictureInPicture(taskContainer.getConfiguration())) {
return false;
}
// 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)) {
continue;
}
if (mPresenter.shouldShowSideBySide(taskContainer.mTaskBounds, (SplitRule) rule)) {
if (mPresenter.shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) {
return true;
}
}
@@ -425,21 +417,36 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
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
* 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);
if (!mTaskContainers.contains(taskId)) {
mTaskContainers.put(taskId, new TaskContainer(taskId));
}
final TaskContainer taskContainer = mTaskContainers.get(taskId);
taskContainer.mContainers.add(container);
if (activity != null && !taskContainer.isTaskBoundsInitialized()
&& onTaskBoundsMayChange(taskContainer,
SplitPresenter.getTaskBoundsFromActivity(activity))) {
// Initial check before any TaskFragment has appeared.
if (!taskContainer.isTaskBoundsInitialized()) {
// Get the initial bounds before the TaskFragment has appeared.
final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask);
if (!taskContainer.setTaskBounds(taskBounds)) {
Log.w(TAG, "Can't find bounds from activity=" + activityInTask);
}
updateAnimationOverride(taskContainer);
}
return container;
@@ -887,6 +894,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
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
* 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
&& 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 android.app.Activity;
import android.app.WindowConfiguration;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
@@ -111,8 +111,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
primaryActivity, primaryRectBounds, null);
// Create new empty task fragment
final TaskFragmentContainer secondaryContainer = mController.newContainer(null,
primaryContainer.getTaskId());
final TaskFragmentContainer secondaryContainer = mController.newContainer(
null /* activity */, primaryActivity, primaryContainer.getTaskId());
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds,
rule, isLtr(primaryActivity, rule));
createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
@@ -168,8 +168,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
* Creates a new expanded container.
*/
TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) {
final TaskFragmentContainer newContainer = mController.newContainer(null,
launchingActivity.getTaskId());
final TaskFragmentContainer newContainer = mController.newContainer(null /* activity */,
launchingActivity, launchingActivity.getTaskId());
final WindowContainerTransaction wct = new WindowContainerTransaction();
createTaskFragment(wct, newContainer.getTaskFragmentToken(),
@@ -236,8 +236,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
launchingActivity.getTaskId());
}
TaskFragmentContainer secondaryContainer = mController.newContainer(null,
primaryContainer.getTaskId());
TaskFragmentContainer secondaryContainer = mController.newContainer(null /* activity */,
launchingActivity, primaryContainer.getTaskId());
final WindowContainerTransaction wct = new WindowContainerTransaction();
mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
rule);
@@ -398,20 +398,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull
Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) {
final Configuration parentConfig = mFragmentParentConfigs.get(
container.getTaskFragmentToken());
if (parentConfig != null) {
return parentConfig.windowConfiguration.getBounds();
final int taskId = container.getTaskId();
final TaskContainer taskContainer = mController.getTaskContainer(taskId);
if (taskContainer == null) {
throw new IllegalStateException("Can't find TaskContainer taskId=" + taskId);
}
// 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;
return taskContainer.getTaskBounds();
}
@NonNull
@@ -419,22 +411,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
final TaskFragmentContainer container = mController.getContainerWithActivity(
activity.getActivityToken());
if (container != null) {
final Configuration parentConfig = mFragmentParentConfigs.get(
container.getTaskFragmentToken());
if (parentConfig != null) {
return parentConfig.windowConfiguration.getBounds();
}
return getParentContainerBounds(container);
}
return getTaskBoundsFromActivity(activity);
}
@NonNull
static Rect getTaskBoundsFromActivity(@NonNull Activity activity) {
final WindowConfiguration windowConfiguration =
activity.getResources().getConfiguration().windowConfiguration;
if (!activity.isInMultiWindowMode()) {
// 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 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.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -29,12 +32,12 @@ import static org.mockito.Mockito.never;
import android.app.Activity;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.window.extensions.embedding.SplitController.TaskContainer;
import org.junit.Before;
import org.junit.Test;
@@ -53,6 +56,7 @@ import org.mockito.MockitoAnnotations;
@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);
@Mock
private Activity mActivity;
@@ -70,8 +74,11 @@ public class SplitControllerTest {
mSplitPresenter = mSplitController.mPresenter;
spyOn(mSplitController);
spyOn(mSplitPresenter);
final Configuration activityConfig = new Configuration();
activityConfig.windowConfiguration.setBounds(TASK_BOUNDS);
activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS);
doReturn(mActivityResources).when(mActivity).getResources();
doReturn(new Configuration()).when(mActivityResources).getConfiguration();
doReturn(activityConfig).when(mActivityResources).getConfiguration();
}
@Test
@@ -117,4 +124,20 @@ public class SplitControllerTest {
verify(mSplitController).removeContainer(tf);
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());
}
}