Wait for TaskFragmentOrganizer to finish handling transaction
When a TaskFragmentTransaction is sent to the organizer during a
transition, have the transition wait for the organizer to finish
handling the transaction.
Bug: 207070762
Test: atest WmTests:TaskFragmentOrganizerControllerTest
Change-Id: I4b7955171cd8ce386686ff2cd64b7c04a6436ddf
diff --git a/core/java/android/window/ITaskFragmentOrganizerController.aidl b/core/java/android/window/ITaskFragmentOrganizerController.aidl
index 8407d10..884ca77 100644
--- a/core/java/android/window/ITaskFragmentOrganizerController.aidl
+++ b/core/java/android/window/ITaskFragmentOrganizerController.aidl
@@ -16,8 +16,10 @@
package android.window;
+import android.os.IBinder;
import android.view.RemoteAnimationDefinition;
import android.window.ITaskFragmentOrganizer;
+import android.window.WindowContainerTransaction;
/** @hide */
interface ITaskFragmentOrganizerController {
@@ -46,8 +48,15 @@
void unregisterRemoteAnimations(in ITaskFragmentOrganizer organizer, int taskId);
/**
- * Checks if an activity organized by a {@link android.window.TaskFragmentOrganizer} and
- * only occupies a portion of Task bounds.
- */
+ * Checks if an activity organized by a {@link android.window.TaskFragmentOrganizer} and
+ * only occupies a portion of Task bounds.
+ */
boolean isActivityEmbedded(in IBinder activityToken);
+
+ /**
+ * Notifies the server that the organizer has finished handling the given transaction. The
+ * server should apply the given {@link WindowContainerTransaction} for the necessary changes.
+ */
+ void onTransactionHandled(in ITaskFragmentOrganizer organizer, in IBinder transactionToken,
+ in WindowContainerTransaction wct);
}
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index c43cf55..7b6139f 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -26,7 +26,6 @@
import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.content.Intent;
import android.content.res.Configuration;
@@ -149,6 +148,28 @@
}
/**
+ * Notifies the server that the organizer has finished handling the given transaction. The
+ * server should apply the given {@link WindowContainerTransaction} for the necessary changes.
+ *
+ * @param transactionToken {@link TaskFragmentTransaction#getTransactionToken()} from
+ * {@link #onTransactionReady(TaskFragmentTransaction)}
+ * @param wct {@link WindowContainerTransaction} that the server should apply for
+ * update of the transaction.
+ * @see com.android.server.wm.WindowOrganizerController#enforceTaskPermission for permission
+ * requirement.
+ * @hide
+ */
+ public void onTransactionHandled(@NonNull IBinder transactionToken,
+ @NonNull WindowContainerTransaction wct) {
+ wct.setTaskFragmentOrganizer(mInterface);
+ try {
+ getController().onTransactionHandled(mInterface, transactionToken, wct);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Called when a TaskFragment is created and organized by this organizer.
*
* @param taskFragmentInfo Info of the TaskFragment that is created.
@@ -318,12 +339,8 @@
/**
* Called when the transaction is ready so that the organizer can update the TaskFragments based
* on the changes in transaction.
- * Note: {@link WindowOrganizer#applyTransaction} permission requirement is conditional for
- * {@link TaskFragmentOrganizer}.
- * @see com.android.server.wm.WindowOrganizerController#enforceTaskPermission
* @hide
*/
- @SuppressLint("AndroidFrameworkRequiresPermission")
public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
@@ -389,8 +406,9 @@
"Unknown TaskFragmentEvent=" + change.getType());
}
}
- // TODO(b/240519866): notify TaskFragmentOrganizerController that the transition is done.
- applyTransaction(wct);
+
+ // Notify the server, and the server should apply the WindowContainerTransaction.
+ onTransactionHandled(transaction.getTransactionToken(), wct);
}
@Override
diff --git a/core/java/android/window/TaskFragmentTransaction.java b/core/java/android/window/TaskFragmentTransaction.java
index 07e8e8c..84a5fea 100644
--- a/core/java/android/window/TaskFragmentTransaction.java
+++ b/core/java/android/window/TaskFragmentTransaction.java
@@ -23,6 +23,7 @@
import android.annotation.Nullable;
import android.content.Intent;
import android.content.res.Configuration;
+import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
@@ -41,19 +42,31 @@
*/
public final class TaskFragmentTransaction implements Parcelable {
+ /** Unique token to represent this transaction. */
+ private final IBinder mTransactionToken;
+
+ /** Changes in this transaction. */
private final ArrayList<Change> mChanges = new ArrayList<>();
- public TaskFragmentTransaction() {}
+ public TaskFragmentTransaction() {
+ mTransactionToken = new Binder();
+ }
private TaskFragmentTransaction(Parcel in) {
+ mTransactionToken = in.readStrongBinder();
in.readTypedList(mChanges, Change.CREATOR);
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeStrongBinder(mTransactionToken);
dest.writeTypedList(mChanges);
}
+ public IBinder getTransactionToken() {
+ return mTransactionToken;
+ }
+
/** Adds a {@link Change} to this transaction. */
public void addChange(@Nullable Change change) {
if (change != null) {
@@ -74,7 +87,9 @@
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
- sb.append("TaskFragmentTransaction{changes=[");
+ sb.append("TaskFragmentTransaction{token=");
+ sb.append(mTransactionToken);
+ sb.append(" changes=[");
for (int i = 0; i < mChanges.size(); ++i) {
if (i > 0) {
sb.append(',');
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index e8873cd..49767844 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -2041,6 +2041,12 @@
"group": "WM_DEBUG_CONFIGURATION",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
+ "-108248992": {
+ "message": "Defer transition ready for TaskFragmentTransaction=%s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+ },
"-106400104": {
"message": "Preload recents with %s",
"level": "DEBUG",
@@ -2089,6 +2095,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/TaskFragment.java"
},
+ "-79016993": {
+ "message": "Continue transition ready for TaskFragmentTransaction=%s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+ },
"-70719599": {
"message": "Unregister remote animations for organizer=%s uid=%d pid=%d",
"level": "VERBOSE",
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 88059e1..d615583 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -49,7 +49,9 @@
import android.window.ITaskFragmentOrganizerController;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentTransaction;
+import android.window.WindowContainerTransaction;
+import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.common.ProtoLog;
import java.lang.annotation.Retention;
@@ -68,6 +70,8 @@
private final ActivityTaskManagerService mAtmService;
private final WindowManagerGlobalLock mGlobalLock;
+ private final WindowOrganizerController mWindowOrganizerController;
+
/**
* A Map which manages the relationship between
* {@link ITaskFragmentOrganizer} and {@link TaskFragmentOrganizerState}
@@ -82,9 +86,11 @@
private final ArraySet<Task> mTmpTaskSet = new ArraySet<>();
- TaskFragmentOrganizerController(ActivityTaskManagerService atm) {
- mAtmService = atm;
+ TaskFragmentOrganizerController(@NonNull ActivityTaskManagerService atm,
+ @NonNull WindowOrganizerController windowOrganizerController) {
+ mAtmService = requireNonNull(atm);
mGlobalLock = atm.mGlobalLock;
+ mWindowOrganizerController = requireNonNull(windowOrganizerController);
}
/**
@@ -131,6 +137,14 @@
private final SparseArray<RemoteAnimationDefinition> mRemoteAnimationDefinitions =
new SparseArray<>();
+ /**
+ * List of {@link TaskFragmentTransaction#getTransactionToken()} that have been sent to the
+ * organizer. If the transaction is sent during a transition, the
+ * {@link TransitionController} will wait until the transaction is finished.
+ * @see #onTransactionFinished(IBinder)
+ */
+ private final List<IBinder> mRunningTransactions = new ArrayList<>();
+
TaskFragmentOrganizerState(ITaskFragmentOrganizer organizer, int pid, int uid) {
mOrganizer = organizer;
mOrganizerPid = pid;
@@ -176,6 +190,10 @@
taskFragment.removeImmediately();
mOrganizedTaskFragments.remove(taskFragment);
}
+ for (int i = mRunningTransactions.size() - 1; i >= 0; i--) {
+ // Cleanup any running transaction to unblock the current transition.
+ onTransactionFinished(mRunningTransactions.get(i));
+ }
mOrganizer.asBinder().unlinkToDeath(this, 0 /*flags*/);
}
@@ -320,6 +338,40 @@
.setActivityIntent(activity.intent)
.setActivityToken(activityToken);
}
+
+ void dispatchTransaction(@NonNull TaskFragmentTransaction transaction) {
+ if (transaction.isEmpty()) {
+ return;
+ }
+ try {
+ mOrganizer.onTransactionReady(transaction);
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Exception sending TaskFragmentTransaction", e);
+ return;
+ }
+ onTransactionStarted(transaction.getTransactionToken());
+ }
+
+ /** Called when the transaction is sent to the organizer. */
+ void onTransactionStarted(@NonNull IBinder transactionToken) {
+ if (!mWindowOrganizerController.getTransitionController().isCollecting()) {
+ return;
+ }
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+ "Defer transition ready for TaskFragmentTransaction=%s", transactionToken);
+ mRunningTransactions.add(transactionToken);
+ mWindowOrganizerController.getTransitionController().deferTransitionReady();
+ }
+
+ /** Called when the transaction is finished. */
+ void onTransactionFinished(@NonNull IBinder transactionToken) {
+ if (!mRunningTransactions.remove(transactionToken)) {
+ return;
+ }
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+ "Continue transition ready for TaskFragmentTransaction=%s", transactionToken);
+ mWindowOrganizerController.getTransitionController().continueTransitionReady();
+ }
}
@Nullable
@@ -336,7 +388,7 @@
}
@Override
- public void registerOrganizer(ITaskFragmentOrganizer organizer) {
+ public void registerOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
final int pid = Binder.getCallingPid();
final int uid = Binder.getCallingUid();
synchronized (mGlobalLock) {
@@ -354,7 +406,7 @@
}
@Override
- public void unregisterOrganizer(ITaskFragmentOrganizer organizer) {
+ public void unregisterOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
validateAndGetState(organizer);
final int pid = Binder.getCallingPid();
final long uid = Binder.getCallingUid();
@@ -372,8 +424,8 @@
}
@Override
- public void registerRemoteAnimations(ITaskFragmentOrganizer organizer, int taskId,
- RemoteAnimationDefinition definition) {
+ public void registerRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer, int taskId,
+ @NonNull RemoteAnimationDefinition definition) {
final int pid = Binder.getCallingPid();
final int uid = Binder.getCallingUid();
synchronized (mGlobalLock) {
@@ -398,7 +450,7 @@
}
@Override
- public void unregisterRemoteAnimations(ITaskFragmentOrganizer organizer, int taskId) {
+ public void unregisterRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer, int taskId) {
final int pid = Binder.getCallingPid();
final long uid = Binder.getCallingUid();
synchronized (mGlobalLock) {
@@ -416,6 +468,17 @@
}
}
+ @Override
+ public void onTransactionHandled(@NonNull ITaskFragmentOrganizer organizer,
+ @NonNull IBinder transactionToken, @NonNull WindowContainerTransaction wct) {
+ synchronized (mGlobalLock) {
+ // Keep the calling identity to avoid unsecure change.
+ mWindowOrganizerController.applyTransaction(wct);
+ final TaskFragmentOrganizerState state = validateAndGetState(organizer);
+ state.onTransactionFinished(transactionToken);
+ }
+ }
+
/**
* Gets the {@link RemoteAnimationDefinition} set on the given organizer if exists. Returns
* {@code null} if it doesn't, or if the organizer has activity(ies) embedded in untrusted mode.
@@ -775,13 +838,13 @@
}
final int organizerNum = mPendingTaskFragmentEvents.size();
for (int i = 0; i < organizerNum; i++) {
- final ITaskFragmentOrganizer organizer = mTaskFragmentOrganizerState.get(
- mPendingTaskFragmentEvents.keyAt(i)).mOrganizer;
- dispatchPendingEvents(organizer, mPendingTaskFragmentEvents.valueAt(i));
+ final TaskFragmentOrganizerState state =
+ mTaskFragmentOrganizerState.get(mPendingTaskFragmentEvents.keyAt(i));
+ dispatchPendingEvents(state, mPendingTaskFragmentEvents.valueAt(i));
}
}
- void dispatchPendingEvents(@NonNull ITaskFragmentOrganizer organizer,
+ void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state,
@NonNull List<PendingTaskFragmentEvent> pendingEvents) {
if (pendingEvents.isEmpty()) {
return;
@@ -817,7 +880,7 @@
if (mTmpTaskSet.add(task)) {
// Make sure the organizer know about the Task config.
transaction.addChange(prepareChange(new PendingTaskFragmentEvent.Builder(
- PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED, organizer)
+ PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED, state.mOrganizer)
.setTask(task)
.build()));
}
@@ -825,7 +888,7 @@
transaction.addChange(prepareChange(event));
}
mTmpTaskSet.clear();
- dispatchTransactionInfo(organizer, transaction);
+ state.dispatchTransaction(transaction);
pendingEvents.removeAll(candidateEvents);
}
@@ -855,6 +918,7 @@
}
final ITaskFragmentOrganizer organizer = taskFragment.getTaskFragmentOrganizer();
+ final TaskFragmentOrganizerState state = validateAndGetState(organizer);
final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
// Make sure the organizer know about the Task config.
transaction.addChange(prepareChange(new PendingTaskFragmentEvent.Builder(
@@ -862,22 +926,10 @@
.setTask(taskFragment.getTask())
.build()));
transaction.addChange(prepareChange(event));
- dispatchTransactionInfo(event.mTaskFragmentOrg, transaction);
+ state.dispatchTransaction(transaction);
mPendingTaskFragmentEvents.get(organizer.asBinder()).remove(event);
}
- private void dispatchTransactionInfo(@NonNull ITaskFragmentOrganizer organizer,
- @NonNull TaskFragmentTransaction transaction) {
- if (transaction.isEmpty()) {
- return;
- }
- try {
- organizer.onTransactionReady(transaction);
- } catch (RemoteException e) {
- Slog.d(TAG, "Exception sending TaskFragmentTransaction", e);
- }
- }
-
@Nullable
private TaskFragmentTransaction.Change prepareChange(
@NonNull PendingTaskFragmentEvent event) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index fa2ab31..fd6f8f1 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1802,6 +1802,8 @@
/** This undoes one call to {@link #deferTransitionReady}. */
void continueTransitionReady() {
--mReadyTracker.mDeferReadyDepth;
+ // Apply ready in case it is waiting for the previous defer call.
+ applyReady();
}
/**
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 3b9cd36..c9e0629 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -147,7 +147,7 @@
mGlobalLock = atm.mGlobalLock;
mTaskOrganizerController = new TaskOrganizerController(mService);
mDisplayAreaOrganizerController = new DisplayAreaOrganizerController(mService);
- mTaskFragmentOrganizerController = new TaskFragmentOrganizerController(atm);
+ mTaskFragmentOrganizerController = new TaskFragmentOrganizerController(atm, this);
}
void setWindowManager(WindowManagerService wms) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index da72030..9274eb3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -25,6 +25,7 @@
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
@@ -46,7 +47,6 @@
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -65,6 +65,7 @@
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentOrganizer;
import android.window.TaskFragmentOrganizerToken;
+import android.window.TaskFragmentTransaction;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import android.window.WindowContainerTransactionCallback;
@@ -90,6 +91,7 @@
private TaskFragmentOrganizerController mController;
private WindowOrganizerController mWindowOrganizerController;
+ private TransitionController mTransitionController;
private TaskFragmentOrganizer mOrganizer;
private TaskFragmentOrganizerToken mOrganizerToken;
private ITaskFragmentOrganizer mIOrganizer;
@@ -107,9 +109,10 @@
private Task mTask;
@Before
- public void setup() {
+ public void setup() throws RemoteException {
MockitoAnnotations.initMocks(this);
mWindowOrganizerController = mAtm.mWindowOrganizerController;
+ mTransitionController = mWindowOrganizerController.mTransitionController;
mController = mWindowOrganizerController.mTaskFragmentOrganizerController;
mOrganizer = new TaskFragmentOrganizer(Runnable::run);
mOrganizerToken = mOrganizer.getOrganizerToken();
@@ -128,11 +131,16 @@
spyOn(mController);
spyOn(mOrganizer);
spyOn(mTaskFragment);
+ spyOn(mWindowOrganizerController);
+ spyOn(mTransitionController);
doReturn(mIOrganizer).when(mTaskFragment).getTaskFragmentOrganizer();
doReturn(mTaskFragmentInfo).when(mTaskFragment).getTaskFragmentInfo();
doReturn(new SurfaceControl()).when(mTaskFragment).getSurfaceControl();
doReturn(mFragmentToken).when(mTaskFragment).getFragmentToken();
doReturn(new Configuration()).when(mTaskFragmentInfo).getConfiguration();
+
+ // To prevent it from calling the real server.
+ doNothing().when(mOrganizer).onTransactionHandled(any(), any());
}
@Test
@@ -866,7 +874,7 @@
assertFalse(parentTask.shouldBeVisible(null));
// Verify the info changed callback still occurred despite the task being invisible
- reset(mOrganizer);
+ clearInvocations(mOrganizer);
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
@@ -899,7 +907,7 @@
verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
// Verify the info changed callback is not called when the task is invisible
- reset(mOrganizer);
+ clearInvocations(mOrganizer);
doReturn(false).when(task).shouldBeVisible(any());
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
@@ -1092,6 +1100,40 @@
.that(mTaskFragment.getBounds()).isEqualTo(task.getBounds());
}
+ @Test
+ public void testOnTransactionReady_invokeOnTransactionHandled() {
+ mController.registerOrganizer(mIOrganizer);
+ final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
+ mOrganizer.onTransactionReady(transaction);
+
+ // Organizer should always trigger #onTransactionHandled when receives #onTransactionReady
+ verify(mOrganizer).onTransactionHandled(eq(transaction.getTransactionToken()), any());
+ verify(mOrganizer, never()).applyTransaction(any());
+ }
+
+ @Test
+ public void testDispatchTransaction_deferTransitionReady() {
+ mController.registerOrganizer(mIOrganizer);
+ setupMockParent(mTaskFragment, mTask);
+ final ArgumentCaptor<IBinder> tokenCaptor = ArgumentCaptor.forClass(IBinder.class);
+ final ArgumentCaptor<WindowContainerTransaction> wctCaptor =
+ ArgumentCaptor.forClass(WindowContainerTransaction.class);
+ doReturn(true).when(mTransitionController).isCollecting();
+
+ mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
+ mController.dispatchPendingEvents();
+
+ // Defer transition when send TaskFragment transaction during transition collection.
+ verify(mTransitionController).deferTransitionReady();
+ verify(mOrganizer).onTransactionHandled(tokenCaptor.capture(), wctCaptor.capture());
+
+ mController.onTransactionHandled(mIOrganizer, tokenCaptor.getValue(), wctCaptor.getValue());
+
+ // Apply the organizer change and continue transition.
+ verify(mWindowOrganizerController).applyTransaction(wctCaptor.getValue());
+ verify(mTransitionController).continueTransitionReady();
+ }
+
/**
* Creates a {@link TaskFragment} with the {@link WindowContainerTransaction}. Calls
* {@link WindowOrganizerController#applyTransaction} to apply the transaction,