SyncSM02: implement addState function

Duplicated state is not allowed.

Test: atest SynStateMachineTest
Change-Id: I0d5c73f666f90aebcfbf535cf0f824c5050941a2
diff --git a/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java b/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
index f2b0cfb..5a984c7 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
@@ -18,11 +18,15 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.internal.util.State;
 
 import java.util.List;
+import java.util.Objects;
+import java.util.Set;
 
 /**
  * An implementation of a state machine, meant to be called synchronously.
@@ -35,6 +39,7 @@
     @NonNull private final String mName;
     @NonNull private final Thread mMyThread;
     private final boolean mDbg;
+    private final ArrayMap<State, StateInfo> mStateInfo = new ArrayMap<>();
 
     // mCurrentState is the current state. mDestState is the target state that mCurrentState will
     // transition to. The value of mDestState can be changed when a state processes a message and
@@ -91,9 +96,9 @@
     }
 
     /**
-     * Add all of states to the state machine. Different StateInfos which have same state but have
-     * different parents are not allowed. A state can not have multiple parent states.
-     * This can only be called once either from mMyThread or before mMyThread started.
+     * Add all of states to the state machine. Different StateInfos which have same state are not
+     * allowed. In other words, a state can not have multiple parent states. #addAllStates can
+     * only be called once either from mMyThread or before mMyThread started.
      */
     public final void addAllStates(@NonNull final List<StateInfo> stateInfos) {
         ensureCorrectOrNotStartedThread();
@@ -101,13 +106,39 @@
         if (mCurrentState != null) {
             throw new IllegalStateException("State only can be added before started");
         }
+
+        if (stateInfos.isEmpty()) throw new IllegalStateException("Empty state is not allowed");
+
+        if (!mStateInfo.isEmpty()) throw new IllegalStateException("States are already configured");
+
+        final Set<Class> usedClasses = new ArraySet<>();
+        for (final StateInfo info : stateInfos) {
+            Objects.requireNonNull(info.state);
+            if (!usedClasses.add(info.state.getClass())) {
+                throw new IllegalStateException("Adding the same state multiple times in a state "
+                        + "machine is forbidden because it tends to be confusing; it can be done "
+                        + "with anonymous subclasses but consider carefully whether you want to "
+                        + "use a single state or other alternatives instead.");
+            }
+
+            mStateInfo.put(info.state, info);
+        }
+
+        // Check whether all of parent states indicated from StateInfo are added.
+        for (final StateInfo info : stateInfos) {
+            if (info.parent != null) ensureExistingState(info.parent);
+        }
     }
 
     /**
      * Start the state machine. The initial state can't be child state.
+     *
+     * @param initialState the first state of this machine. The state must be exact state object
+     * setting up by {@link #addAllStates}, not a copy of it.
      */
     public final void start(@NonNull final State initialState) {
         ensureCorrectThread();
+        ensureExistingState(initialState);
 
         mCurrentState = initialState;
         mDestState = initialState;
@@ -127,11 +158,13 @@
      * This function can NOT be called inside the State enter and exit function. The transition
      * target is always defined and can never be changed mid-way of state transition.
      *
-     * @param destState will be the state to transition to.
+     * @param destState will be the state to transition to. The state must be the same instance set
+     * up by {@link #addAllStates}, not a copy of it.
      */
     public final void transitionTo(@NonNull final State destState) {
         if (mDbg) Log.d(mName, "transitionTo " + destState);
         ensureCorrectThread();
+        ensureExistingState(destState);
 
         if (mDestState == mCurrentState) {
             mDestState = destState;
@@ -151,4 +184,8 @@
 
         ensureCorrectThread();
     }
+
+    private void ensureExistingState(@NonNull final State state) {
+        if (!mStateInfo.containsKey(state)) throw new IllegalStateException("Invalid state");
+    }
 }