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