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