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