1 /*
2  * Copyright (C) 2017 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.tv.settings.connectivity.util;
18 
19 import android.app.Activity;
20 
21 import androidx.annotation.IntDef;
22 import androidx.lifecycle.ViewModel;
23 
24 import java.lang.annotation.Retention;
25 import java.lang.annotation.RetentionPolicy;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.LinkedList;
29 import java.util.List;
30 import java.util.Map;
31 
32 /**
33  * State machine responsible for handling the logic between different states.
34  */
35 public class StateMachine extends ViewModel {
36 
37     private Callback mCallback;
38     private Map<State, List<Transition>> mTransitionMap = new HashMap<>();
39     private LinkedList<State> mStatesList = new LinkedList<>();
40     private State.StateCompleteListener mCompletionListener = this::updateState;
41 
42     public static final int ADD_START = 0;
43     public static final int CANCEL = 1;
44     public static final int CONTINUE = 2;
45     public static final int FAIL = 3;
46     public static final int EARLY_EXIT = 4;
47     public static final int CONNECT = 5;
48     public static final int SELECT_WIFI = 6;
49     public static final int PASSWORD = 7;
50     public static final int OTHER_NETWORK = 8;
51     public static final int KNOWN_NETWORK = 9;
52     public static final int RESULT_REJECTED_BY_AP = 10;
53     public static final int RESULT_UNKNOWN_ERROR = 11;
54     public static final int RESULT_TIMEOUT = 12;
55     public static final int RESULT_BAD_AUTH = 13;
56     public static final int RESULT_SUCCESS = 14;
57     public static final int RESULT_FAILURE = 15;
58     public static final int TRY_AGAIN = 16;
59     public static final int ADD_PAGE_BASED_ON_NETWORK_CHOICE = 17;
60     public static final int OPTIONS_OR_CONNECT = 18;
61     public static final int IP_SETTINGS = 19;
62     public static final int IP_SETTINGS_INVALID = 20;
63     public static final int PROXY_HOSTNAME = 21;
64     public static final int PROXY_SETTINGS_INVALID = 22;
65     public static final int ADVANCED_FLOW_COMPLETE = 23;
66     public static final int ENTER_ADVANCED_FLOW = 24;
67     public static final int EXIT_ADVANCED_FLOW = 25;
68     @IntDef({
69             ADD_START,
70             CANCEL,
71             CONTINUE,
72             FAIL,
73             EARLY_EXIT,
74             CONNECT,
75             SELECT_WIFI,
76             PASSWORD,
77             OTHER_NETWORK,
78             KNOWN_NETWORK,
79             RESULT_REJECTED_BY_AP,
80             RESULT_UNKNOWN_ERROR,
81             RESULT_TIMEOUT,
82             RESULT_BAD_AUTH,
83             RESULT_SUCCESS,
84             RESULT_FAILURE,
85             TRY_AGAIN,
86             ADD_PAGE_BASED_ON_NETWORK_CHOICE,
87             OPTIONS_OR_CONNECT,
88             IP_SETTINGS,
89             IP_SETTINGS_INVALID,
90             PROXY_HOSTNAME,
91             PROXY_SETTINGS_INVALID,
92             ADVANCED_FLOW_COMPLETE,
93             ENTER_ADVANCED_FLOW,
94             EXIT_ADVANCED_FLOW})
95     @Retention(RetentionPolicy.SOURCE)
96     public @interface Event {
97     }
98 
StateMachine()99     public StateMachine() {
100     }
101 
StateMachine(Callback callback)102     public StateMachine(Callback callback) {
103         mCallback = callback;
104     }
105 
106     /**
107      * Set the callback for the things need to done when the state machine leaves end state.
108      */
setCallback(Callback callback)109     public void setCallback(Callback callback) {
110         mCallback = callback;
111     }
112 
113     /**
114      * Add state with transition.
115      *
116      * @param state       start state.
117      * @param event       transition between two states.
118      * @param destination destination state.
119      */
addState(State state, @Event int event, State destination)120     public void addState(State state, @Event int event, State destination) {
121         if (!mTransitionMap.containsKey(state)) {
122             mTransitionMap.put(state, new ArrayList<>());
123         }
124 
125         mTransitionMap.get(state).add(new Transition(state, event, destination));
126     }
127 
128     /**
129      * Add a state that has no outward transitions, but will end the state machine flow.
130      */
addTerminalState(State state)131     public void addTerminalState(State state) {
132         mTransitionMap.put(state, new ArrayList<>());
133     }
134 
135     /**
136      * Enables the activity to be notified when state machine enter end state.
137      */
138     public interface Callback {
139         /**
140          * Implement this to define what to do when the activity is finished.
141          *
142          * @param result the activity result.
143          */
onFinish(int result)144         void onFinish(int result);
145     }
146 
147     /**
148      * Set the start state of state machine/
149      *
150      * @param startState start state.
151      */
setStartState(State startState)152     public void setStartState(State startState) {
153         mStatesList.addLast(startState);
154     }
155 
156     /**
157      * Start the state machine.
158      */
start(boolean movingForward)159     public void start(boolean movingForward) {
160         if (mStatesList.isEmpty()) {
161             throw new IllegalArgumentException("Start state not set");
162         }
163         State currentState = getCurrentState();
164         if (movingForward) {
165             currentState.processForward();
166         } else {
167             currentState.processBackward();
168         }
169     }
170 
171     /**
172      * Initialize the states list.
173      */
reset()174     public void reset() {
175         mStatesList = new LinkedList<>();
176     }
177 
178     /**
179      * Make the state machine go back to the previous state.
180      */
back()181     public void back() {
182         updateState(CANCEL);
183     }
184 
185     /**
186      * Return the current state of state machine.
187      */
getCurrentState()188     public State getCurrentState() {
189         if (!mStatesList.isEmpty()) {
190             return mStatesList.getLast();
191         } else {
192             return null;
193         }
194     }
195 
196     /**
197      * Notify state machine that current activity is finished.
198      *
199      * @param result the result of activity.
200      */
finish(int result)201     public void finish(int result) {
202         mCallback.onFinish(result);
203     }
204 
updateState(@vent int event)205     private void updateState(@Event int event) {
206         // Handle early exits first.
207         if (event == EARLY_EXIT) {
208             finish(Activity.RESULT_OK);
209             return;
210         } else if (event == FAIL) {
211             finish(Activity.RESULT_CANCELED);
212             return;
213         }
214 
215         // Handle Event.CANCEL, it happens when the back button is pressed.
216         if (event == CANCEL) {
217             if (mStatesList.size() < 2) {
218                 mCallback.onFinish(Activity.RESULT_CANCELED);
219             } else {
220                 mStatesList.removeLast();
221                 State prev = mStatesList.getLast();
222                 prev.processBackward();
223             }
224             return;
225         }
226 
227         State next = null;
228         State currentState = getCurrentState();
229 
230         List<Transition> list = mTransitionMap.get(currentState);
231         if (list != null) {
232             for (Transition transition : mTransitionMap.get(currentState)) {
233                 if (transition.event == event) {
234                     next = transition.destination;
235                 }
236             }
237         }
238 
239         if (next == null) {
240             if (event == CONTINUE) {
241                 mCallback.onFinish(Activity.RESULT_OK);
242                 return;
243             }
244             throw new IllegalArgumentException(
245                     getCurrentState().getClass() + "Invalid transition " + event);
246         }
247 
248         addToStack(next);
249         next.processForward();
250     }
251 
addToStack(State state)252     private void addToStack(State state) {
253         for (int i = mStatesList.size() - 1; i >= 0; i--) {
254             if (state.getClass().equals(mStatesList.get(i).getClass())) {
255                 for (int j = mStatesList.size() - 1; j >= i; j--) {
256                     mStatesList.removeLast();
257                 }
258             }
259         }
260         mStatesList.addLast(state);
261     }
262 
getListener()263     public State.StateCompleteListener getListener() {
264         return mCompletionListener;
265     }
266 }
267