Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e382326e authored by Chris Li's avatar Chris Li
Browse files

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
parent d41df352
Loading
Loading
Loading
Loading
+36 −57
Original line number Diff line number Diff line
@@ -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();
        }
    }
}
+17 −28
Original line number Diff line number Diff line
@@ -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();
        }

        // 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");
        final int taskId = container.getTaskId();
        final TaskContainer taskContainer = mController.getTaskContainer(taskId);
        if (taskContainer == null) {
            throw new IllegalStateException("Can't find TaskContainer taskId=" + taskId);
        }
        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();
    }
}
+97 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+25 −2
Original line number Diff line number Diff line
@@ -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());
    }
}
+82 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}