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