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