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