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