• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2023 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 com.android.net.module.util;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.Message;
22 import android.util.ArrayMap;
23 import android.util.ArraySet;
24 import android.util.Log;
25 
26 import com.android.internal.util.State;
27 
28 import java.util.ArrayDeque;
29 import java.util.List;
30 import java.util.Objects;
31 import java.util.Set;
32 
33 /**
34  * An implementation of a state machine, meant to be called synchronously.
35  *
36  * This class implements a finite state automaton based on the same State
37  * class as StateMachine.
38  * All methods of this class must be called on only one thread.
39  */
40 public class SyncStateMachine {
41     @NonNull private final String mName;
42     @NonNull private final Thread mMyThread;
43     private final boolean mDbg;
44     private final ArrayMap<State, StateInfo> mStateInfo = new ArrayMap<>();
45 
46     // mCurrentState is the current state. mDestState is the target state that mCurrentState will
47     // transition to. The value of mDestState can be changed when a state processes a message and
48     // calls #transitionTo, but it cannot be changed during the state transition. When the state
49     // transition is complete, mDestState will be set to mCurrentState. Both mCurrentState and
50     // mDestState only be null before state machine starts and must only be touched on mMyThread.
51     @Nullable private State mCurrentState;
52     @Nullable private State mDestState;
53     private final ArrayDeque<Message> mSelfMsgQueue = new ArrayDeque<Message>();
54 
55     // MIN_VALUE means not currently processing any message.
56     private int mCurrentlyProcessing = Integer.MIN_VALUE;
57     // Indicates whether automaton can send self message. Self messages can only be sent by
58     // automaton from State#enter, State#exit, or State#processMessage. Calling from outside
59     // of State is not allowed.
60     private boolean mSelfMsgAllowed = false;
61 
62     /**
63      * A information class about a state and its parent. Used to maintain the state hierarchy.
64      */
65     public static class StateInfo {
66         /** The state who owns this StateInfo. */
67         public final State state;
68         /** The parent state. */
69         public final State parent;
70         // True when the state has been entered and on the stack.
71         private boolean mActive;
72 
StateInfo(@onNull final State child, @Nullable final State parent)73         public StateInfo(@NonNull final State child, @Nullable final State parent) {
74             this.state = child;
75             this.parent = parent;
76         }
77     }
78 
79     /**
80      * The constructor.
81      *
82      * @param name of this machine.
83      * @param thread the running thread of this machine. It must either be the thread on which this
84      * constructor is called, or a thread that is not started yet.
85      */
SyncStateMachine(@onNull final String name, @NonNull final Thread thread)86     public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread) {
87         this(name, thread, false /* debug */);
88     }
89 
90     /**
91      * The constructor.
92      *
93      * @param name of this machine.
94      * @param thread the running thread of this machine. It must either be the thread on which this
95      * constructor is called, or a thread that is not started yet.
96      * @param dbg whether to print debug logs.
97      */
SyncStateMachine(@onNull final String name, @NonNull final Thread thread, final boolean dbg)98     public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread,
99             final boolean dbg) {
100         mMyThread = thread;
101         // Machine can either be setup from machine thread or before machine thread started.
102         ensureCorrectOrNotStartedThread();
103 
104         mName = name;
105         mDbg = dbg;
106     }
107 
108     /**
109      * Add all of states to the state machine. Different StateInfos which have same state are not
110      * allowed. In other words, a state can not have multiple parent states. #addAllStates can
111      * only be called once either from mMyThread or before mMyThread started.
112      */
addAllStates(@onNull final List<StateInfo> stateInfos)113     public final void addAllStates(@NonNull final List<StateInfo> stateInfos) {
114         ensureCorrectOrNotStartedThread();
115 
116         if (mCurrentState != null) {
117             throw new IllegalStateException("State only can be added before started");
118         }
119 
120         if (stateInfos.isEmpty()) throw new IllegalStateException("Empty state is not allowed");
121 
122         if (!mStateInfo.isEmpty()) throw new IllegalStateException("States are already configured");
123 
124         final Set<Class> usedClasses = new ArraySet<>();
125         for (final StateInfo info : stateInfos) {
126             Objects.requireNonNull(info.state);
127             if (!usedClasses.add(info.state.getClass())) {
128                 throw new IllegalStateException("Adding the same state multiple times in a state "
129                         + "machine is forbidden because it tends to be confusing; it can be done "
130                         + "with anonymous subclasses but consider carefully whether you want to "
131                         + "use a single state or other alternatives instead.");
132             }
133 
134             mStateInfo.put(info.state, info);
135         }
136 
137         // Check whether all of parent states indicated from StateInfo are added.
138         for (final StateInfo info : stateInfos) {
139             if (info.parent != null) ensureExistingState(info.parent);
140         }
141     }
142 
143     /**
144      * Start the state machine. The initial state can't be child state.
145      *
146      * @param initialState the first state of this machine. The state must be exact state object
147      * setting up by {@link #addAllStates}, not a copy of it.
148      */
start(@onNull final State initialState)149     public final void start(@NonNull final State initialState) {
150         ensureCorrectThread();
151         ensureExistingState(initialState);
152 
153         mDestState = initialState;
154         mSelfMsgAllowed = true;
155         performTransitions();
156         mSelfMsgAllowed = false;
157         // If sendSelfMessage was called inside initialState#enter(), mSelfMsgQueue must be
158         // processed.
159         maybeProcessSelfMessageQueue();
160     }
161 
162     /**
163      * Process the message synchronously then perform state transition. This method is used
164      * externally to the automaton to request that the automaton process the given message.
165      * The message is processed sequentially, so calling this method recursively is not permitted.
166      * In other words, using this method inside State#enter, State#exit, or State#processMessage
167      * is incorrect and will result in an IllegalStateException.
168      *
169      * @param what is assigned to Message.what
170      * @param arg1 is assigned to Message.arg1
171      * @param arg2 is assigned to Message.arg2
172      * @param obj  is assigned to Message.obj
173      */
processMessage(int what, int arg1, int arg2, @Nullable Object obj)174     public final void processMessage(int what, int arg1, int arg2, @Nullable Object obj) {
175         ensureCorrectThread();
176 
177         if (mCurrentlyProcessing != Integer.MIN_VALUE) {
178             throw new IllegalStateException("Message(" + mCurrentlyProcessing
179                     + ") is still being processed");
180         }
181 
182         // mCurrentlyProcessing tracks the external message request and it prevents this method to
183         // be called recursively. Once this message is processed and the transitions have been
184         // performed, the automaton will process the self message queue. The messages in the self
185         // message queue are added from within the automaton during processing external message.
186         // mCurrentlyProcessing is still the original external one and it will not prevent self
187         // messages from being processed.
188         mCurrentlyProcessing = what;
189         final Message msg = Message.obtain(null, what, arg1, arg2, obj);
190         currentStateProcessMessageThenPerformTransitions(msg);
191         msg.recycle();
192         maybeProcessSelfMessageQueue();
193 
194         mCurrentlyProcessing = Integer.MIN_VALUE;
195     }
196 
197     /**
198      * Synchronously process a message and perform state transition.
199      *
200      * @param what is assigned to Message.what.
201      */
processMessage(int what)202     public final void processMessage(int what) {
203         processMessage(what, 0, 0, null);
204     }
205 
maybeProcessSelfMessageQueue()206     private void maybeProcessSelfMessageQueue() {
207         while (!mSelfMsgQueue.isEmpty()) {
208             currentStateProcessMessageThenPerformTransitions(mSelfMsgQueue.poll());
209         }
210     }
211 
currentStateProcessMessageThenPerformTransitions(@onNull final Message msg)212     private void currentStateProcessMessageThenPerformTransitions(@NonNull final Message msg) {
213         mSelfMsgAllowed = true;
214         StateInfo consideredState = mStateInfo.get(mCurrentState);
215         while (null != consideredState) {
216             // Ideally this should compare with IState.HANDLED, but it is not public field so just
217             // checking whether the return value is true (IState.HANDLED = true).
218             if (consideredState.state.processMessage(msg)) {
219                 if (mDbg) {
220                     Log.d(mName, "State " + consideredState.state
221                             + " processed message " + msg.what);
222                 }
223                 break;
224             }
225             consideredState = mStateInfo.get(consideredState.parent);
226         }
227         if (null == consideredState) {
228             final String state = mCurrentState == null ? "null" : mCurrentState.getName();
229             Log.wtf(mName, "Message " + msg.what + " was not handled. Current state: " + state);
230         }
231 
232         performTransitions();
233         mSelfMsgAllowed = false;
234     }
235 
236     /**
237      * Send self message during state transition.
238      *
239      * Must only be used inside State processMessage, enter or exit. The typical use case is
240      * something wrong happens during state transition, sending an error message which would be
241      * handled after finishing current state transitions.
242      */
sendSelfMessage(int what, int arg1, int arg2, Object obj)243     public final void sendSelfMessage(int what, int arg1, int arg2, Object obj) {
244         if (!mSelfMsgAllowed) {
245             throw new IllegalStateException("sendSelfMessage can only be called inside "
246                     + "State#enter, State#exit or State#processMessage");
247         }
248 
249         mSelfMsgQueue.add(Message.obtain(null, what, arg1, arg2, obj));
250     }
251 
252     /**
253      * Transition to destination state. Upon returning from processMessage the automaton will
254      * transition to the given destination state.
255      *
256      * This function can NOT be called inside the State enter and exit function. The transition
257      * target is always defined and can never be changed mid-way of state transition.
258      *
259      * @param destState will be the state to transition to. The state must be the same instance set
260      * up by {@link #addAllStates}, not a copy of it.
261      */
transitionTo(@onNull final State destState)262     public final void transitionTo(@NonNull final State destState) {
263         if (mDbg) Log.d(mName, "transitionTo " + destState);
264         ensureCorrectThread();
265         ensureExistingState(destState);
266 
267         if (mDestState == mCurrentState) {
268             mDestState = destState;
269         } else {
270             throw new IllegalStateException("Destination already specified");
271         }
272     }
273 
performTransitions()274     private void performTransitions() {
275         // 1. Determine the common ancestor state of current/destination states
276         // 2. Invoke state exit list from current state to common ancestor state.
277         // 3. Invoke state enter list from common ancestor state to destState by going
278         // through mEnterStateStack.
279         if (mDestState == mCurrentState) return;
280 
281         final StateInfo commonAncestor = getLastActiveAncestor(mStateInfo.get(mDestState));
282 
283         executeExitMethods(commonAncestor, mStateInfo.get(mCurrentState));
284         executeEnterMethods(commonAncestor, mStateInfo.get(mDestState));
285         mCurrentState = mDestState;
286     }
287 
288     // Null is the root of all states.
getLastActiveAncestor(@ullable final StateInfo start)289     private StateInfo getLastActiveAncestor(@Nullable final StateInfo start) {
290         if (null == start || start.mActive) return start;
291 
292         return getLastActiveAncestor(mStateInfo.get(start.parent));
293     }
294 
295     // Call the exit method from current state to common ancestor state.
296     // Both the commonAncestor and exitingState StateInfo can be null because null is the ancestor
297     // of all states.
298     // For example: When transitioning from state1 to state2, the
299     // executeExitMethods(commonAncestor, exitingState) function will be called twice, once with
300     // null and state1 as the argument, and once with null and null as the argument.
301     //              root
302     //              |   \
303     // current <- state1 state2 -> destination
executeExitMethods(@ullable StateInfo commonAncestor, @Nullable StateInfo exitingState)304     private void executeExitMethods(@Nullable StateInfo commonAncestor,
305             @Nullable StateInfo exitingState) {
306         if (commonAncestor == exitingState) return;
307 
308         if (mDbg) Log.d(mName, exitingState.state + " exit()");
309         exitingState.state.exit();
310         exitingState.mActive = false;
311         executeExitMethods(commonAncestor, mStateInfo.get(exitingState.parent));
312     }
313 
314     // Call the enter method from common ancestor state to destination state.
315     // Both the commonAncestor and enteringState StateInfo can be null because null is the ancestor
316     // of all states.
317     // For example: When transitioning from state1 to state2, the
318     // executeEnterMethods(commonAncestor, enteringState) function will be called twice, once with
319     // null and state2 as the argument, and once with null and null as the argument.
320     //              root
321     //              |   \
322     // current <- state1 state2 -> destination
executeEnterMethods(@ullable StateInfo commonAncestor, @Nullable StateInfo enteringState)323     private void executeEnterMethods(@Nullable StateInfo commonAncestor,
324             @Nullable StateInfo enteringState) {
325         if (enteringState == commonAncestor) return;
326 
327         executeEnterMethods(commonAncestor, mStateInfo.get(enteringState.parent));
328         if (mDbg) Log.d(mName, enteringState.state + " enter()");
329         enteringState.state.enter();
330         enteringState.mActive = true;
331     }
332 
ensureCorrectThread()333     private void ensureCorrectThread() {
334         if (!mMyThread.equals(Thread.currentThread())) {
335             throw new IllegalStateException("Called from wrong thread");
336         }
337     }
338 
ensureCorrectOrNotStartedThread()339     private void ensureCorrectOrNotStartedThread() {
340         if (!mMyThread.isAlive()) return;
341 
342         ensureCorrectThread();
343     }
344 
ensureExistingState(@onNull final State state)345     private void ensureExistingState(@NonNull final State state) {
346         if (!mStateInfo.containsKey(state)) throw new IllegalStateException("Invalid state");
347     }
348 }
349