├── AndroidManifest.xml
├── README.md
├── ant.properties
├── build.xml
├── proguard-project.txt
├── project.properties
├── res
├── drawable-hdpi
│ └── ic_launcher.png
├── drawable-ldpi
│ └── ic_launcher.png
├── drawable-mdpi
│ └── ic_launcher.png
├── drawable-xhdpi
│ └── ic_launcher.png
├── layout
│ └── main.xml
└── values
│ └── strings.xml
└── src
└── au
└── com
└── ds
└── ef
└── ae
└── AtmEmulator
├── MainActivity.java
└── UiThreadExecutor.java
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | EasyFlow-example-AtmEmulator
2 | ============================
3 |
4 | AtmEmulator is an example Android app illustrating how **EasyFlow** can be used to simplify design and implementaion of event-driven applicaions.
5 |
6 | [Download](http://datasymphony.com.au/?wpdmact=process&did=Mi5ob3RsaW5r) binary **apk** file
7 |
8 | For more details, please, see [EasyFlow](https://github.com/Beh01der/EasyFlow) a simple and light-weight Finite State Machine for java
9 |
--------------------------------------------------------------------------------
/ant.properties:
--------------------------------------------------------------------------------
1 | # This file is used to override default values used by the Ant build system.
2 | #
3 | # This file must be checked into Version Control Systems, as it is
4 | # integral to the build system of your project.
5 |
6 | # This file is only used by the Ant script.
7 |
8 | # You can use this to override default values such as
9 | # 'source.dir' for the location of your java source folder and
10 | # 'out.dir' for the location of your output folder.
11 |
12 | # You can also use it define how the release builds are signed by declaring
13 | # the following properties:
14 | # 'key.store' for the location of your keystore and
15 | # 'key.alias' for the name of the key to use.
16 | # The password will be asked during the build when you use the 'release' target.
17 |
18 |
--------------------------------------------------------------------------------
/build.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
29 |
30 |
31 |
35 |
36 |
37 |
38 |
39 |
40 |
49 |
50 |
51 |
52 |
56 |
57 |
69 |
70 |
71 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/proguard-project.txt:
--------------------------------------------------------------------------------
1 | # To enable ProGuard in your project, edit project.properties
2 | # to define the proguard.config property as described in that file.
3 | #
4 | # Add project specific ProGuard rules here.
5 | # By default, the flags in this file are appended to flags specified
6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt
7 | # You can edit the include path and order by changing the ProGuard
8 | # include property in project.properties.
9 | #
10 | # For more details, see
11 | # http://developer.android.com/guide/developing/tools/proguard.html
12 |
13 | # Add any project specific keep options here:
14 |
15 | # If your project uses WebView with JS, uncomment the following
16 | # and specify the fully qualified class name to the JavaScript interface
17 | # class:
18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
19 | # public *;
20 | #}
21 |
--------------------------------------------------------------------------------
/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-16
15 |
--------------------------------------------------------------------------------
/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Beh01der/EasyFlow-example-AtmEmulator/f8eba7efec56fe304104026fc009faf7d371a707/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Beh01der/EasyFlow-example-AtmEmulator/f8eba7efec56fe304104026fc009faf7d371a707/res/drawable-ldpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Beh01der/EasyFlow-example-AtmEmulator/f8eba7efec56fe304104026fc009faf7d371a707/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Beh01der/EasyFlow-example-AtmEmulator/f8eba7efec56fe304104026fc009faf7d371a707/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
26 |
27 |
33 |
34 |
39 |
40 |
45 |
46 |
51 |
52 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | AtmEmulator
4 |
5 |
--------------------------------------------------------------------------------
/src/au/com/ds/ef/ae/AtmEmulator/MainActivity.java:
--------------------------------------------------------------------------------
1 | package au.com.ds.ef.ae.AtmEmulator;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 | import android.text.Editable;
6 | import android.text.TextWatcher;
7 | import android.view.View;
8 | import android.widget.Button;
9 | import android.widget.EditText;
10 | import android.widget.TextView;
11 | import au.com.ds.ef.*;
12 | import au.com.ds.ef.call.StateHandler;
13 |
14 | public class MainActivity extends Activity {
15 | private TextView txtCaption;
16 | private TextView txtMessage;
17 | private EditText txtInput;
18 | private Button btnOption1;
19 | private Button btnOption2;
20 | private Button btnOption3;
21 | private Button btnOption4;
22 |
23 | private static class FlowContext extends StatefulContext {
24 | private TextWatcher textWatcher;
25 | private String pin;
26 | private int invalidPinCounter;
27 | private int balance = 1000;
28 | private int withdrawAmt;
29 | }
30 |
31 | // defining states
32 | private final State SHOWING_WELCOME = FlowBuilder.state();
33 | private final State WAITING_FOR_PIN = FlowBuilder.state();
34 | private final State CHECKING_PIN = FlowBuilder.state();
35 | private final State RETURNING_CARD = FlowBuilder.state();
36 | private final State SHOWING_MAIN_MENU = FlowBuilder.state();
37 | private final State SHOWING_PIN_INVALID = FlowBuilder.state();
38 | private final State SHOWING_CARD_LOCKED = FlowBuilder.state();
39 | private final State SHOWING_BALANCE = FlowBuilder.state();
40 | private final State SHOWING_WITHDRAW_MENU = FlowBuilder.state();
41 | private final State SHOWING_TAKE_CASH = FlowBuilder.state();
42 |
43 | // defining events
44 | private final Event onCardPresent = FlowBuilder.event();
45 | private final Event onCardExtracted = FlowBuilder.event();
46 | private final Event onPinProvided = FlowBuilder.event();
47 | private final Event onPinValid = FlowBuilder.event();
48 | private final Event onPinInvalid = FlowBuilder.event();
49 | private final Event onTryAgain = FlowBuilder.event();
50 | private final Event onNoMoreTries = FlowBuilder.event();
51 | private final Event onCancel = FlowBuilder.event();
52 | private final Event onConfirm = FlowBuilder.event();
53 | private final Event onMenuShowBalance = FlowBuilder.event();
54 | private final Event onMenuWithdrawCash = FlowBuilder.event();
55 | private final Event onMenuExit = FlowBuilder.event();
56 | private final Event onCashExtracted = FlowBuilder.event();
57 |
58 | private EasyFlow flow;
59 |
60 | private void initFlow() {
61 | if (flow != null) {
62 | return;
63 | }
64 |
65 | // building our FSM
66 | flow = FlowBuilder
67 |
68 | .from(SHOWING_WELCOME).transit(
69 | onCardPresent.to(WAITING_FOR_PIN).transit(
70 | onPinProvided.to(CHECKING_PIN).transit(
71 | onPinValid.to(SHOWING_MAIN_MENU).transit(
72 | onMenuShowBalance.to(SHOWING_BALANCE).transit(
73 | onCancel.to(SHOWING_MAIN_MENU)
74 | ),
75 | onMenuWithdrawCash.to(SHOWING_WITHDRAW_MENU).transit(
76 | onCancel.to(SHOWING_MAIN_MENU),
77 | onConfirm.to(SHOWING_TAKE_CASH).transit(
78 | onCashExtracted.to(SHOWING_MAIN_MENU)
79 | )
80 | ),
81 | onMenuExit.to(RETURNING_CARD)
82 | ),
83 | onPinInvalid.to(SHOWING_PIN_INVALID).transit(
84 | onTryAgain.to(WAITING_FOR_PIN),
85 | onNoMoreTries.to(SHOWING_CARD_LOCKED).transit(
86 | onConfirm.to(SHOWING_WELCOME)
87 | ),
88 | onCancel.to(RETURNING_CARD)
89 | )
90 | ),
91 | onCancel.to(RETURNING_CARD).transit(
92 | onCardExtracted.to(SHOWING_WELCOME)
93 | )
94 | )
95 | )
96 |
97 | .executor(new UiThreadExecutor());
98 | }
99 |
100 | private void bindFlow() {
101 | SHOWING_WELCOME.whenEnter(new StateHandler() {
102 | @Override
103 | public void call(State state, final FlowContext context) throws Exception {
104 | txtCaption.setText("Welcome");
105 | txtMessage.setText("Welcome to our ATM\nPlease insert your card");
106 | txtInput.setVisibility(View.GONE);
107 | btnOption1.setText("Insert a Card");
108 | btnOption1.setVisibility(View.VISIBLE);
109 | btnOption1.setOnClickListener(new View.OnClickListener() {
110 | @Override
111 | public void onClick(View v) {
112 | onCardPresent.trigger(context);
113 | }
114 | });
115 | btnOption2.setVisibility(View.GONE);
116 | btnOption3.setVisibility(View.GONE);
117 | btnOption4.setVisibility(View.GONE);
118 | context.invalidPinCounter = 0;
119 | }
120 | });
121 |
122 | WAITING_FOR_PIN.whenEnter(new StateHandler() {
123 | @Override
124 | public void call(State state, final FlowContext context) throws Exception {
125 | context.textWatcher = new TextWatcher() {
126 | @Override
127 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
128 |
129 | @Override
130 | public void onTextChanged(CharSequence s, int start, int before, int count) {}
131 |
132 | @Override
133 | public void afterTextChanged(Editable s) {
134 | btnOption1.setEnabled(s.length() == 4);
135 | }
136 | };
137 |
138 | txtCaption.setText("Waiting for PIN");
139 | txtMessage.setText("Please enter your PIN and press 'Continue'\n(BTW current PIN is 1234)");
140 | txtInput.setText("");
141 | txtInput.setVisibility(View.VISIBLE);
142 | txtInput.addTextChangedListener(context.textWatcher);
143 | btnOption1.setText("Continue");
144 | btnOption1.setVisibility(View.VISIBLE);
145 | btnOption1.setEnabled(false);
146 | btnOption1.setOnClickListener(new View.OnClickListener() {
147 | @Override
148 | public void onClick(View v) {
149 | context.pin = txtInput.getText().toString();
150 | onPinProvided.trigger(context);
151 | }
152 | });
153 | btnOption2.setText("Cancel");
154 | btnOption2.setVisibility(View.VISIBLE);
155 | btnOption2.setOnClickListener(new View.OnClickListener() {
156 | @Override
157 | public void onClick(View v) {
158 | onCancel.trigger(context);
159 | }
160 | });
161 | btnOption3.setVisibility(View.GONE);
162 | btnOption4.setVisibility(View.GONE);
163 | }
164 | }).whenLeave(new StateHandler() {
165 | @Override
166 | public void call(State state, FlowContext context) throws Exception {
167 | txtInput.removeTextChangedListener(context.textWatcher);
168 | btnOption1.setEnabled(true);
169 | }
170 | });
171 |
172 | CHECKING_PIN.whenEnter(new StateHandler() {
173 | @Override
174 | public void call(State state, FlowContext context) throws Exception {
175 | if (context.pin.equals("1234")) {
176 | onPinValid.trigger(context);
177 | } else {
178 | context.invalidPinCounter++;
179 | onPinInvalid.trigger(context);
180 | }
181 | }
182 | });
183 |
184 | SHOWING_MAIN_MENU.whenEnter(new StateHandler() {
185 | @Override
186 | public void call(State state, final FlowContext context) throws Exception {
187 | txtCaption.setText("Main Menu");
188 | txtMessage.setText("I want to:");
189 | txtInput.setVisibility(View.GONE);
190 | btnOption1.setText("See balance");
191 | btnOption1.setVisibility(View.VISIBLE);
192 | btnOption1.setOnClickListener(new View.OnClickListener() {
193 | @Override
194 | public void onClick(View v) {
195 | onMenuShowBalance.trigger(context);
196 | }
197 | });
198 | btnOption2.setText("Get cash");
199 | btnOption2.setVisibility(View.VISIBLE);
200 | btnOption2.setOnClickListener(new View.OnClickListener() {
201 | @Override
202 | public void onClick(View v) {
203 | onMenuWithdrawCash.trigger(context);
204 | }
205 | });
206 | btnOption3.setText("Exit");
207 | btnOption3.setOnClickListener(new View.OnClickListener() {
208 | @Override
209 | public void onClick(View v) {
210 | onMenuExit.trigger(context);
211 | }
212 | });
213 | btnOption3.setVisibility(View.VISIBLE);
214 | btnOption4.setVisibility(View.GONE);
215 | }
216 | });
217 |
218 | SHOWING_BALANCE.whenEnter(new StateHandler() {
219 | @Override
220 | public void call(State state, final FlowContext context) throws Exception {
221 | txtCaption.setText("Showing Balance");
222 | txtMessage.setText("You currently have $" + context.balance + " on your account");
223 | txtInput.setVisibility(View.GONE);
224 | btnOption1.setText("Ok");
225 | btnOption1.setVisibility(View.VISIBLE);
226 | btnOption1.setOnClickListener(new View.OnClickListener() {
227 | @Override
228 | public void onClick(View v) {
229 | onCancel.trigger(context);
230 | }
231 | });
232 | btnOption2.setVisibility(View.GONE);
233 | btnOption3.setVisibility(View.GONE);
234 | btnOption4.setVisibility(View.GONE);
235 | }
236 | });
237 |
238 | SHOWING_WITHDRAW_MENU.whenEnter(new StateHandler() {
239 | @Override
240 | public void call(State state, final FlowContext context) throws Exception {
241 | txtCaption.setText("Withdraw Cash");
242 | txtMessage.setText("How much cash do you want to withdraw?");
243 | txtInput.setVisibility(View.GONE);
244 | btnOption1.setText("$50");
245 | btnOption1.setVisibility(View.VISIBLE);
246 | btnOption1.setEnabled(context.balance > 50);
247 | btnOption1.setOnClickListener(new View.OnClickListener() {
248 | @Override
249 | public void onClick(View v) {
250 | context.withdrawAmt = 50;
251 | onConfirm.trigger(context);
252 | }
253 | });
254 | btnOption2.setText("$100");
255 | btnOption2.setVisibility(View.VISIBLE);
256 | btnOption2.setEnabled(context.balance > 100);
257 | btnOption2.setOnClickListener(new View.OnClickListener() {
258 | @Override
259 | public void onClick(View v) {
260 | context.withdrawAmt = 100;
261 | onConfirm.trigger(context);
262 | }
263 | });
264 | btnOption3.setText("$200");
265 | btnOption3.setVisibility(View.VISIBLE);
266 | btnOption3.setEnabled(context.balance > 200);
267 | btnOption3.setOnClickListener(new View.OnClickListener() {
268 | @Override
269 | public void onClick(View v) {
270 | context.withdrawAmt = 200;
271 | onConfirm.trigger(context);
272 | }
273 | });
274 | btnOption4.setText("Cancel");
275 | btnOption4.setVisibility(View.VISIBLE);
276 | btnOption4.setOnClickListener(new View.OnClickListener() {
277 | @Override
278 | public void onClick(View v) {
279 | onCancel.trigger(context);
280 | }
281 | });
282 | }
283 | }).whenLeave(new StateHandler() {
284 | @Override
285 | public void call(State state, FlowContext context) throws Exception {
286 | btnOption1.setEnabled(true);
287 | btnOption2.setEnabled(true);
288 | btnOption3.setEnabled(true);
289 | }
290 | });
291 |
292 | SHOWING_TAKE_CASH.whenEnter(new StateHandler() {
293 | @Override
294 | public void call(State state, final FlowContext context) throws Exception {
295 | txtCaption.setText("Take your cash");
296 | txtMessage.setText("Please, take your cash");
297 | txtInput.setVisibility(View.GONE);
298 | btnOption1.setText("Take my $" + context.withdrawAmt);
299 | btnOption1.setVisibility(View.VISIBLE);
300 | btnOption1.setOnClickListener(new View.OnClickListener() {
301 | @Override
302 | public void onClick(View v) {
303 | context.balance -= context.withdrawAmt;
304 | onCashExtracted.trigger(context);
305 | }
306 | });
307 | btnOption2.setVisibility(View.GONE);
308 | btnOption3.setVisibility(View.GONE);
309 | btnOption4.setVisibility(View.GONE);
310 | }
311 | });
312 |
313 | SHOWING_PIN_INVALID.whenEnter(new StateHandler() {
314 | @Override
315 | public void call(State state, final FlowContext context) throws Exception {
316 | boolean canTryAgain = context.invalidPinCounter < 3;
317 |
318 | txtCaption.setText("Invalid PIN");
319 | txtMessage.setText("You entered invalid PIN.\n(" + (3 - context.invalidPinCounter) + " attempts left)");
320 | if (canTryAgain) {
321 | btnOption1.setText("Try Again");
322 | btnOption1.setOnClickListener(new View.OnClickListener() {
323 | @Override
324 | public void onClick(View v) {
325 | onTryAgain.trigger(context);
326 | }
327 | });
328 | btnOption2.setText("Cancel");
329 | btnOption2.setOnClickListener(new View.OnClickListener() {
330 | @Override
331 | public void onClick(View v) {
332 | onCancel.trigger(context);
333 | }
334 | });
335 | btnOption2.setVisibility(View.VISIBLE);
336 | } else {
337 | btnOption1.setText("Ok");
338 | btnOption1.setOnClickListener(new View.OnClickListener() {
339 | @Override
340 | public void onClick(View v) {
341 | onNoMoreTries.trigger(context);
342 | }
343 | });
344 | btnOption2.setVisibility(View.GONE);
345 | }
346 |
347 | btnOption1.setVisibility(View.VISIBLE);
348 | txtInput.setVisibility(View.GONE);
349 | btnOption3.setVisibility(View.GONE);
350 | btnOption4.setVisibility(View.GONE);
351 | }
352 | });
353 |
354 | SHOWING_CARD_LOCKED.whenEnter(new StateHandler() {
355 | @Override
356 | public void call(State state, final FlowContext context) throws Exception {
357 | txtCaption.setText("Your Card has been locked");
358 | txtMessage.setText("You have entered invalid PIN 3 times so I swallowed your card.\n" +
359 | "Mmm... Yummy ;)");
360 | txtInput.setVisibility(View.GONE);
361 | btnOption1.setText("Ok");
362 | btnOption1.setVisibility(View.VISIBLE);
363 | btnOption1.setOnClickListener(new View.OnClickListener() {
364 | @Override
365 | public void onClick(View v) {
366 | onConfirm.trigger(context);
367 | }
368 | });
369 | btnOption2.setVisibility(View.GONE);
370 | btnOption3.setVisibility(View.GONE);
371 | btnOption4.setVisibility(View.GONE);
372 | }
373 | });
374 |
375 | RETURNING_CARD.whenEnter(new StateHandler() {
376 | @Override
377 | public void call(State state, final FlowContext context) throws Exception {
378 | txtCaption.setText("Returning Card");
379 | txtMessage.setText("Thanks for using our ATM\nPlease take your card");
380 | txtInput.setVisibility(View.GONE);
381 | btnOption1.setText("Take the Card");
382 | btnOption1.setVisibility(View.VISIBLE);
383 | btnOption1.setOnClickListener(new View.OnClickListener() {
384 | @Override
385 | public void onClick(View v) {
386 | onCardExtracted.trigger(context);
387 | }
388 | });
389 | btnOption2.setVisibility(View.GONE);
390 | btnOption3.setVisibility(View.GONE);
391 | btnOption4.setVisibility(View.GONE);
392 | }
393 | });
394 | }
395 |
396 | @Override
397 | public void onCreate(Bundle savedInstanceState) {
398 | super.onCreate(savedInstanceState);
399 | setContentView(R.layout.main);
400 |
401 | txtCaption = (TextView) findViewById(R.id.txtCaption);
402 | txtMessage = (TextView) findViewById(R.id.txtMessage);
403 | txtInput = (EditText) findViewById(R.id.txtInput);
404 | btnOption1 = (Button) findViewById(R.id.btnOption1);
405 | btnOption2 = (Button) findViewById(R.id.btnOption2);
406 | btnOption3 = (Button) findViewById(R.id.btnOption3);
407 | btnOption4 = (Button) findViewById(R.id.btnOption4);
408 |
409 | initFlow();
410 | bindFlow();
411 |
412 | flow.start(new FlowContext());
413 | }
414 | }
415 |
--------------------------------------------------------------------------------
/src/au/com/ds/ef/ae/AtmEmulator/UiThreadExecutor.java:
--------------------------------------------------------------------------------
1 | package au.com.ds.ef.ae.AtmEmulator;
2 |
3 | import android.os.Handler;
4 | import android.os.Looper;
5 |
6 | import java.util.concurrent.Executor;
7 |
8 | /**
9 | * User: andrey
10 | * Date: 27/03/13
11 | * Time: 3:00 PM
12 | */
13 | public class UiThreadExecutor implements Executor {
14 | private Handler handler = new Handler(Looper.getMainLooper());
15 |
16 | @Override
17 | public void execute(Runnable command) {
18 | handler.post(command);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------