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