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