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     public static final int RESULT_CAPTIVE_PORTAL = 26;
69     public static final int RESTART = 27;
70     public static final int FINISH_SECURITY_FLOW = 28;
71 
72     @IntDef({
73             ADD_START,
74             CANCEL,
75             CONTINUE,
76             FAIL,
77             EARLY_EXIT,
78             CONNECT,
79             SELECT_WIFI,
80             PASSWORD,
81             OTHER_NETWORK,
82             KNOWN_NETWORK,
83             RESULT_REJECTED_BY_AP,
84             RESULT_UNKNOWN_ERROR,
85             RESULT_TIMEOUT,
86             RESULT_BAD_AUTH,
87             RESULT_SUCCESS,
88             RESULT_FAILURE,
89             TRY_AGAIN,
90             ADD_PAGE_BASED_ON_NETWORK_CHOICE,
91             OPTIONS_OR_CONNECT,
92             IP_SETTINGS,
93             IP_SETTINGS_INVALID,
94             PROXY_HOSTNAME,
95             PROXY_SETTINGS_INVALID,
96             ADVANCED_FLOW_COMPLETE,
97             ENTER_ADVANCED_FLOW,
98             EXIT_ADVANCED_FLOW,
99             RESULT_CAPTIVE_PORTAL,
100             FINISH_SECURITY_FLOW})
101     @Retention(RetentionPolicy.SOURCE)
102     public @interface Event {
103     }
104 
StateMachine()105     public StateMachine() {
106     }
107 
StateMachine(Callback callback)108     public StateMachine(Callback callback) {
109         mCallback = callback;
110     }
111 
112     /**
113      * Set the callback for the things need to done when the state machine leaves end state.
114      */
setCallback(Callback callback)115     public void setCallback(Callback callback) {
116         mCallback = callback;
117     }
118 
119     /**
120      * Add state with transition.
121      *
122      * @param state       start state.
123      * @param event       transition between two states.
124      * @param destination destination state.
125      */
addState(State state, @Event int event, State destination)126     public void addState(State state, @Event int event, State destination) {
127         if (!mTransitionMap.containsKey(state)) {
128             mTransitionMap.put(state, new ArrayList<>());
129         }
130 
131         mTransitionMap.get(state).add(new Transition(state, event, destination));
132     }
133 
134     /**
135      * Add a state that has no outward transitions, but will end the state machine flow.
136      */
addTerminalState(State state)137     public void addTerminalState(State state) {
138         mTransitionMap.put(state, new ArrayList<>());
139     }
140 
141     /**
142      * Enables the activity to be notified when state machine enter end state.
143      */
144     public interface Callback {
145         /**
146          * Implement this to define what to do when the activity is finished.
147          *
148          * @param result the activity result.
149          */
onFinish(int result)150         void onFinish(int result);
151     }
152 
153     /**
154      * Set the start state of state machine/
155      *
156      * @param startState start state.
157      */
setStartState(State startState)158     public void setStartState(State startState) {
159         mStatesList.addLast(startState);
160     }
161 
162     /**
163      * Start the state machine.
164      */
start(boolean movingForward)165     public void start(boolean movingForward) {
166         if (mStatesList.isEmpty()) {
167             throw new IllegalArgumentException("Start state not set");
168         }
169         State currentState = getCurrentState();
170         if (movingForward) {
171             currentState.processForward();
172         } else {
173             currentState.processBackward();
174         }
175     }
176 
177     /**
178      * Initialize the states list.
179      */
reset()180     public void reset() {
181         mStatesList = new LinkedList<>();
182     }
183 
184     /**
185      * Make the state machine go back to the previous state.
186      */
back()187     public void back() {
188         updateState(CANCEL);
189     }
190 
191     /**
192      * Return the current state of state machine.
193      */
getCurrentState()194     public State getCurrentState() {
195         if (!mStatesList.isEmpty()) {
196             return mStatesList.getLast();
197         } else {
198             return null;
199         }
200     }
201 
202     /**
203      * Notify state machine that current activity is finished.
204      *
205      * @param result the result of activity.
206      */
finish(int result)207     public void finish(int result) {
208         mCallback.onFinish(result);
209     }
210 
updateState(@vent int event)211     private void updateState(@Event int event) {
212         // Handle early exits first.
213         if (event == EARLY_EXIT) {
214             finish(Activity.RESULT_OK);
215             return;
216         } else if (event == FAIL) {
217             finish(Activity.RESULT_CANCELED);
218             return;
219         }
220 
221         // Handle Event.CANCEL, it happens when the back button is pressed.
222         if (event == CANCEL) {
223             if (mStatesList.size() < 2) {
224                 mCallback.onFinish(Activity.RESULT_CANCELED);
225             } else {
226                 mStatesList.removeLast();
227                 State prev = mStatesList.getLast();
228                 prev.processBackward();
229             }
230             return;
231         }
232 
233         State next = null;
234         State currentState = getCurrentState();
235 
236         List<Transition> list = mTransitionMap.get(currentState);
237         if (list != null) {
238             for (Transition transition : mTransitionMap.get(currentState)) {
239                 if (transition.event == event) {
240                     next = transition.destination;
241                 }
242             }
243         }
244 
245         if (next == null) {
246             if (event == CONTINUE) {
247                 mCallback.onFinish(Activity.RESULT_OK);
248                 return;
249             }
250             throw new IllegalArgumentException(
251                     getCurrentState().getClass() + "Invalid transition " + event);
252         }
253 
254         addToStack(next);
255         next.processForward();
256     }
257 
addToStack(State state)258     private void addToStack(State state) {
259         for (int i = mStatesList.size() - 1; i >= 0; i--) {
260             if (equal(state, mStatesList.get(i))) {
261                 for (int j = mStatesList.size() - 1; j >= i; j--) {
262                     mStatesList.removeLast();
263                 }
264             }
265         }
266         mStatesList.addLast(state);
267     }
268 
equal(State s1, State s2)269     private boolean equal(State s1, State s2) {
270         if (!s1.getClass().equals(s2.getClass())) {
271             return false;
272         }
273         return true;
274     }
275 
getListener()276     public State.StateCompleteListener getListener() {
277         return mCompletionListener;
278     }
279 }
280