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