403 lines
18 KiB
Java
403 lines
18 KiB
Java
/*
|
|
* Copyright 2018 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 android.app.servertransaction;
|
|
|
|
import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE;
|
|
import static android.app.servertransaction.ActivityLifecycleItem.ON_DESTROY;
|
|
import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
|
|
import static android.app.servertransaction.ActivityLifecycleItem.ON_RESTART;
|
|
import static android.app.servertransaction.ActivityLifecycleItem.ON_RESUME;
|
|
import static android.app.servertransaction.ActivityLifecycleItem.ON_START;
|
|
import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
|
|
import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE;
|
|
import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.app.Activity;
|
|
import android.app.ActivityThread.ActivityClientRecord;
|
|
import android.app.ClientTransactionHandler;
|
|
import android.os.IBinder;
|
|
import android.util.IntArray;
|
|
import android.util.Log;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
|
|
import java.io.PrintWriter;
|
|
import java.io.StringWriter;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Helper class for {@link TransactionExecutor} that contains utils for lifecycle path resolution.
|
|
* @hide
|
|
*/
|
|
public class TransactionExecutorHelper {
|
|
private static final String TAG = TransactionExecutorHelper.class.getSimpleName();
|
|
// A penalty applied to path with destruction when looking for the shortest one.
|
|
private static final int DESTRUCTION_PENALTY = 10;
|
|
|
|
private static final int[] ON_RESUME_PRE_EXCUTION_STATES = new int[] { ON_START, ON_PAUSE };
|
|
|
|
// Temp holder for lifecycle path.
|
|
// No direct transition between two states should take more than one complete cycle of 6 states.
|
|
@ActivityLifecycleItem.LifecycleState
|
|
private IntArray mLifecycleSequence = new IntArray(6);
|
|
|
|
/**
|
|
* Calculate the path through main lifecycle states for an activity and fill
|
|
* @link #mLifecycleSequence} with values starting with the state that follows the initial
|
|
* state.
|
|
* <p>NOTE: The returned value is used internally in this class and is not a copy. It's contents
|
|
* may change after calling other methods of this class.</p>
|
|
*/
|
|
@VisibleForTesting
|
|
public IntArray getLifecyclePath(int start, int finish, boolean excludeLastState) {
|
|
if (start == UNDEFINED || finish == UNDEFINED) {
|
|
throw new IllegalArgumentException("Can't resolve lifecycle path for undefined state");
|
|
}
|
|
if (start == ON_RESTART || finish == ON_RESTART) {
|
|
throw new IllegalArgumentException(
|
|
"Can't start or finish in intermittent RESTART state");
|
|
}
|
|
if (finish == PRE_ON_CREATE && start != finish) {
|
|
throw new IllegalArgumentException("Can only start in pre-onCreate state");
|
|
}
|
|
|
|
mLifecycleSequence.clear();
|
|
if (finish >= start) {
|
|
if (start == ON_START && finish == ON_STOP) {
|
|
// A case when we from start to stop state soon, we don't need to go
|
|
// through the resumed, paused state.
|
|
mLifecycleSequence.add(ON_STOP);
|
|
} else {
|
|
// just go there
|
|
for (int i = start + 1; i <= finish; i++) {
|
|
mLifecycleSequence.add(i);
|
|
}
|
|
}
|
|
} else { // finish < start, can't just cycle down
|
|
if (start == ON_PAUSE && finish == ON_RESUME) {
|
|
// Special case when we can just directly go to resumed state.
|
|
mLifecycleSequence.add(ON_RESUME);
|
|
} else if (start <= ON_STOP && finish >= ON_START) {
|
|
// Restart and go to required state.
|
|
|
|
// Go to stopped state first.
|
|
for (int i = start + 1; i <= ON_STOP; i++) {
|
|
mLifecycleSequence.add(i);
|
|
}
|
|
// Restart
|
|
mLifecycleSequence.add(ON_RESTART);
|
|
// Go to required state
|
|
for (int i = ON_START; i <= finish; i++) {
|
|
mLifecycleSequence.add(i);
|
|
}
|
|
} else {
|
|
// Relaunch and go to required state
|
|
|
|
// Go to destroyed state first.
|
|
for (int i = start + 1; i <= ON_DESTROY; i++) {
|
|
mLifecycleSequence.add(i);
|
|
}
|
|
// Go to required state
|
|
for (int i = ON_CREATE; i <= finish; i++) {
|
|
mLifecycleSequence.add(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove last transition in case we want to perform it with some specific params.
|
|
if (excludeLastState && mLifecycleSequence.size() != 0) {
|
|
mLifecycleSequence.remove(mLifecycleSequence.size() - 1);
|
|
}
|
|
|
|
return mLifecycleSequence;
|
|
}
|
|
|
|
/**
|
|
* Pick a state that goes before provided post-execution state and would require the least
|
|
* lifecycle transitions to get to.
|
|
* It will also make sure to try avoiding a path with activity destruction and relaunch if
|
|
* possible.
|
|
* @param r An activity that we're trying to resolve the transition for.
|
|
* @param postExecutionState Post execution state to compute for.
|
|
* @return One of states that precede the provided post-execution state, or
|
|
* {@link ActivityLifecycleItem#UNDEFINED} if there is not path.
|
|
*/
|
|
@VisibleForTesting
|
|
public int getClosestPreExecutionState(ActivityClientRecord r,
|
|
int postExecutionState) {
|
|
switch (postExecutionState) {
|
|
case UNDEFINED:
|
|
return UNDEFINED;
|
|
case ON_RESUME:
|
|
return getClosestOfStates(r, ON_RESUME_PRE_EXCUTION_STATES);
|
|
default:
|
|
throw new UnsupportedOperationException("Pre-execution states for state: "
|
|
+ postExecutionState + " is not supported.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pick a state that would require the least lifecycle transitions to get to.
|
|
* It will also make sure to try avoiding a path with activity destruction and relaunch if
|
|
* possible.
|
|
* @param r An activity that we're trying to resolve the transition for.
|
|
* @param finalStates An array of valid final states.
|
|
* @return One of the provided final states, or {@link ActivityLifecycleItem#UNDEFINED} if none
|
|
* were provided or there is not path.
|
|
*/
|
|
@VisibleForTesting
|
|
public int getClosestOfStates(ActivityClientRecord r, int[] finalStates) {
|
|
if (finalStates == null || finalStates.length == 0) {
|
|
return UNDEFINED;
|
|
}
|
|
if (r == null) {
|
|
// Early return because the ActivityClientRecord hasn't been created or cannot be found.
|
|
Log.w(TAG, "ActivityClientRecord was null");
|
|
return UNDEFINED;
|
|
}
|
|
|
|
final int currentState = r.getLifecycleState();
|
|
int closestState = UNDEFINED;
|
|
for (int i = 0, shortestPath = Integer.MAX_VALUE, pathLength; i < finalStates.length; i++) {
|
|
getLifecyclePath(currentState, finalStates[i], false /* excludeLastState */);
|
|
pathLength = mLifecycleSequence.size();
|
|
if (pathInvolvesDestruction(mLifecycleSequence)) {
|
|
pathLength += DESTRUCTION_PENALTY;
|
|
}
|
|
if (shortestPath > pathLength) {
|
|
shortestPath = pathLength;
|
|
closestState = finalStates[i];
|
|
}
|
|
}
|
|
return closestState;
|
|
}
|
|
|
|
/** Get the lifecycle state request to match the current state in the end of a transaction. */
|
|
public static ActivityLifecycleItem getLifecycleRequestForCurrentState(ActivityClientRecord r) {
|
|
final int prevState = r.getLifecycleState();
|
|
final ActivityLifecycleItem lifecycleItem;
|
|
switch (prevState) {
|
|
// TODO(lifecycler): Extend to support all possible states.
|
|
case ON_START:
|
|
// Fall through to return the PAUSE item to ensure the activity is properly
|
|
// resumed while relaunching.
|
|
case ON_PAUSE:
|
|
lifecycleItem = PauseActivityItem.obtain(r.token);
|
|
break;
|
|
case ON_STOP:
|
|
lifecycleItem = StopActivityItem.obtain(r.token);
|
|
break;
|
|
default:
|
|
lifecycleItem = ResumeActivityItem.obtain(r.token, false /* isForward */,
|
|
false /* shouldSendCompatFakeFocus */);
|
|
break;
|
|
}
|
|
|
|
return lifecycleItem;
|
|
}
|
|
|
|
/**
|
|
* Check if there is a destruction involved in the path. We want to avoid a lifecycle sequence
|
|
* that involves destruction and recreation if there is another path.
|
|
*/
|
|
private static boolean pathInvolvesDestruction(IntArray lifecycleSequence) {
|
|
final int size = lifecycleSequence.size();
|
|
for (int i = 0; i < size; i++) {
|
|
if (lifecycleSequence.get(i) == ON_DESTROY) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return the index of the last callback that requests the state in which 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.
|
|
* @deprecated to be removed with {@link TransactionExecutor#executeCallbacks}.
|
|
*/
|
|
@Deprecated
|
|
static int lastCallbackRequestingState(@NonNull ClientTransaction transaction) {
|
|
final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
|
|
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 = 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
|
|
// candidate.
|
|
lastRequestedState = postExecutionState;
|
|
lastRequestingCallback = i;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
final StringWriter stringWriter = new StringWriter();
|
|
final PrintWriter pw = new PrintWriter(stringWriter);
|
|
final String prefix = tId(transaction);
|
|
transaction.dump(prefix, pw, transactionHandler);
|
|
return stringWriter.toString();
|
|
}
|
|
|
|
/** @return A string in format "tId:<transaction hashcode> ". */
|
|
static String tId(ClientTransaction transaction) {
|
|
return "tId:" + transaction.hashCode() + " ";
|
|
}
|
|
|
|
/** Get activity string name for provided token. */
|
|
static String getActivityName(IBinder token, ClientTransactionHandler transactionHandler) {
|
|
final Activity activity = getActivityForToken(token, transactionHandler);
|
|
if (activity != null) {
|
|
return activity.getComponentName().getClassName();
|
|
}
|
|
return "Not found for token: " + token;
|
|
}
|
|
|
|
/** Get short activity class name for provided token. */
|
|
static String getShortActivityName(IBinder token, ClientTransactionHandler transactionHandler) {
|
|
final Activity activity = getActivityForToken(token, transactionHandler);
|
|
if (activity != null) {
|
|
return activity.getComponentName().getShortClassName();
|
|
}
|
|
return "Not found for token: " + token;
|
|
}
|
|
|
|
private static Activity getActivityForToken(IBinder token,
|
|
ClientTransactionHandler transactionHandler) {
|
|
if (token == null) {
|
|
return null;
|
|
}
|
|
return transactionHandler.getActivity(token);
|
|
}
|
|
|
|
/** Get lifecycle state string name. */
|
|
static String getStateName(int state) {
|
|
switch (state) {
|
|
case UNDEFINED:
|
|
return "UNDEFINED";
|
|
case PRE_ON_CREATE:
|
|
return "PRE_ON_CREATE";
|
|
case ON_CREATE:
|
|
return "ON_CREATE";
|
|
case ON_START:
|
|
return "ON_START";
|
|
case ON_RESUME:
|
|
return "ON_RESUME";
|
|
case ON_PAUSE:
|
|
return "ON_PAUSE";
|
|
case ON_STOP:
|
|
return "ON_STOP";
|
|
case ON_DESTROY:
|
|
return "ON_DESTROY";
|
|
case ON_RESTART:
|
|
return "ON_RESTART";
|
|
default:
|
|
throw new IllegalArgumentException("Unexpected lifecycle state: " + state);
|
|
}
|
|
}
|
|
}
|