diff options
| author | 2022-10-18 12:15:00 +0000 | |
|---|---|---|
| committer | 2022-10-18 12:15:00 +0000 | |
| commit | a08ba967973fb16ecdcd380cf84a82920f808a53 (patch) | |
| tree | 3dbb793fff0b2a10091df6a8402cb4e7d75684cb | |
| parent | bde4635f43d54f743136fd25497eac6e9edb6c4f (diff) | |
| parent | 3dd646cf2065088f4caef304c66d7831fc00a27d (diff) | |
Merge "Keep track of the WCT changes to determine the transition type" into tm-qpr-dev am: 3dd646cf20
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19992916
Change-Id: Ia08c54546071ff4f5d5afaa0c084839c2e529b17
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
4 files changed, 468 insertions, 23 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 1174b685e92c..bf7326a5b30e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -20,10 +20,11 @@ import static android.app.ActivityManager.START_SUCCESS; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE; -import static android.window.TaskFragmentOrganizer.getTransitionType; import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; @@ -76,6 +77,7 @@ import androidx.annotation.Nullable; import androidx.window.common.CommonFoldingFeature; import androidx.window.common.EmptyLifecycleCallbacksAdapter; import androidx.window.extensions.WindowExtensionsProvider; +import androidx.window.extensions.embedding.TransactionManager.TransactionRecord; import androidx.window.extensions.layout.WindowLayoutComponentImpl; import com.android.internal.annotations.VisibleForTesting; @@ -100,6 +102,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") final SplitPresenter mPresenter; + @VisibleForTesting + @GuardedBy("mLock") + final TransactionManager mTransactionManager; + // Currently applied split configuration. @GuardedBy("mLock") private final List<EmbeddingRule> mSplitRules = new ArrayList<>(); @@ -150,6 +156,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final MainThreadExecutor executor = new MainThreadExecutor(); mHandler = executor.mHandler; mPresenter = new SplitPresenter(executor, this); + mTransactionManager = new TransactionManager(mPresenter); final ActivityThread activityThread = ActivityThread.currentActivityThread(); final Application application = activityThread.getApplication(); // Register a callback to be notified about activities being created. @@ -167,7 +174,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void accept(List<CommonFoldingFeature> foldingFeatures) { synchronized (mLock) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); for (int i = 0; i < mTaskContainers.size(); i++) { final TaskContainer taskContainer = mTaskContainers.valueAt(i); if (!taskContainer.isVisible()) { @@ -186,7 +195,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen updateContainersInTask(wct, taskContainer); updateAnimationOverride(taskContainer); } - mPresenter.applyTransaction(wct); + // The WCT should be applied and merged to the device state change transition if + // there is one. + transactionRecord.apply(false /* shouldApplyIndependently */); } } } @@ -256,7 +267,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) { synchronized (mLock) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction( + transaction.getTransactionToken()); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); for (TaskFragmentTransaction.Change change : changes) { final int taskId = change.getTaskId(); @@ -307,8 +320,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Notify the server, and the server should apply and merge the // WindowContainerTransaction to the active sync to finish the TaskFragmentTransaction. - mPresenter.onTransactionHandled(transaction.getTransactionToken(), wct, - getTransitionType(wct), false /* shouldApplyIndependently */); + transactionRecord.apply(false /* shouldApplyIndependently */); updateCallbackIfNecessary(); } } @@ -333,6 +345,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen container.setInfo(wct, taskFragmentInfo); if (container.isFinished()) { + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } else { // Update with the latest Task configuration. @@ -368,15 +381,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Do not finish the dependents if the last activity is reparented to PiP. // Instead, the original split should be cleanup, and the dependent may be // expanded to fullscreen. + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); cleanupForEnterPip(wct, container); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } else if (taskFragmentInfo.isTaskClearedForReuse()) { // Do not finish the dependents if this TaskFragment was cleared due to // launching activity in the Task. + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } else if (!container.isWaitingActivityAppear()) { // Do not finish the container before the expected activity appear until // timeout. + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */); } } else if (wasInPip && isInPip) { @@ -571,6 +587,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen container.setInfo(wct, taskFragmentInfo); container.clearPendingAppearedActivities(); if (container.isEmpty()) { + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } break; @@ -1009,11 +1026,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @GuardedBy("mLock") void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - onTaskFragmentAppearEmptyTimeout(wct, container); + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + onTaskFragmentAppearEmptyTimeout(transactionRecord.getTransaction(), container); // Can be applied independently as a timeout callback. - mPresenter.applyTransaction(wct, getTransitionType(wct), - true /* shouldApplyIndependently */); + transactionRecord.apply(true /* shouldApplyIndependently */); } /** @@ -1023,6 +1039,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") void onTaskFragmentAppearEmptyTimeout(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } @@ -1562,6 +1579,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * @param isOnCreated whether this happens during the primary activity onCreated. */ @VisibleForTesting + @GuardedBy("mLock") @Nullable Bundle getPlaceholderOptions(@NonNull Activity primaryActivity, boolean isOnCreated) { // Setting avoid move to front will also skip the animation. We only want to do that when @@ -1569,6 +1587,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Check if the primary is resumed or if this is called when the primary is onCreated // (not resumed yet). if (isOnCreated || primaryActivity.isResumed()) { + // Only set trigger type if the launch happens in foreground. + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_OPEN); return null; } final ActivityOptions options = ActivityOptions.makeBasic(); @@ -1595,6 +1615,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (SplitPresenter.shouldShowSplit(splitAttributes)) { return false; } + + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(), false /* shouldFinishDependent */); return true; @@ -1905,23 +1927,26 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // that we don't launch it if an activity itself already requested something to be // launched to side. synchronized (mLock) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - SplitController.this.onActivityCreated(wct, activity); + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + transactionRecord.setOriginType(TRANSIT_OPEN); + SplitController.this.onActivityCreated(transactionRecord.getTransaction(), + activity); // The WCT should be applied and merged to the activity launch transition. - mPresenter.applyTransaction(wct, getTransitionType(wct), - false /* shouldApplyIndependently */); + transactionRecord.apply(false /* shouldApplyIndependently */); } } @Override public void onActivityConfigurationChanged(@NonNull Activity activity) { synchronized (mLock) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - SplitController.this.onActivityConfigurationChanged(wct, activity); + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + SplitController.this.onActivityConfigurationChanged( + transactionRecord.getTransaction(), activity); // The WCT should be applied and merged to the Task change transition so that the // placeholder is launched in the same transition. - mPresenter.applyTransaction(wct, getTransitionType(wct), - false /* shouldApplyIndependently */); + transactionRecord.apply(false /* shouldApplyIndependently */); } } @@ -1977,7 +2002,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } synchronized (mLock) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + transactionRecord.setOriginType(TRANSIT_OPEN); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); final TaskFragmentContainer launchedInTaskFragment; if (launchingActivity != null) { final int taskId = getTaskId(launchingActivity); @@ -1990,13 +2018,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (launchedInTaskFragment != null) { // Make sure the WCT is applied immediately instead of being queued so that the // TaskFragment will be ready before activity attachment. - mPresenter.applyTransaction(wct, getTransitionType(wct), - false /* shouldApplyIndependently */); + transactionRecord.apply(false /* shouldApplyIndependently */); // Amend the request to let the WM know that the activity should be placed in // the dedicated container. options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, launchedInTaskFragment.getTaskFragmentToken()); mCurrentIntent = intent; + } else { + transactionRecord.abort(); } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java new file mode 100644 index 000000000000..0071fea41aa8 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java @@ -0,0 +1,201 @@ +/* + * 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 android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_NONE; + +import android.os.IBinder; +import android.view.WindowManager.TransitionType; +import android.window.TaskFragmentOrganizer; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Responsible for managing the current {@link WindowContainerTransaction} as a response to device + * state changes and app interactions. + * + * A typical use flow: + * 1. Call {@link #startNewTransaction} to start tracking the changes. + * 2. Use {@link TransactionRecord#setOriginType(int)} (int)} to record the type of operation that + * will start a new transition on system server. + * 3. Use {@link #getCurrentTransactionRecord()} to get current {@link TransactionRecord} for + * changes. + * 4. Call {@link TransactionRecord#apply(boolean)} to request the system server to apply changes in + * the current {@link WindowContainerTransaction}, or call {@link TransactionRecord#abort()} to + * dispose the current one. + * + * Note: + * There should be only one transaction at a time. The caller should not call + * {@link #startNewTransaction} again before calling {@link TransactionRecord#apply(boolean)} or + * {@link TransactionRecord#abort()} to the previous transaction. + */ +class TransactionManager { + + @NonNull + private final TaskFragmentOrganizer mOrganizer; + + @Nullable + private TransactionRecord mCurrentTransaction; + + TransactionManager(@NonNull TaskFragmentOrganizer organizer) { + mOrganizer = organizer; + } + + @NonNull + TransactionRecord startNewTransaction() { + return startNewTransaction(null /* taskFragmentTransactionToken */); + } + + /** + * Starts tracking the changes in a new {@link WindowContainerTransaction}. Caller can call + * {@link #getCurrentTransactionRecord()} later to continue adding change to the current + * transaction until {@link TransactionRecord#apply(boolean)} or + * {@link TransactionRecord#abort()} is called. + * @param taskFragmentTransactionToken {@link android.window.TaskFragmentTransaction + * #getTransactionToken()} if this is a response to a + * {@link android.window.TaskFragmentTransaction}. + */ + @NonNull + TransactionRecord startNewTransaction(@Nullable IBinder taskFragmentTransactionToken) { + if (mCurrentTransaction != null) { + mCurrentTransaction = null; + throw new IllegalStateException( + "The previous transaction has not been applied or aborted,"); + } + mCurrentTransaction = new TransactionRecord(taskFragmentTransactionToken); + return mCurrentTransaction; + } + + /** + * Gets the current {@link TransactionRecord} started from {@link #startNewTransaction}. + */ + @NonNull + TransactionRecord getCurrentTransactionRecord() { + if (mCurrentTransaction == null) { + throw new IllegalStateException("startNewTransaction() is not invoked before calling" + + " getCurrentTransactionRecord()."); + } + return mCurrentTransaction; + } + + /** The current transaction. The manager should only handle one transaction at a time. */ + class TransactionRecord { + /** + * {@link WindowContainerTransaction} containing the current change. + * @see #startNewTransaction(IBinder) + * @see #apply (boolean) + */ + @NonNull + private final WindowContainerTransaction mTransaction = new WindowContainerTransaction(); + + /** + * If the current transaction is a response to a + * {@link android.window.TaskFragmentTransaction}, this is the + * {@link android.window.TaskFragmentTransaction#getTransactionToken()}. + * @see #startNewTransaction(IBinder) + */ + @Nullable + private final IBinder mTaskFragmentTransactionToken; + + /** + * To track of the origin type of the current {@link #mTransaction}. When + * {@link #apply (boolean)} to start a new transition, this is the type to request. + * @see #setOriginType(int) + * @see #getTransactionTransitionType() + */ + @TransitionType + private int mOriginType = TRANSIT_NONE; + + TransactionRecord(@Nullable IBinder taskFragmentTransactionToken) { + mTaskFragmentTransactionToken = taskFragmentTransactionToken; + } + + @NonNull + WindowContainerTransaction getTransaction() { + ensureCurrentTransaction(); + return mTransaction; + } + + /** + * Sets the {@link TransitionType} that triggers this transaction. If there are multiple + * calls, only the first call will be respected as the "origin" type. + */ + void setOriginType(@TransitionType int type) { + ensureCurrentTransaction(); + if (mOriginType != TRANSIT_NONE) { + // Skip if the origin type has already been set. + return; + } + mOriginType = type; + } + + /** + * Requests the system server to apply the current transaction started from + * {@link #startNewTransaction}. + * @param shouldApplyIndependently If {@code true}, the {@link #mCurrentTransaction} will + * request a new transition, which will be queued until the + * sync engine is free if there is any other active sync. + * If {@code false}, the {@link #startNewTransaction} will + * be directly applied to the active sync. + */ + void apply(boolean shouldApplyIndependently) { + ensureCurrentTransaction(); + if (mTaskFragmentTransactionToken != null) { + // If this is a response to a TaskFragmentTransaction. + mOrganizer.onTransactionHandled(mTaskFragmentTransactionToken, mTransaction, + getTransactionTransitionType(), shouldApplyIndependently); + } else { + mOrganizer.applyTransaction(mTransaction, getTransactionTransitionType(), + shouldApplyIndependently); + } + dispose(); + } + + /** Called when there is no need to {@link #apply(boolean)} the current transaction. */ + void abort() { + ensureCurrentTransaction(); + dispose(); + } + + private void dispose() { + TransactionManager.this.mCurrentTransaction = null; + } + + private void ensureCurrentTransaction() { + if (TransactionManager.this.mCurrentTransaction != this) { + throw new IllegalStateException( + "This transaction has already been apply() or abort()."); + } + } + + /** + * Gets the {@link TransitionType} that we will request transition with for the + * current {@link WindowContainerTransaction}. + */ + @VisibleForTesting + @TransitionType + int getTransactionTransitionType() { + // Use TRANSIT_CHANGE as default if there is not opening/closing window. + return mOriginType != TRANSIT_NONE ? mOriginType : TRANSIT_CHANGE; + } + } +} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 25d034756265..a40303150079 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -132,6 +132,7 @@ public class SplitControllerTest { private SplitController mSplitController; private SplitPresenter mSplitPresenter; + private TransactionManager mTransactionManager; @Before public void setUp() { @@ -140,8 +141,10 @@ public class SplitControllerTest { .getCurrentWindowLayoutInfo(anyInt(), any()); mSplitController = new SplitController(mWindowLayoutComponent); mSplitPresenter = mSplitController.mPresenter; + mTransactionManager = mSplitController.mTransactionManager; spyOn(mSplitController); spyOn(mSplitPresenter); + spyOn(mTransactionManager); doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean()); final Configuration activityConfig = new Configuration(); activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); @@ -212,6 +215,8 @@ public class SplitControllerTest { @Test public void testOnTaskFragmentAppearEmptyTimeout() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); doCallRealMethod().when(mSplitController).onTaskFragmentAppearEmptyTimeout(any(), any()); mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf); @@ -615,6 +620,8 @@ public class SplitControllerTest { @Test public void testResolveActivityToContainer_placeholderRule_notInTaskFragment() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); setupPlaceholderRule(mActivity); final SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); @@ -647,6 +654,8 @@ public class SplitControllerTest { @Test public void testResolveActivityToContainer_placeholderRule_inTopMostTaskFragment() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); setupPlaceholderRule(mActivity); final SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); @@ -679,6 +688,8 @@ public class SplitControllerTest { @Test public void testResolveActivityToContainer_placeholderRule_inSecondarySplit() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); setupPlaceholderRule(mActivity); final SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); @@ -961,6 +972,8 @@ public class SplitControllerTest { @Test public void testGetPlaceholderOptions() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); doReturn(true).when(mActivity).isResumed(); assertNull(mSplitController.getPlaceholderOptions(mActivity, false /* isOnCreated */)); @@ -1147,8 +1160,6 @@ public class SplitControllerTest { + "of other properties", SplitController.haveSamePresentation(splitRule1, splitRule2, new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); - - } /** Creates a mock activity in the organizer process. */ diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java new file mode 100644 index 000000000000..62006bd51399 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java @@ -0,0 +1,204 @@ +/* + * 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 android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.clearInvocations; + +import android.os.Binder; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.window.TaskFragmentOrganizer; +import android.window.WindowContainerTransaction; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.window.extensions.embedding.TransactionManager.TransactionRecord; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test class for {@link TransactionManager}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:TransactionManagerTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TransactionManagerTest { + + @Mock + private TaskFragmentOrganizer mOrganizer; + private TransactionManager mTransactionManager; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mTransactionManager = new TransactionManager(mOrganizer); + } + + @Test + public void testStartNewTransaction() { + mTransactionManager.startNewTransaction(); + + // Throw exception if #startNewTransaction is called twice without #apply() or #abort(). + assertThrows(IllegalStateException.class, mTransactionManager::startNewTransaction); + + // Allow to start new after #apply() the last transaction. + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + transactionRecord.apply(false /* shouldApplyIndependently */); + transactionRecord = mTransactionManager.startNewTransaction(); + + // Allow to start new after #abort() the last transaction. + transactionRecord.abort(); + mTransactionManager.startNewTransaction(); + } + + @Test + public void testSetTransactionOriginType() { + // Return TRANSIT_CHANGE if there is no trigger type set. + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + + assertEquals(TRANSIT_CHANGE, transactionRecord.getTransactionTransitionType()); + + // Return the first set type. + mTransactionManager.getCurrentTransactionRecord().abort(); + transactionRecord = mTransactionManager.startNewTransaction(); + transactionRecord.setOriginType(TRANSIT_OPEN); + + assertEquals(TRANSIT_OPEN, transactionRecord.getTransactionTransitionType()); + + transactionRecord.setOriginType(TRANSIT_CLOSE); + + assertEquals(TRANSIT_OPEN, transactionRecord.getTransactionTransitionType()); + + // Reset when #startNewTransaction(). + transactionRecord.abort(); + transactionRecord = mTransactionManager.startNewTransaction(); + + assertEquals(TRANSIT_CHANGE, transactionRecord.getTransactionTransitionType()); + } + + @Test + public void testGetCurrentTransactionRecord() { + // Throw exception if #getTransaction is called without calling #startNewTransaction(). + assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord); + + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + assertNotNull(transactionRecord); + + // Same WindowContainerTransaction should be returned. + assertSame(transactionRecord, mTransactionManager.getCurrentTransactionRecord()); + + // Reset after #abort(). + transactionRecord.abort(); + assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord); + + // New WindowContainerTransaction after #startNewTransaction(). + mTransactionManager.startNewTransaction(); + assertNotEquals(transactionRecord, mTransactionManager.getCurrentTransactionRecord()); + + // Reset after #apply(). + mTransactionManager.getCurrentTransactionRecord().apply( + false /* shouldApplyIndependently */); + assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord); + } + + @Test + public void testApply() { + // #applyTransaction(false) + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + int transitionType = transactionRecord.getTransactionTransitionType(); + WindowContainerTransaction wct = transactionRecord.getTransaction(); + transactionRecord.apply(false /* shouldApplyIndependently */); + + verify(mOrganizer).applyTransaction(wct, transitionType, + false /* shouldApplyIndependently */); + + // #applyTransaction(true) + clearInvocations(mOrganizer); + transactionRecord = mTransactionManager.startNewTransaction(); + transitionType = transactionRecord.getTransactionTransitionType(); + wct = transactionRecord.getTransaction(); + transactionRecord.apply(true /* shouldApplyIndependently */); + + verify(mOrganizer).applyTransaction(wct, transitionType, + true /* shouldApplyIndependently */); + + // #onTransactionHandled(false) + clearInvocations(mOrganizer); + IBinder token = new Binder(); + transactionRecord = mTransactionManager.startNewTransaction(token); + transitionType = transactionRecord.getTransactionTransitionType(); + wct = transactionRecord.getTransaction(); + transactionRecord.apply(false /* shouldApplyIndependently */); + + verify(mOrganizer).onTransactionHandled(token, wct, transitionType, + false /* shouldApplyIndependently */); + + // #onTransactionHandled(true) + clearInvocations(mOrganizer); + token = new Binder(); + transactionRecord = mTransactionManager.startNewTransaction(token); + transitionType = transactionRecord.getTransactionTransitionType(); + wct = transactionRecord.getTransaction(); + transactionRecord.apply(true /* shouldApplyIndependently */); + + verify(mOrganizer).onTransactionHandled(token, wct, transitionType, + true /* shouldApplyIndependently */); + + // Throw exception if there is any more interaction. + final TransactionRecord record = transactionRecord; + assertThrows(IllegalStateException.class, + () -> record.apply(false /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + () -> record.apply(true /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + record::abort); + } + + @Test + public void testAbort() { + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + transactionRecord.abort(); + + // Throw exception if there is any more interaction. + verifyNoMoreInteractions(mOrganizer); + assertThrows(IllegalStateException.class, + () -> transactionRecord.apply(false /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + () -> transactionRecord.apply(true /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + transactionRecord::abort); + } +} |