summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Chris Li <lihongyu@google.com> 2023-10-12 15:27:39 +0800
committer Chris Li <lihongyu@google.com> 2023-10-18 22:42:05 +0000
commitdef520df682bfb9e54137814b6f5f03393f93ef4 (patch)
tree9d8275a122c1e189a0397bea51ec4d143354dd28
parent36d818fbf268a2c798d89ccd63f1e66bdcde1e6a (diff)
Synchronize window config updates (7/n)
Allow ClientTransaction to take more than one ActivityLifecycleItem. To keep the client side behavior unchanged, this CL refactors to allow ClientTransaction to take lifecycle request in the same way as non-lifecyle request, so that they are queued in order. As a refactor, this should be no-op until replacing the server side deprecated methods to use #addTransactionItem. Bug: 260873529 Test: atest FrameworksCoreTests:TransactionExecutorTests Change-Id: Ida33994bc78673a548c137be50a51daed4e873db
-rw-r--r--core/java/android/app/servertransaction/ActivityLifecycleItem.java5
-rw-r--r--core/java/android/app/servertransaction/ClientTransaction.java105
-rw-r--r--core/java/android/app/servertransaction/ClientTransactionItem.java7
-rw-r--r--core/java/android/app/servertransaction/TransactionExecutor.java169
-rw-r--r--core/java/android/app/servertransaction/TransactionExecutorHelper.java79
-rw-r--r--core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java20
-rw-r--r--core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java110
-rw-r--r--core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java4
8 files changed, 419 insertions, 80 deletions
diff --git a/core/java/android/app/servertransaction/ActivityLifecycleItem.java b/core/java/android/app/servertransaction/ActivityLifecycleItem.java
index b34f6788fb60..06bff5df490a 100644
--- a/core/java/android/app/servertransaction/ActivityLifecycleItem.java
+++ b/core/java/android/app/servertransaction/ActivityLifecycleItem.java
@@ -58,6 +58,11 @@ public abstract class ActivityLifecycleItem extends ActivityTransactionItem {
super(in);
}
+ @Override
+ boolean isActivityLifecycleItem() {
+ return true;
+ }
+
/** A final lifecycle state that an activity should reach. */
@LifecycleState
public abstract int getTargetState();
diff --git a/core/java/android/app/servertransaction/ClientTransaction.java b/core/java/android/app/servertransaction/ClientTransaction.java
index 8617386516af..f7f901b26646 100644
--- a/core/java/android/app/servertransaction/ClientTransaction.java
+++ b/core/java/android/app/servertransaction/ClientTransaction.java
@@ -45,6 +45,13 @@ import java.util.Objects;
*/
public class ClientTransaction implements Parcelable, ObjectPoolItem {
+ /**
+ * List of transaction items that should be executed in order. Including both
+ * {@link ActivityLifecycleItem} and other {@link ClientTransactionItem}.
+ */
+ @Nullable
+ private List<ClientTransactionItem> mTransactionItems;
+
/** A list of individual callbacks to a client. */
@UnsupportedAppUsage
private List<ClientTransactionItem> mActivityCallbacks;
@@ -64,9 +71,32 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem {
}
/**
- * Add a message to the end of the sequence of callbacks.
+ * Adds a message to the end of the sequence of transaction items.
+ * @param item A single message that can contain a client activity/window request/callback.
+ * TODO(b/260873529): replace both {@link #addCallback} and {@link #setLifecycleStateRequest}.
+ */
+ public void addTransactionItem(@NonNull ClientTransactionItem item) {
+ if (mTransactionItems == null) {
+ mTransactionItems = new ArrayList<>();
+ }
+ mTransactionItems.add(item);
+ }
+
+ /**
+ * Gets the list of client window requests/callbacks.
+ * TODO(b/260873529): must be non null after remove the deprecated methods.
+ */
+ @Nullable
+ public List<ClientTransactionItem> getTransactionItems() {
+ return mTransactionItems;
+ }
+
+ /**
+ * Adds a message to the end of the sequence of callbacks.
* @param activityCallback A single message that can contain a lifecycle request/callback.
+ * @deprecated use {@link #addTransactionItem(ClientTransactionItem)} instead.
*/
+ @Deprecated
public void addCallback(@NonNull ClientTransactionItem activityCallback) {
if (mActivityCallbacks == null) {
mActivityCallbacks = new ArrayList<>();
@@ -74,25 +104,35 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem {
mActivityCallbacks.add(activityCallback);
}
- /** Get the list of callbacks. */
+ /**
+ * Gets the list of callbacks.
+ * @deprecated use {@link #getTransactionItems()} instead.
+ */
@Nullable
@VisibleForTesting
@UnsupportedAppUsage
+ @Deprecated
public List<ClientTransactionItem> getCallbacks() {
return mActivityCallbacks;
}
- /** Get the target state lifecycle request. */
+ /**
+ * Gets the target state lifecycle request.
+ * @deprecated use {@link #getTransactionItems()} instead.
+ */
@VisibleForTesting(visibility = PACKAGE)
@UnsupportedAppUsage
+ @Deprecated
public ActivityLifecycleItem getLifecycleStateRequest() {
return mLifecycleStateRequest;
}
/**
- * Set the lifecycle state in which the client should be after executing the transaction.
+ * Sets the lifecycle state in which the client should be after executing the transaction.
* @param stateRequest A lifecycle request initialized with right parameters.
+ * @deprecated use {@link #addTransactionItem(ClientTransactionItem)} instead.
*/
+ @Deprecated
public void setLifecycleStateRequest(@NonNull ActivityLifecycleItem stateRequest) {
mLifecycleStateRequest = stateRequest;
}
@@ -103,6 +143,14 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem {
* requested by transaction items.
*/
public void preExecute(@NonNull ClientTransactionHandler clientTransactionHandler) {
+ if (mTransactionItems != null) {
+ final int size = mTransactionItems.size();
+ for (int i = 0; i < size; ++i) {
+ mTransactionItems.get(i).preExecute(clientTransactionHandler);
+ }
+ return;
+ }
+
if (mActivityCallbacks != null) {
final int size = mActivityCallbacks.size();
for (int i = 0; i < size; ++i) {
@@ -147,12 +195,19 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem {
@Override
public void recycle() {
+ if (mTransactionItems != null) {
+ int size = mTransactionItems.size();
+ for (int i = 0; i < size; i++) {
+ mTransactionItems.get(i).recycle();
+ }
+ mTransactionItems = null;
+ }
if (mActivityCallbacks != null) {
int size = mActivityCallbacks.size();
for (int i = 0; i < size; i++) {
mActivityCallbacks.get(i).recycle();
}
- mActivityCallbacks.clear();
+ mActivityCallbacks = null;
}
if (mLifecycleStateRequest != null) {
mLifecycleStateRequest.recycle();
@@ -165,8 +220,15 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem {
// Parcelable implementation
/** Write to Parcel. */
+ @SuppressWarnings("AndroidFrameworkEfficientParcelable") // Item class is not final.
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
+ final boolean writeTransactionItems = mTransactionItems != null;
+ dest.writeBoolean(writeTransactionItems);
+ if (writeTransactionItems) {
+ dest.writeParcelableList(mTransactionItems, flags);
+ }
+
dest.writeParcelable(mLifecycleStateRequest, flags);
final boolean writeActivityCallbacks = mActivityCallbacks != null;
dest.writeBoolean(writeActivityCallbacks);
@@ -177,11 +239,20 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem {
/** Read from Parcel. */
private ClientTransaction(@NonNull Parcel in) {
- mLifecycleStateRequest = in.readParcelable(getClass().getClassLoader(), android.app.servertransaction.ActivityLifecycleItem.class);
+ final boolean readTransactionItems = in.readBoolean();
+ if (readTransactionItems) {
+ mTransactionItems = new ArrayList<>();
+ in.readParcelableList(mTransactionItems, getClass().getClassLoader(),
+ ClientTransactionItem.class);
+ }
+
+ mLifecycleStateRequest = in.readParcelable(getClass().getClassLoader(),
+ ActivityLifecycleItem.class);
final boolean readActivityCallbacks = in.readBoolean();
if (readActivityCallbacks) {
mActivityCallbacks = new ArrayList<>();
- in.readParcelableList(mActivityCallbacks, getClass().getClassLoader(), android.app.servertransaction.ClientTransactionItem.class);
+ in.readParcelableList(mActivityCallbacks, getClass().getClassLoader(),
+ ClientTransactionItem.class);
}
}
@@ -209,7 +280,8 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem {
return false;
}
final ClientTransaction other = (ClientTransaction) o;
- return Objects.equals(mActivityCallbacks, other.mActivityCallbacks)
+ return Objects.equals(mTransactionItems, other.mTransactionItems)
+ && Objects.equals(mActivityCallbacks, other.mActivityCallbacks)
&& Objects.equals(mLifecycleStateRequest, other.mLifecycleStateRequest)
&& mClient == other.mClient;
}
@@ -217,6 +289,7 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem {
@Override
public int hashCode() {
int result = 17;
+ result = 31 * result + Objects.hashCode(mTransactionItems);
result = 31 * result + Objects.hashCode(mActivityCallbacks);
result = 31 * result + Objects.hashCode(mLifecycleStateRequest);
result = 31 * result + Objects.hashCode(mClient);
@@ -227,6 +300,22 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem {
void dump(@NonNull String prefix, @NonNull PrintWriter pw,
@NonNull ClientTransactionHandler transactionHandler) {
pw.append(prefix).println("ClientTransaction{");
+ if (mTransactionItems != null) {
+ pw.append(prefix).print(" transactionItems=[");
+ final String itemPrefix = prefix + " ";
+ final int size = mTransactionItems.size();
+ if (size > 0) {
+ pw.println();
+ for (int i = 0; i < size; i++) {
+ mTransactionItems.get(i).dump(itemPrefix, pw, transactionHandler);
+ }
+ pw.append(prefix).println(" ]");
+ } else {
+ pw.println("]");
+ }
+ pw.append(prefix).println("}");
+ return;
+ }
pw.append(prefix).print(" callbacks=[");
final String itemPrefix = prefix + " ";
final int size = mActivityCallbacks != null ? mActivityCallbacks.size() : 0;
diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java
index 07e5a7dc5f02..f94e22de06e5 100644
--- a/core/java/android/app/servertransaction/ClientTransactionItem.java
+++ b/core/java/android/app/servertransaction/ClientTransactionItem.java
@@ -72,6 +72,13 @@ public abstract class ClientTransactionItem implements BaseClientRequest, Parcel
return null;
}
+ /**
+ * Whether this is a {@link ActivityLifecycleItem}.
+ */
+ boolean isActivityLifecycleItem() {
+ return false;
+ }
+
/** Dumps this transaction item. */
void dump(@NonNull String prefix, @NonNull PrintWriter pw,
@NonNull ClientTransactionHandler transactionHandler) {
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index 066f9fe84970..9f5e0dc14cca 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -28,6 +28,7 @@ import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED;
import static android.app.servertransaction.TransactionExecutorHelper.getShortActivityName;
import static android.app.servertransaction.TransactionExecutorHelper.getStateName;
import static android.app.servertransaction.TransactionExecutorHelper.lastCallbackRequestingState;
+import static android.app.servertransaction.TransactionExecutorHelper.shouldExcludeLastLifecycleState;
import static android.app.servertransaction.TransactionExecutorHelper.tId;
import static android.app.servertransaction.TransactionExecutorHelper.transactionToString;
@@ -61,6 +62,9 @@ public class TransactionExecutor {
private final PendingTransactionActions mPendingActions = new PendingTransactionActions();
private final TransactionExecutorHelper mHelper = new TransactionExecutorHelper();
+ /** Keeps track of display ids whose Configuration got updated within a transaction. */
+ private final ArraySet<Integer> mConfigUpdatedDisplayIds = new ArraySet<>();
+
/** Initialize an instance with transaction handler, that will execute all requested actions. */
public TransactionExecutor(@NonNull ClientTransactionHandler clientTransactionHandler) {
mTransactionHandler = clientTransactionHandler;
@@ -79,15 +83,52 @@ public class TransactionExecutor {
Slog.d(TAG, transactionToString(transaction, mTransactionHandler));
}
- executeCallbacks(transaction);
- executeLifecycleState(transaction);
+ if (transaction.getTransactionItems() != null) {
+ executeTransactionItems(transaction);
+ } else {
+ // TODO(b/260873529): cleanup after launch.
+ executeCallbacks(transaction);
+ executeLifecycleState(transaction);
+ }
+
+ if (!mConfigUpdatedDisplayIds.isEmpty()) {
+ // Whether this transaction should trigger DisplayListener#onDisplayChanged.
+ final ClientTransactionListenerController controller =
+ ClientTransactionListenerController.getInstance();
+ final int displayCount = mConfigUpdatedDisplayIds.size();
+ for (int i = 0; i < displayCount; i++) {
+ final int displayId = mConfigUpdatedDisplayIds.valueAt(i);
+ controller.onDisplayChanged(displayId);
+ }
+ mConfigUpdatedDisplayIds.clear();
+ }
mPendingActions.clear();
if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "End resolving transaction");
}
- /** Cycle through all states requested by callbacks and execute them at proper times. */
+ /** Cycles through all transaction items and execute them at proper times. */
@VisibleForTesting
+ public void executeTransactionItems(@NonNull ClientTransaction transaction) {
+ final List<ClientTransactionItem> items = transaction.getTransactionItems();
+ final int size = items.size();
+ for (int i = 0; i < size; i++) {
+ final ClientTransactionItem item = items.get(i);
+ if (item.isActivityLifecycleItem()) {
+ executeLifecycleItem(transaction, (ActivityLifecycleItem) item);
+ } else {
+ executeNonLifecycleItem(transaction, item,
+ shouldExcludeLastLifecycleState(items, i));
+ }
+ }
+ }
+
+ /**
+ * Cycle through all states requested by callbacks and execute them at proper times.
+ * @deprecated use {@link #executeTransactionItems} instead.
+ */
+ @VisibleForTesting
+ @Deprecated
public void executeCallbacks(@NonNull ClientTransaction transaction) {
final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
if (callbacks == null || callbacks.isEmpty()) {
@@ -105,83 +146,78 @@ public class TransactionExecutor {
// Index of the last callback that requests some post-execution state.
final int lastCallbackRequestingState = lastCallbackRequestingState(transaction);
- // Keep track of display ids whose Configuration got updated with this transaction.
- ArraySet<Integer> configUpdatedDisplays = null;
-
final int size = callbacks.size();
for (int i = 0; i < size; ++i) {
final ClientTransactionItem item = callbacks.get(i);
- final IBinder token = item.getActivityToken();
- ActivityClientRecord r = mTransactionHandler.getActivityClient(token);
-
- if (token != null && r == null
- && mTransactionHandler.getActivitiesToBeDestroyed().containsKey(token)) {
- // The activity has not been created but has been requested to destroy, so all
- // transactions for the token are just like being cancelled.
- Slog.w(TAG, "Skip pre-destroyed transaction item:\n" + item);
- continue;
- }
- if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item);
+ // Skip the very last transition and perform it by explicit state request instead.
final int postExecutionState = item.getPostExecutionState();
+ final boolean shouldExcludeLastLifecycleState = postExecutionState != UNDEFINED
+ && i == lastCallbackRequestingState && finalState == postExecutionState;
+ executeNonLifecycleItem(transaction, item, shouldExcludeLastLifecycleState);
+ }
+ }
- if (item.shouldHaveDefinedPreExecutionState()) {
- final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
- item.getPostExecutionState());
- if (closestPreExecutionState != UNDEFINED) {
- cycleToPath(r, closestPreExecutionState, transaction);
- }
- }
+ private void executeNonLifecycleItem(@NonNull ClientTransaction transaction,
+ @NonNull ClientTransactionItem item, boolean shouldExcludeLastLifecycleState) {
+ final IBinder token = item.getActivityToken();
+ ActivityClientRecord r = mTransactionHandler.getActivityClient(token);
- // Can't read flag from isolated process.
- final boolean isSyncWindowConfigUpdateFlagEnabled = !Process.isIsolated()
- && syncWindowConfigUpdateFlag();
- final Context configUpdatedContext = isSyncWindowConfigUpdateFlagEnabled
- ? item.getContextToUpdate(mTransactionHandler)
- : null;
- final Configuration preExecutedConfig = configUpdatedContext != null
- ? new Configuration(configUpdatedContext.getResources().getConfiguration())
- : null;
-
- item.execute(mTransactionHandler, mPendingActions);
-
- if (configUpdatedContext != null) {
- final Configuration postExecutedConfig = configUpdatedContext.getResources()
- .getConfiguration();
- if (!areConfigurationsEqualForDisplay(postExecutedConfig, preExecutedConfig)) {
- if (configUpdatedDisplays == null) {
- configUpdatedDisplays = new ArraySet<>();
- }
- configUpdatedDisplays.add(configUpdatedContext.getDisplayId());
- }
- }
+ if (token != null && r == null
+ && mTransactionHandler.getActivitiesToBeDestroyed().containsKey(token)) {
+ // The activity has not been created but has been requested to destroy, so all
+ // transactions for the token are just like being cancelled.
+ Slog.w(TAG, "Skip pre-destroyed transaction item:\n" + item);
+ return;
+ }
- item.postExecute(mTransactionHandler, mPendingActions);
- if (r == null) {
- // Launch activity request will create an activity record.
- r = mTransactionHandler.getActivityClient(token);
- }
+ if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item);
+ final int postExecutionState = item.getPostExecutionState();
- if (postExecutionState != UNDEFINED && r != null) {
- // Skip the very last transition and perform it by explicit state request instead.
- final boolean shouldExcludeLastTransition =
- i == lastCallbackRequestingState && finalState == postExecutionState;
- cycleToPath(r, postExecutionState, shouldExcludeLastTransition, transaction);
+ if (item.shouldHaveDefinedPreExecutionState()) {
+ final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
+ postExecutionState);
+ if (closestPreExecutionState != UNDEFINED) {
+ cycleToPath(r, closestPreExecutionState, transaction);
}
}
- if (configUpdatedDisplays != null) {
- final ClientTransactionListenerController controller =
- ClientTransactionListenerController.getInstance();
- final int displayCount = configUpdatedDisplays.size();
- for (int i = 0; i < displayCount; i++) {
- final int displayId = configUpdatedDisplays.valueAt(i);
- controller.onDisplayChanged(displayId);
+ // Can't read flag from isolated process.
+ final boolean isSyncWindowConfigUpdateFlagEnabled = !Process.isIsolated()
+ && syncWindowConfigUpdateFlag();
+ final Context configUpdatedContext = isSyncWindowConfigUpdateFlagEnabled
+ ? item.getContextToUpdate(mTransactionHandler)
+ : null;
+ final Configuration preExecutedConfig = configUpdatedContext != null
+ ? new Configuration(configUpdatedContext.getResources().getConfiguration())
+ : null;
+
+ item.execute(mTransactionHandler, mPendingActions);
+
+ if (configUpdatedContext != null) {
+ final Configuration postExecutedConfig = configUpdatedContext.getResources()
+ .getConfiguration();
+ if (!areConfigurationsEqualForDisplay(postExecutedConfig, preExecutedConfig)) {
+ mConfigUpdatedDisplayIds.add(configUpdatedContext.getDisplayId());
}
}
+
+ item.postExecute(mTransactionHandler, mPendingActions);
+ if (r == null) {
+ // Launch activity request will create an activity record.
+ r = mTransactionHandler.getActivityClient(token);
+ }
+
+ if (postExecutionState != UNDEFINED && r != null) {
+ cycleToPath(r, postExecutionState, shouldExcludeLastLifecycleState, transaction);
+ }
}
- /** Transition to the final state if requested by the transaction. */
+ /**
+ * Transition to the final state if requested by the transaction.
+ * @deprecated use {@link #executeTransactionItems} instead
+ */
+ @Deprecated
private void executeLifecycleState(@NonNull ClientTransaction transaction) {
final ActivityLifecycleItem lifecycleItem = transaction.getLifecycleStateRequest();
if (lifecycleItem == null) {
@@ -189,6 +225,11 @@ public class TransactionExecutor {
return;
}
+ executeLifecycleItem(transaction, lifecycleItem);
+ }
+
+ private void executeLifecycleItem(@NonNull ClientTransaction transaction,
+ @NonNull ActivityLifecycleItem lifecycleItem) {
final IBinder token = lifecycleItem.getActivityToken();
final ActivityClientRecord r = mTransactionHandler.getActivityClient(token);
if (DEBUG_RESOLVER) {
diff --git a/core/java/android/app/servertransaction/TransactionExecutorHelper.java b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
index 7e89a5b45a2d..dfbccb41d045 100644
--- a/core/java/android/app/servertransaction/TransactionExecutorHelper.java
+++ b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
@@ -236,21 +236,39 @@ public class TransactionExecutorHelper {
* index 1 will be returned, because ActivityResult request on position 1 will be the last
* request that moves activity to the RESUMED state where it will eventually end.
*/
- static int lastCallbackRequestingState(ClientTransaction transaction) {
+ static int lastCallbackRequestingState(@NonNull ClientTransaction transaction) {
final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
- if (callbacks == null || callbacks.size() == 0) {
+ if (callbacks == null || callbacks.isEmpty()
+ || transaction.getLifecycleStateRequest() == null) {
return -1;
}
+ return lastCallbackRequestingStateIndex(callbacks, 0, callbacks.size() - 1,
+ transaction.getLifecycleStateRequest().getActivityToken());
+ }
+ /**
+ * Returns the index of the last callback between the start index and last index that requests
+ * the state for the given activity token in which that activity will be after execution.
+ * If there is a group of callbacks in the end that requests the same specific state or doesn't
+ * request any - we will find the first one from such group.
+ *
+ * E.g. ActivityResult requests RESUMED post-execution state, Configuration does not request any
+ * specific state. If there is a sequence
+ * Configuration - ActivityResult - Configuration - ActivityResult
+ * index 1 will be returned, because ActivityResult request on position 1 will be the last
+ * request that moves activity to the RESUMED state where it will eventually end.
+ */
+ private static int lastCallbackRequestingStateIndex(@NonNull List<ClientTransactionItem> items,
+ int startIndex, int lastIndex, @NonNull IBinder activityToken) {
// Go from the back of the list to front, look for the request closes to the beginning that
// requests the state in which activity will end after all callbacks are executed.
int lastRequestedState = UNDEFINED;
int lastRequestingCallback = -1;
- for (int i = callbacks.size() - 1; i >= 0; i--) {
- final ClientTransactionItem callback = callbacks.get(i);
- final int postExecutionState = callback.getPostExecutionState();
- if (postExecutionState != UNDEFINED) {
- // Found a callback that requests some post-execution state.
+ for (int i = lastIndex; i >= startIndex; i--) {
+ final ClientTransactionItem item = items.get(i);
+ final int postExecutionState = item.getPostExecutionState();
+ if (postExecutionState != UNDEFINED && activityToken.equals(item.getActivityToken())) {
+ // Found a callback that requests some post-execution state for the given activity.
if (lastRequestedState == UNDEFINED || lastRequestedState == postExecutionState) {
// It's either a first-from-end callback that requests state or it requests
// the same state as the last one. In both cases, we will use it as the new
@@ -266,6 +284,53 @@ public class TransactionExecutorHelper {
return lastRequestingCallback;
}
+ /**
+ * For the transaction item at {@code currentIndex}, if it is requesting post execution state,
+ * whether or not to exclude the last state. This only returns {@code true} when there is a
+ * following explicit {@link ActivityLifecycleItem} requesting the same state for the same
+ * activity, so that last state will be covered by the following {@link ActivityLifecycleItem}.
+ */
+ static boolean shouldExcludeLastLifecycleState(@NonNull List<ClientTransactionItem> items,
+ int currentIndex) {
+ final ClientTransactionItem item = items.get(currentIndex);
+ final IBinder activityToken = item.getActivityToken();
+ final int postExecutionState = item.getPostExecutionState();
+ if (activityToken == null || postExecutionState == UNDEFINED) {
+ // Not a transaction item requesting post execution state.
+ return false;
+ }
+ final int nextLifecycleItemIndex = findNextLifecycleItemIndex(items, currentIndex + 1,
+ activityToken);
+ if (nextLifecycleItemIndex == -1) {
+ // No following ActivityLifecycleItem for this activity token.
+ return false;
+ }
+ final ActivityLifecycleItem lifecycleItem =
+ (ActivityLifecycleItem) items.get(nextLifecycleItemIndex);
+ if (postExecutionState != lifecycleItem.getTargetState()) {
+ // The explicit ActivityLifecycleItem is not requesting the same state.
+ return false;
+ }
+ // Only exclude for the first non-lifecycle item that requests the same specific state.
+ return currentIndex == lastCallbackRequestingStateIndex(items, currentIndex,
+ nextLifecycleItemIndex - 1, activityToken);
+ }
+
+ /**
+ * Finds the index of the next {@link ActivityLifecycleItem} for the given activity token.
+ */
+ private static int findNextLifecycleItemIndex(@NonNull List<ClientTransactionItem> items,
+ int startIndex, @NonNull IBinder activityToken) {
+ final int size = items.size();
+ for (int i = startIndex; i < size; i++) {
+ final ClientTransactionItem item = items.get(i);
+ if (item.isActivityLifecycleItem() && item.getActivityToken().equals(activityToken)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
/** Dump transaction to string. */
static String transactionToString(@NonNull ClientTransaction transaction,
@NonNull ClientTransactionHandler transactionHandler) {
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java
index 531404bffd50..d10cf1691408 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java
@@ -62,4 +62,24 @@ public class ClientTransactionTests {
verify(callback2, times(1)).preExecute(clientTransactionHandler);
verify(stateRequest, times(1)).preExecute(clientTransactionHandler);
}
+
+ @Test
+ public void testPreExecuteTransactionItems() {
+ final ClientTransactionItem callback1 = mock(ClientTransactionItem.class);
+ final ClientTransactionItem callback2 = mock(ClientTransactionItem.class);
+ final ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class);
+ final ClientTransactionHandler clientTransactionHandler =
+ mock(ClientTransactionHandler.class);
+
+ final ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
+ transaction.addTransactionItem(callback1);
+ transaction.addTransactionItem(callback2);
+ transaction.addTransactionItem(stateRequest);
+
+ transaction.preExecute(clientTransactionHandler);
+
+ verify(callback1, times(1)).preExecute(clientTransactionHandler);
+ verify(callback2, times(1)).preExecute(clientTransactionHandler);
+ verify(stateRequest, times(1)).preExecute(clientTransactionHandler);
+ }
}
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
index 44a4d580dbc0..f2b0f2e622b8 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
@@ -247,6 +247,31 @@ public class TransactionExecutorTests {
}
@Test
+ public void testExecuteTransactionItems_transactionResolution() {
+ ClientTransactionItem callback1 = mock(ClientTransactionItem.class);
+ when(callback1.getPostExecutionState()).thenReturn(UNDEFINED);
+ ClientTransactionItem callback2 = mock(ClientTransactionItem.class);
+ when(callback2.getPostExecutionState()).thenReturn(UNDEFINED);
+ ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class);
+ IBinder token = mock(IBinder.class);
+ when(stateRequest.getActivityToken()).thenReturn(token);
+ when(mTransactionHandler.getActivity(token)).thenReturn(mock(Activity.class));
+
+ ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
+ transaction.addTransactionItem(callback1);
+ transaction.addTransactionItem(callback2);
+ transaction.addTransactionItem(stateRequest);
+
+ transaction.preExecute(mTransactionHandler);
+ mExecutor.execute(transaction);
+
+ InOrder inOrder = inOrder(mTransactionHandler, callback1, callback2, stateRequest);
+ inOrder.verify(callback1).execute(eq(mTransactionHandler), any());
+ inOrder.verify(callback2).execute(eq(mTransactionHandler), any());
+ inOrder.verify(stateRequest).execute(eq(mTransactionHandler), eq(mClientRecord), any());
+ }
+
+ @Test
public void testDoNotLaunchDestroyedActivity() {
final Map<IBinder, DestroyActivityItem> activitiesToBeDestroyed = new ArrayMap<>();
when(mTransactionHandler.getActivitiesToBeDestroyed()).thenReturn(activitiesToBeDestroyed);
@@ -279,12 +304,43 @@ public class TransactionExecutorTests {
}
@Test
+ public void testExecuteTransactionItems_doNotLaunchDestroyedActivity() {
+ final Map<IBinder, DestroyActivityItem> activitiesToBeDestroyed = new ArrayMap<>();
+ when(mTransactionHandler.getActivitiesToBeDestroyed()).thenReturn(activitiesToBeDestroyed);
+ // Assume launch transaction is still in queue, so there is no client record.
+ when(mTransactionHandler.getActivityClient(any())).thenReturn(null);
+
+ // An incoming destroy transaction enters binder thread (preExecute).
+ final IBinder token = mock(IBinder.class);
+ final ClientTransaction destroyTransaction = ClientTransaction.obtain(null /* client */);
+ destroyTransaction.addTransactionItem(
+ DestroyActivityItem.obtain(token, false /* finished */, 0 /* configChanges */));
+ destroyTransaction.preExecute(mTransactionHandler);
+ // The activity should be added to to-be-destroyed container.
+ assertEquals(1, activitiesToBeDestroyed.size());
+
+ // A previous queued launch transaction runs on main thread (execute).
+ final ClientTransaction launchTransaction = ClientTransaction.obtain(null /* client */);
+ final LaunchActivityItem launchItem =
+ spy(new LaunchActivityItemBuilder().setActivityToken(token).build());
+ launchTransaction.addTransactionItem(launchItem);
+ mExecutor.execute(launchTransaction);
+
+ // The launch transaction should not be executed because its token is in the
+ // to-be-destroyed container.
+ verify(launchItem, never()).execute(any(), any());
+
+ // After the destroy transaction has been executed, the token should be removed.
+ mExecutor.execute(destroyTransaction);
+ assertTrue(activitiesToBeDestroyed.isEmpty());
+ }
+
+ @Test
public void testActivityResultRequiredStateResolution() {
when(mTransactionHandler.getActivity(any())).thenReturn(mock(Activity.class));
PostExecItem postExecItem = new PostExecItem(ON_RESUME);
- IBinder token = mock(IBinder.class);
ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
transaction.addCallback(postExecItem);
@@ -300,6 +356,26 @@ public class TransactionExecutorTests {
}
@Test
+ public void testExecuteTransactionItems_activityResultRequiredStateResolution() {
+ when(mTransactionHandler.getActivity(any())).thenReturn(mock(Activity.class));
+
+ PostExecItem postExecItem = new PostExecItem(ON_RESUME);
+
+ ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
+ transaction.addTransactionItem(postExecItem);
+
+ // Verify resolution that should get to onPause
+ mClientRecord.setState(ON_RESUME);
+ mExecutor.executeTransactionItems(transaction);
+ verify(mExecutor).cycleToPath(eq(mClientRecord), eq(ON_PAUSE), eq(transaction));
+
+ // Verify resolution that should get to onStart
+ mClientRecord.setState(ON_STOP);
+ mExecutor.executeTransactionItems(transaction);
+ verify(mExecutor).cycleToPath(eq(mClientRecord), eq(ON_START), eq(transaction));
+ }
+
+ @Test
public void testClosestStateResolutionForSameState() {
final int[] allStates = new int[] {
ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY};
@@ -444,6 +520,18 @@ public class TransactionExecutorTests {
mExecutor.executeCallbacks(transaction);
}
+ @Test(expected = IllegalArgumentException.class)
+ public void testExecuteTransactionItems_activityItemNullRecordThrowsException() {
+ final ActivityTransactionItem activityItem = mock(ActivityTransactionItem.class);
+ when(activityItem.getPostExecutionState()).thenReturn(UNDEFINED);
+ final IBinder token = mock(IBinder.class);
+ final ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
+ transaction.addTransactionItem(activityItem);
+ when(mTransactionHandler.getActivityClient(token)).thenReturn(null);
+
+ mExecutor.executeTransactionItems(transaction);
+ }
+
@Test
public void testActivityItemExecute() {
final IBinder token = mock(IBinder.class);
@@ -464,6 +552,26 @@ public class TransactionExecutorTests {
inOrder.verify(stateRequest).execute(eq(mTransactionHandler), eq(mClientRecord), any());
}
+ @Test
+ public void testExecuteTransactionItems_activityItemExecute() {
+ final IBinder token = mock(IBinder.class);
+ final ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
+ final ActivityTransactionItem activityItem = mock(ActivityTransactionItem.class);
+ when(activityItem.getPostExecutionState()).thenReturn(UNDEFINED);
+ when(activityItem.getActivityToken()).thenReturn(token);
+ transaction.addTransactionItem(activityItem);
+ final ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class);
+ transaction.addTransactionItem(stateRequest);
+ when(stateRequest.getActivityToken()).thenReturn(token);
+ when(mTransactionHandler.getActivity(token)).thenReturn(mock(Activity.class));
+
+ mExecutor.execute(transaction);
+
+ final InOrder inOrder = inOrder(activityItem, stateRequest);
+ inOrder.verify(activityItem).execute(eq(mTransactionHandler), eq(mClientRecord), any());
+ inOrder.verify(stateRequest).execute(eq(mTransactionHandler), eq(mClientRecord), any());
+ }
+
private static int[] shuffledArray(int[] inputArray) {
final List<Integer> list = Arrays.stream(inputArray).boxed().collect(Collectors.toList());
Collections.shuffle(list);
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index 7d047c93520f..4aa62c503a41 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -285,6 +285,10 @@ public class TransactionParcelTests {
78 /* configChanges */);
ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
+ transaction.addTransactionItem(callback1);
+ transaction.addTransactionItem(callback2);
+ transaction.addTransactionItem(lifecycleRequest);
+
transaction.addCallback(callback1);
transaction.addCallback(callback2);
transaction.setLifecycleStateRequest(lifecycleRequest);