1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app.servertransaction; 18 19 import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE; 20 import static android.app.servertransaction.ActivityLifecycleItem.ON_DESTROY; 21 import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; 22 import static android.app.servertransaction.ActivityLifecycleItem.ON_RESTART; 23 import static android.app.servertransaction.ActivityLifecycleItem.ON_RESUME; 24 import static android.app.servertransaction.ActivityLifecycleItem.ON_START; 25 import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; 26 import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE; 27 import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED; 28 29 import android.annotation.NonNull; 30 import android.app.Activity; 31 import android.app.ActivityThread.ActivityClientRecord; 32 import android.app.ClientTransactionHandler; 33 import android.os.IBinder; 34 import android.util.IntArray; 35 import android.util.Log; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 39 import java.io.PrintWriter; 40 import java.io.StringWriter; 41 import java.util.List; 42 43 /** 44 * Helper class for {@link TransactionExecutor} that contains utils for lifecycle path resolution. 45 * @hide 46 */ 47 public class TransactionExecutorHelper { 48 private static final String TAG = TransactionExecutorHelper.class.getSimpleName(); 49 // A penalty applied to path with destruction when looking for the shortest one. 50 private static final int DESTRUCTION_PENALTY = 10; 51 52 private static final int[] ON_RESUME_PRE_EXCUTION_STATES = new int[] { ON_START, ON_PAUSE }; 53 54 // Temp holder for lifecycle path. 55 // No direct transition between two states should take more than one complete cycle of 6 states. 56 @ActivityLifecycleItem.LifecycleState 57 private IntArray mLifecycleSequence = new IntArray(6); 58 59 /** 60 * Calculate the path through main lifecycle states for an activity and fill 61 * @link #mLifecycleSequence} with values starting with the state that follows the initial 62 * state. 63 * <p>NOTE: The returned value is used internally in this class and is not a copy. It's contents 64 * may change after calling other methods of this class.</p> 65 */ 66 @VisibleForTesting getLifecyclePath(int start, int finish, boolean excludeLastState)67 public IntArray getLifecyclePath(int start, int finish, boolean excludeLastState) { 68 if (start == UNDEFINED || finish == UNDEFINED) { 69 throw new IllegalArgumentException("Can't resolve lifecycle path for undefined state"); 70 } 71 if (start == ON_RESTART || finish == ON_RESTART) { 72 throw new IllegalArgumentException( 73 "Can't start or finish in intermittent RESTART state"); 74 } 75 if (finish == PRE_ON_CREATE && start != finish) { 76 throw new IllegalArgumentException("Can only start in pre-onCreate state"); 77 } 78 79 mLifecycleSequence.clear(); 80 if (finish >= start) { 81 if (start == ON_START && finish == ON_STOP) { 82 // A case when we from start to stop state soon, we don't need to go 83 // through the resumed, paused state. 84 mLifecycleSequence.add(ON_STOP); 85 } else { 86 // just go there 87 for (int i = start + 1; i <= finish; i++) { 88 mLifecycleSequence.add(i); 89 } 90 } 91 } else { // finish < start, can't just cycle down 92 if (start == ON_PAUSE && finish == ON_RESUME) { 93 // Special case when we can just directly go to resumed state. 94 mLifecycleSequence.add(ON_RESUME); 95 } else if (start <= ON_STOP && finish >= ON_START) { 96 // Restart and go to required state. 97 98 // Go to stopped state first. 99 for (int i = start + 1; i <= ON_STOP; i++) { 100 mLifecycleSequence.add(i); 101 } 102 // Restart 103 mLifecycleSequence.add(ON_RESTART); 104 // Go to required state 105 for (int i = ON_START; i <= finish; i++) { 106 mLifecycleSequence.add(i); 107 } 108 } else { 109 // Relaunch and go to required state 110 111 // Go to destroyed state first. 112 for (int i = start + 1; i <= ON_DESTROY; i++) { 113 mLifecycleSequence.add(i); 114 } 115 // Go to required state 116 for (int i = ON_CREATE; i <= finish; i++) { 117 mLifecycleSequence.add(i); 118 } 119 } 120 } 121 122 // Remove last transition in case we want to perform it with some specific params. 123 if (excludeLastState && mLifecycleSequence.size() != 0) { 124 mLifecycleSequence.remove(mLifecycleSequence.size() - 1); 125 } 126 127 return mLifecycleSequence; 128 } 129 130 /** 131 * Pick a state that goes before provided post-execution state and would require the least 132 * lifecycle transitions to get to. 133 * It will also make sure to try avoiding a path with activity destruction and relaunch if 134 * possible. 135 * @param r An activity that we're trying to resolve the transition for. 136 * @param postExecutionState Post execution state to compute for. 137 * @return One of states that precede the provided post-execution state, or 138 * {@link ActivityLifecycleItem#UNDEFINED} if there is not path. 139 */ 140 @VisibleForTesting getClosestPreExecutionState(ActivityClientRecord r, int postExecutionState)141 public int getClosestPreExecutionState(ActivityClientRecord r, 142 int postExecutionState) { 143 switch (postExecutionState) { 144 case UNDEFINED: 145 return UNDEFINED; 146 case ON_RESUME: 147 return getClosestOfStates(r, ON_RESUME_PRE_EXCUTION_STATES); 148 default: 149 throw new UnsupportedOperationException("Pre-execution states for state: " 150 + postExecutionState + " is not supported."); 151 } 152 } 153 154 /** 155 * Pick a state that would require the least lifecycle transitions to get to. 156 * It will also make sure to try avoiding a path with activity destruction and relaunch if 157 * possible. 158 * @param r An activity that we're trying to resolve the transition for. 159 * @param finalStates An array of valid final states. 160 * @return One of the provided final states, or {@link ActivityLifecycleItem#UNDEFINED} if none 161 * were provided or there is not path. 162 */ 163 @VisibleForTesting getClosestOfStates(ActivityClientRecord r, int[] finalStates)164 public int getClosestOfStates(ActivityClientRecord r, int[] finalStates) { 165 if (finalStates == null || finalStates.length == 0) { 166 return UNDEFINED; 167 } 168 if (r == null) { 169 // Early return because the ActivityClientRecord hasn't been created or cannot be found. 170 Log.w(TAG, "ActivityClientRecord was null"); 171 return UNDEFINED; 172 } 173 174 final int currentState = r.getLifecycleState(); 175 int closestState = UNDEFINED; 176 for (int i = 0, shortestPath = Integer.MAX_VALUE, pathLength; i < finalStates.length; i++) { 177 getLifecyclePath(currentState, finalStates[i], false /* excludeLastState */); 178 pathLength = mLifecycleSequence.size(); 179 if (pathInvolvesDestruction(mLifecycleSequence)) { 180 pathLength += DESTRUCTION_PENALTY; 181 } 182 if (shortestPath > pathLength) { 183 shortestPath = pathLength; 184 closestState = finalStates[i]; 185 } 186 } 187 return closestState; 188 } 189 190 /** Get the lifecycle state request to match the current state in the end of a transaction. */ getLifecycleRequestForCurrentState(ActivityClientRecord r)191 public static ActivityLifecycleItem getLifecycleRequestForCurrentState(ActivityClientRecord r) { 192 final int prevState = r.getLifecycleState(); 193 final ActivityLifecycleItem lifecycleItem; 194 switch (prevState) { 195 // TODO(lifecycler): Extend to support all possible states. 196 case ON_START: 197 // Fall through to return the PAUSE item to ensure the activity is properly 198 // resumed while relaunching. 199 case ON_PAUSE: 200 lifecycleItem = PauseActivityItem.obtain(r.token); 201 break; 202 case ON_STOP: 203 lifecycleItem = StopActivityItem.obtain(r.token); 204 break; 205 default: 206 lifecycleItem = ResumeActivityItem.obtain(r.token, false /* isForward */, 207 false /* shouldSendCompatFakeFocus */); 208 break; 209 } 210 211 return lifecycleItem; 212 } 213 214 /** 215 * Check if there is a destruction involved in the path. We want to avoid a lifecycle sequence 216 * that involves destruction and recreation if there is another path. 217 */ pathInvolvesDestruction(IntArray lifecycleSequence)218 private static boolean pathInvolvesDestruction(IntArray lifecycleSequence) { 219 final int size = lifecycleSequence.size(); 220 for (int i = 0; i < size; i++) { 221 if (lifecycleSequence.get(i) == ON_DESTROY) { 222 return true; 223 } 224 } 225 return false; 226 } 227 228 /** 229 * Return the index of the last callback that requests the state in which activity will be after 230 * execution. If there is a group of callbacks in the end that requests the same specific state 231 * or doesn't request any - we will find the first one from such group. 232 * 233 * E.g. ActivityResult requests RESUMED post-execution state, Configuration does not request any 234 * specific state. If there is a sequence 235 * Configuration - ActivityResult - Configuration - ActivityResult 236 * index 1 will be returned, because ActivityResult request on position 1 will be the last 237 * request that moves activity to the RESUMED state where it will eventually end. 238 * @deprecated to be removed with {@link TransactionExecutor#executeCallbacks}. 239 */ 240 @Deprecated lastCallbackRequestingState(@onNull ClientTransaction transaction)241 static int lastCallbackRequestingState(@NonNull ClientTransaction transaction) { 242 final List<ClientTransactionItem> callbacks = transaction.getCallbacks(); 243 if (callbacks == null || callbacks.isEmpty() 244 || transaction.getLifecycleStateRequest() == null) { 245 return -1; 246 } 247 return lastCallbackRequestingStateIndex(callbacks, 0, callbacks.size() - 1, 248 transaction.getLifecycleStateRequest().getActivityToken()); 249 } 250 251 /** 252 * Returns the index of the last callback between the start index and last index that requests 253 * the state for the given activity token in which that activity will be after execution. 254 * If there is a group of callbacks in the end that requests the same specific state or doesn't 255 * request any - we will find the first one from such group. 256 * 257 * E.g. ActivityResult requests RESUMED post-execution state, Configuration does not request any 258 * specific state. If there is a sequence 259 * Configuration - ActivityResult - Configuration - ActivityResult 260 * index 1 will be returned, because ActivityResult request on position 1 will be the last 261 * request that moves activity to the RESUMED state where it will eventually end. 262 */ lastCallbackRequestingStateIndex(@onNull List<ClientTransactionItem> items, int startIndex, int lastIndex, @NonNull IBinder activityToken)263 private static int lastCallbackRequestingStateIndex(@NonNull List<ClientTransactionItem> items, 264 int startIndex, int lastIndex, @NonNull IBinder activityToken) { 265 // Go from the back of the list to front, look for the request closes to the beginning that 266 // requests the state in which activity will end after all callbacks are executed. 267 int lastRequestedState = UNDEFINED; 268 int lastRequestingCallback = -1; 269 for (int i = lastIndex; i >= startIndex; i--) { 270 final ClientTransactionItem item = items.get(i); 271 final int postExecutionState = item.getPostExecutionState(); 272 if (postExecutionState != UNDEFINED && activityToken.equals(item.getActivityToken())) { 273 // Found a callback that requests some post-execution state for the given activity. 274 if (lastRequestedState == UNDEFINED || lastRequestedState == postExecutionState) { 275 // It's either a first-from-end callback that requests state or it requests 276 // the same state as the last one. In both cases, we will use it as the new 277 // candidate. 278 lastRequestedState = postExecutionState; 279 lastRequestingCallback = i; 280 } else { 281 break; 282 } 283 } 284 } 285 286 return lastRequestingCallback; 287 } 288 289 /** 290 * For the transaction item at {@code currentIndex}, if it is requesting post execution state, 291 * whether or not to exclude the last state. This only returns {@code true} when there is a 292 * following explicit {@link ActivityLifecycleItem} requesting the same state for the same 293 * activity, so that last state will be covered by the following {@link ActivityLifecycleItem}. 294 */ shouldExcludeLastLifecycleState(@onNull List<ClientTransactionItem> items, int currentIndex)295 static boolean shouldExcludeLastLifecycleState(@NonNull List<ClientTransactionItem> items, 296 int currentIndex) { 297 final ClientTransactionItem item = items.get(currentIndex); 298 final IBinder activityToken = item.getActivityToken(); 299 final int postExecutionState = item.getPostExecutionState(); 300 if (activityToken == null || postExecutionState == UNDEFINED) { 301 // Not a transaction item requesting post execution state. 302 return false; 303 } 304 final int nextLifecycleItemIndex = findNextLifecycleItemIndex(items, currentIndex + 1, 305 activityToken); 306 if (nextLifecycleItemIndex == -1) { 307 // No following ActivityLifecycleItem for this activity token. 308 return false; 309 } 310 final ActivityLifecycleItem lifecycleItem = 311 (ActivityLifecycleItem) items.get(nextLifecycleItemIndex); 312 if (postExecutionState != lifecycleItem.getTargetState()) { 313 // The explicit ActivityLifecycleItem is not requesting the same state. 314 return false; 315 } 316 // Only exclude for the first non-lifecycle item that requests the same specific state. 317 return currentIndex == lastCallbackRequestingStateIndex(items, currentIndex, 318 nextLifecycleItemIndex - 1, activityToken); 319 } 320 321 /** 322 * Finds the index of the next {@link ActivityLifecycleItem} for the given activity token. 323 */ findNextLifecycleItemIndex(@onNull List<ClientTransactionItem> items, int startIndex, @NonNull IBinder activityToken)324 private static int findNextLifecycleItemIndex(@NonNull List<ClientTransactionItem> items, 325 int startIndex, @NonNull IBinder activityToken) { 326 final int size = items.size(); 327 for (int i = startIndex; i < size; i++) { 328 final ClientTransactionItem item = items.get(i); 329 if (item.isActivityLifecycleItem() && item.getActivityToken().equals(activityToken)) { 330 return i; 331 } 332 } 333 return -1; 334 } 335 336 /** Dump transaction to string. */ transactionToString(@onNull ClientTransaction transaction, @NonNull ClientTransactionHandler transactionHandler)337 static String transactionToString(@NonNull ClientTransaction transaction, 338 @NonNull ClientTransactionHandler transactionHandler) { 339 final StringWriter stringWriter = new StringWriter(); 340 final PrintWriter pw = new PrintWriter(stringWriter); 341 final String prefix = tId(transaction); 342 transaction.dump(prefix, pw, transactionHandler); 343 return stringWriter.toString(); 344 } 345 346 /** @return A string in format "tId:<transaction hashcode> ". */ tId(ClientTransaction transaction)347 static String tId(ClientTransaction transaction) { 348 return "tId:" + transaction.hashCode() + " "; 349 } 350 351 /** Get activity string name for provided token. */ getActivityName(IBinder token, ClientTransactionHandler transactionHandler)352 static String getActivityName(IBinder token, ClientTransactionHandler transactionHandler) { 353 final Activity activity = getActivityForToken(token, transactionHandler); 354 if (activity != null) { 355 return activity.getComponentName().getClassName(); 356 } 357 return "Not found for token: " + token; 358 } 359 360 /** Get short activity class name for provided token. */ getShortActivityName(IBinder token, ClientTransactionHandler transactionHandler)361 static String getShortActivityName(IBinder token, ClientTransactionHandler transactionHandler) { 362 final Activity activity = getActivityForToken(token, transactionHandler); 363 if (activity != null) { 364 return activity.getComponentName().getShortClassName(); 365 } 366 return "Not found for token: " + token; 367 } 368 getActivityForToken(IBinder token, ClientTransactionHandler transactionHandler)369 private static Activity getActivityForToken(IBinder token, 370 ClientTransactionHandler transactionHandler) { 371 if (token == null) { 372 return null; 373 } 374 return transactionHandler.getActivity(token); 375 } 376 377 /** Get lifecycle state string name. */ getStateName(int state)378 static String getStateName(int state) { 379 switch (state) { 380 case UNDEFINED: 381 return "UNDEFINED"; 382 case PRE_ON_CREATE: 383 return "PRE_ON_CREATE"; 384 case ON_CREATE: 385 return "ON_CREATE"; 386 case ON_START: 387 return "ON_START"; 388 case ON_RESUME: 389 return "ON_RESUME"; 390 case ON_PAUSE: 391 return "ON_PAUSE"; 392 case ON_STOP: 393 return "ON_STOP"; 394 case ON_DESTROY: 395 return "ON_DESTROY"; 396 case ON_RESTART: 397 return "ON_RESTART"; 398 default: 399 throw new IllegalArgumentException("Unexpected lifecycle state: " + state); 400 } 401 } 402 } 403