Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Android 5: Wari 1 2 Introduction • This set of overheads is more than just Wari • It begins with a simple app where the sendMessage() method is small, but contains some significant code • It is a lead-in to debugging • As soon as code gets more complicated you have to be able to debug • It is also a lead-in to the code for Wari 3 • Wari illustrates both code logic and a more complicated layout • There are two perspectives on what’s going on: • First of all, it should be apparent that things are easier because GUI layout work is separate from the Java code • On the other hand, in order to deal with this separation between logic and layout, the implementation of Wari will differ significatly from implementations you’ve seen before 4 • • • • • • • • 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 The OneButtonRecursion app Unhelpful Error Messages Logging Output Wari—General Remarks layout.xml for Wari strings.xml for Wari R.java for Wari MainActivity.java for Wari 5 5.1 The OneButtonRecursion app • The OneButtonRecursion app will be presented in the order in which it was developed: • 1. activity_main.xml, the layout • 2. strings.xml, the resources • 3. Look at R.java, the resources as made available by the system • 4. MainActivity.java, the code for the app 6 • As you might guess, part of what makes the app interesting is the fact that it includes recursion • This is a preview of the fact that recursion will be used in the implementation of Wari 7 • The other thing that makes the app interesting, and relevant to Wari, is that the contents of the text view are changeable, and are treated as an integer • This introduces some syntax, which in turn, introduces the possibility of errors • These errors turn out to be runtime errors, so it behooves us to consider the topic of debugging in Android 8 activity_main.xml for OneButtonRecursion • A screenshot of the layout of the OneButtonRecursion app in the development environment is shown on the following overhead • It is a simple layout, containing a button and a text view 9 10 • This app happens to use relative layout • There is nothing particularly striking about the syntax for that • The XML code for the layout is given on the following overheads for reference 11 • • • • • • • • • <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity" > 12 • <Button • android:id="@+id/button1" • style="?android:attr/buttonStyleSmall" • android:layout_width="wrap_content" • android:layout_height="wrap_content" • android:layout_alignParentLeft="true" • android:layout_alignParentTop="true" • android:text="@string/button1Contents" • android:onClick="sendMessage" /> 13 • • • • • • • • • • <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@+id/button1" android:layout_alignBottom="@+id/button1" android:layout_toRightOf="@+id/button1" android:text="@string/textView1Contents" /> </RelativeLayout> 14 strings.xml for OneButtonRecursion • The strings.xml file for the app is shown on the following overhead • There are no surprises • The button and the text view have string resources associated with them 15 • • • • • • • • • <?xml version="1.0" encoding="utf-8"?> <resources> <string <string <string <string <string name="app_name">One Button Recursion</string> name="action_settings">Settings</string> name="hello_world">Hello world!</string> name="button1Contents">Button1</string> name="textView1Contents">4</string> </resources> 16 R.Java for OneButtonRecursion • As you recall, the R.java file is generated for you • It’s important because it’s the place where you can access resources when writing the Java code for MainActivity • The relevant parts of R.java are shown on the following overhead 17 • • • • • • • • • • • public static final class id { … public static final int button1=0x7f080000; public static final int textView1=0x7f080001; } … } public static final class string { … public static final int button1Contents=0x7f050003; public static final int textView1Contents=0x7f050004; 18 MainActivity.java for OneButtonRecursion • The logic of OneButtonRecursion is quite simple • The TextView is initially set to a value of 4 • When you click the button, you call sendMessage() 19 • sendMessage() checks to see what the current value is • If the value is >0, then a call is made to a method named playNextCup() and the value is passed in 20 • playNextCup() decrements the value • It updates the text area to this new value • If the value has not yet reached 0, it calls itself recursively • The end result of this sequence of actions is that the value in the text view goes 4, 3, 2, 1, 0—too fast for the eye to see • One click of the button causes the text view to go from 4 to 0, recursively 21 • The logic is simple, but the code will look needlessly complex • It repeatedly gets a handle on the text view and passes this around—which is needless, considering there is only one text area • It also includes if statements to check “which button was clicked” when there is only one button 22 • The code is written in this way because it is a preview of what will have to be done to implement Wari • It is clumsy here, but it’s easier to first get an idea of what’s involved in a mindless recursion program with only one text view and one button • Then, in an app with >1 text view and button, it becomes apparent that it is necessary to repeatedly get references and pass them around and check which button was clicked 23 An Import and a Declaration and Initialization for Debugging • The import and TAG definition shown here will be useful for logging • They come at the beginning of the MainActivity class, so they are shown now • Further explanations will be given later • import android.util.Log; • public class MainActivity extends Activity { • private static final String TAG = "OneButtonRecursion"; • … 24 sendMessage() for OneButtonRecursion • The code for sendMessage() is shown on the following overhead • Note that the calls to findViewById() and playNextCup() are floating in space • They are calls on the MainActivity itself • The concept of activities is important and will be pursued further in later sets of overheads 25 • • public void sendMessage(View view) { Button clickedButton = (Button) view; • • • • if(clickedButton == findViewById(R.id.button1)) { TextView cup = (TextView) findViewById(R.id.textView1); int handFull = Integer.parseInt(cup.getText().toString()); • • • • • if(handFull > 0) { cup.setText(handFull + ""); playNextCup((TextView) findViewById(R.id.textView1), handFull); } • } • • • • else { } } 26 playNextCup() for OneButtonRecursion • playNextCup() contains the recursive call • It also contains the uses of Log and TAG • These will be explained in a moment 27 • • • • • • • • • • • • • public static void playNextCup(TextView cupIn, int handFullIn) { handFullIn--; cupIn.setText(handFullIn + ""); if(handFullIn != 0) { playNextCup(cupIn, handFullIn); Log.i(TAG, "In recursion " + handFullIn); } else { } } 28 5.2 Unhelpful Error Messages • When writing the code even for this simple app, I made mistakes (go figure) • It quickly became apparent that I would need to be able to debug in the Android environment • I’m not referring now to the debugging tools in Eclipse—they’re beyond the scope of this course • I’m referring to simply figuring out what was going on 29 • Compiler errors are shown at the bottom of the screen under the “Problems” tab, and they’re marked in red there and flagged in your code • You will find that Eclipse has a helpful “Quick Fix” tool that frequently tells you pretty clearly what’s wrong and how to fix it • These things are illustrated on the following overhead 30 31 • I also got two good (bad) examples with OneButtonRecursion of what can go more seriously wrong and the quandary you’re left in • These were runtime errors, and they illustrated two features of the development environment 32 • 1. In Java programming, you may be used to the fact that even runtime errors show the line numbers in your code where the errors occur • This does not appear to be the case with Android • You do see a trace of calls, but it’s up to you to figure out where that trace intersects your code and the exact location of the problem 33 • 2. The second aspect accounts for the heading of this section • Like with many systems, the error messages are not necessarily helpful • As usual, being able to look the error message up on the Web was very helpful in trying to figure out what it meant 34 Example 1 • Here is a snippet of code that’s in error: • • • int handFull = Integer.parseInt(cup.getText().toString()); … cup.setText(handFull); • Here is the error message you get: • No package identifier when getting value for resource number HideousHexValue 35 • This is not helpful • It seems to suggest some deep problem with the use of resources (like the values in R.java) or maybe with passing parameters, like the activity, around • It doesn’t identify which method call is the source of the problem 36 • The explanation turns out to be much simpler • When writing the incorrect code, I heedlessly assumed that setText() converted whatever parameter was sent to it to a string by an internal call to toString() • This is not so • There is a version of setText() which accepts an integer parameter, where the integer is the id for a resource 37 • The value contained in handFull doesn’t agree with any defined resource id value, so the system has a runtime problem • (Imagine the fun you’d have if, by accident, handFull did contain a valid resource id, and you got inexplicable results instead of a runtime error) 38 • In any case, once you know what’s wrong, the solution is simple • This is the corrected code: • • • int handFull = Integer.parseInt(cup.getText().toString()); … cup.setText(handFull + “”); 39 Example 2 • The same two lines of code contain a call that can also generate an unhelpful runtime error • • • int handFull = Integer.parseInt(cup.getText().toString()); … cup.setText(handFull + “”); • Consider the call to parseInt() 40 • This problem could result anywhere and is the result of carelessness in coding • parseInt() will fail if you apply it to a string that can’t be parsed as an integer • A runtime error can be avoided by careful coding or by putting the call in a try catch block 41 • However, if you write the code as shown • And the text view you’re getting the string from doesn’t contain an integer • You will get this runtime error message: • FATAL EXCEPTION: main java.lang.IllegalStateException • It sounds grievous, doesn’t it? 42 • The tracing of calls that’s presented eventually reaches the parseInt() call, but it’s not immediately apparent what it is about that call that’s causing such a serious problem • The error message obviously doesn’t tell you something simple, like the parameter passed to the method isn’t right • I sorted this out with good, old-fashioned debugging, the subject of the next section 43 5.3 Logging Output • A screenshot of the current example is shown on the following overhead • The playNext() method is shown in the editor • The LogCat tab has been selected at the bottom • This shows the non-graphical output of the app 44 45 • These are the lines of code in the application related to logging • import android.util.Log; • public class MainActivity extends Activity { • • • private static final String TAG = "OneButtonRecursion"; … Log.i(TAG, "In recursion " + handFullIn); 46 • In short, Log is more or less equivalent to System.out in Java • You need to import the Log file • The methods on log are like “i”, which stands for “information” (as opposed to println() for System.out) • These log methods take two strings as parameters, a tag, and the actual output 47 • It’s simplest to just define a TAG so that every line from a given app is clearly identified in the LogCat • Then at strategic places put calls to Log.i() to see what’s happening • Eclipse has fancier debugging tools, but I’ve always done it by hand in this way 48 An Example • On the following overhead is a block of code that gave me a runtime error • As noted earlier, the runtime error messages are not always as helpful as they might be • I couldn’t tell what exactly was wrong 49 • TextView capturedCup = (TextView) activityIn.findViewById(R.id.textView16); • int capturedCount = Integer.parseInt(capturedCup.getText().toStri ng()); • capturedCount += seedCount; • capturedCup.setText(capturedCount + ""); 50 • The code on the following overhead shows how I picked the block apart and was able to identify exactly which line and which call was causing the problem • (It was the call to parseInt()) 51 • • • /* Debugging code /* Log.i(TAG, "1"); • TextView capturedCup = (TextView) activityIn.findViewById(R.id.textView16); • Log.i(TAG, "2, capturedCup id: • CharSequence tempText = capturedCup.getText(); • Log.i(TAG, "3"); • String tempString = tempText.toString(); • Log.i(TAG, "4, tempString contents of capturedCup: • int capturedCount = Integer.parseInt(tempString); • Log.i(TAG, "5"); • capturedCount += seedCount; • Log.i(TAG, "6"); • capturedCup.setText("x"); //(capturedCount + ""); • • Log.i(TAG, "7"); */ */ " + capturedCup); " + tempString); 52 5.4 Wari—General Remarks 53 Wari in CSCE 202 • Obviously, the point of using Wari as an example is that it needs no introduction • For those who didn’t take CSCE 202 from me, go to the link for that course on my Web page for full information 54 • You may recall these characteristics of the implementation of Wari in CSCE 202 • There was a Cup class • The board consisted of an array of cups • The cups contained references to another cup so that they could be linked together in circular fashion, the way play progressed around the board 55 • The cups also had text fields • In assembling the overall graphical user interface for the application, the text fields belonging to the cups were presented in panels on the screen • The design was approaching a model-viewcontroller design, but it’s true that components were still tightly integrated 56 • The following observation might resonate more with people who were less successful than with those who were more successful • In Java, with the information given in CSCE 202, it was not so hard to simply create a “dead” graphical user interface, with all of the visual components displayed on the screen but no functionality • (This was the option that gave you 40 points even if you could go no further…) 57 Wari in Android • By now you may have noticed the general plan for developing simple Android apps: • Using widgets, develop a graphical layout (activity_main.xml) • You can acquire handles on these widgets through R.java • However, the widgets themselves have a life of their own independent of the MainActivity.java source file 58 • Although it may be possible, there’s been no consideration of the following: • Constructing arrays of widgets • Devising application classes where instance variables of those classes are widgets • Trying to link widgets or objects containing them together • In other words, there’s been no indication so far that an Android version of Wari would mimic the straight Java implementation 59 • That is the starting point for the Wari example given in this set of units • The intention is to implement roughly the same functionality • But the intention is not to try and duplicate the earlier implementation • The goal is to implement the functionality while leaving the widgets as independent graphical items in the interface 60 • With these constraints in the Android environment a radically different solution to the code logic presents itself • The fundamental beginning step in Wari is picking a cup to play • If the cup contains seeds, you then move to the next cup 61 • If the cups aren’t linked together, you can implement moving from one cup to another in this way: • Have a sequence of if/else if statements that encapsulate this logic, hardcoding the id’s of the cups: • If you’re on cup x, the next cup is y; else if you’re on cup y, the next cup is z; and so on, all the way around the board 62 • You can progress around the board as far as needed and no further by doing recursion on the value of handFull that you pick up in the first cup • The stopping condition is easy and natural: • When the handFull is empty (== 0) you’re done 63 • This unit contains the first assignment, where you’re expected to implement something Wari-like as an Android app • In the following sections further explanations will be given of how to do this, some example code will be shown, but complete code will not be given or posted 64 • It will be up to you to fill in the missing spaces and get it to work • I know it can be done because I did it • The incomplete example work you’re being shown is taken from the code of a working app 65 5.5 layout.xml for Wari • Step 1: Create a layout that presents a Wari-like interface with the necessary components • I used buttons for play and text views to hold the contents of cups • I decided to use a table layout • Android has something called a grid layout, but it’s not exactly the same as a Java grid layout • The Android table layout is similar to a Java grid layout 66 • I decided that it would be helpful to use the graphical tools in Android rather than trying to master the XML syntax • This was not trouble-free 67 • More than once I had to start over and create components in a different order so that the default names they received would be consistent with the logic of their use in my code • There were 12 buttons and 12 text views in the layout • I wanted them numbered 1-12 in the order they would be played in the game 68 • I also discovered that my machine was underpowered or something • Some of the tools caused my system to crash • The main lesson there was don’t touch those tools • I tried to upgrade to a different machine, but it was impossible to install the Google driver, so I was stuck with the lesser of two evils 69 • The following overhead shows a screenshot of my layout in the development environment • I’m showing you this in order to remind you of the graphical tools • My layout consisted entirely of text view and small buttons • Notice that among the tools is an option to choose a layout • This is where I found the table layout 70 71 • The following overheads show the XML code that was generated up through the first row of the table layout • The full file goes on at length • This is where you can check to see what default names your widgets are being given • They are numbered in creation order • Your code doesn’t have to be the same, but it will at least be similar 72 • • • • • • <?xml version="1.0" encoding="utf-8"?> <!-- This is the activity_main.xml file for Wari. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/androi d" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > • • • • • <TextView android:id="@+id/textView13" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/textView13Contents" /> • • • • • <TextView android:id="@+id/textView14" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/textView14Contents" /> 73 • • • • • • • <TableLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <TableRow android:id="@+id/tableRow1" android:layout_width="wrap_content" android:layout_height="wrap_content" > • • • • • • • <Button android:id="@+id/button1" style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/button1Contents" android:onClick="sendMessage" /> • • • • • <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/textView1Contents" /> 74 • • • • • • • • • • • • • • • • • • <TextView android:id="@+id/filler1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/filler" /> <TextView android:id="@+id/textView12" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/textView12Contents" /> <Button android:id="@+id/button12" style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/button12Contents" android:onClick="sendMessage" /> 75 5.6 strings.xml for Wari • Creating a strings.xml file for an app isn’t too difficult • Largely you can just copy the layout.xml file and then edit it • It already contains all of the named items that were created in the layout that are referred to in strings.xml • In strings.xml you give them initial values 76 • To that you add the right syntax for a strings.xml file • You can also add any other strings necessary for your app • A subset of the code for strings.xml for Wari is shown on the following overhead 77 • • <?xml version="1.0" encoding="utf-8"?> <resources> • • • • • <string <string <string <string … • • • <string name="textView1Contents">4</string> <string name="textView2Contents">4</string> … • <string name="filler">" • </resources> name="app_name">Wari</string> name="action_settings">Settings</string> name="button1Contents">P1, C1</string> name="button2Contents">P1, C2</string> "</string> 78 5.7 R.java for Wari • As you recall, R.java is auto-generated • If your layout and strings files were complete, R.java will have everything you need in it • The point is that when writing the code for MainActivity.java, you refer to R.java • A subset of R.java is shown on the following overhead 79 • • • • • • • • • • … public final class R { … public static final class id { public static final int action_settings=0x7f080028; public static final int button1=0x7f080003; public static final int button10=0x7f080013; public static final int button11=0x7f08000d; public static final int button12=0x7f080007; public static final int button2=0x7f080009; 80 5.8 MainActivity.java for Wari • There is nothing special about the onCreate() and onCreateOptionsMenu() methods for Wari • They’re just copied as usual • There’s also nothing unusual about the imports • Some need to be added so that the code can work with various classes, but they won’t be detailed • The big deal is what’s in the sendMessage() and playNextCup() methods 81 sendMessage() for Wari • As always, sendMessage() is the method called on a button click • This is critically important to this application: • Each of the 12 buttons in Wari is linked to the one sendMessage() method 82 • This goes back to a discussion that occurs at the end of CSCE 202 • You may recall that it’s possible to have one event and many listeners • It’s also possible to have many events and one listener 83 • You may also recall that in the development of Wari in swing, the possibility of having a listener full of if statements was mentioned in passing and rejected as not the ideal solution • We have now come full circle • sendMessage() is like a common listener for clicks on multiple different buttons 84 • sendMessage() determines which button was clicked • There is then a series of if statements • In each if statement, effectively the same logic is implemented • The different cases only differ by which button/cup/next cup are involved 85 • As a result, the code is highly redundant • No claim is made that this is the ideal solution • It is presented as a quick and dirty solution which is relatively easy to understand in the context of Android widgets • The redundancy actually makes it easy to write • You just copy and paste and change the numbers identifying the buttons and cups 86 • The first two if cases for the sendMessage() method are shown on the following overhead • The full method just continues the pattern • If you need more knowledge in order to understand what’s going, you’ll have to figure it out • In other words, this is part of what you’ll do for the assignment 87 • • public void sendMessage(View view) { Button clickedButton = (Button) view; • • • • if(clickedButton == findViewById(R.id.button1)) { TextView cup = (TextView) findViewById(R.id.textView1); int handFull = Integer.parseInt(cup.getText().toString()); • • • • if(handFull > 0) { cup.setText("0"); playNextCup(this, (TextView) findViewById(R.id.textView2), handFull); } } else if(clickedButton == findViewById(R.id.button2)) { TextView cup = (TextView) findViewById(R.id.textView2); int handFull = Integer.parseInt(cup.getText().toString()); • • • • • • • • • • • • • if(handFull > 0) { cup.setText("0"); playNextCup(this, (TextView) findViewById(R.id.textView3), handFull); } } … 88 • There is one last thing to mention because it’s a segue into an important aspect of the playNextCup() method, which will be the next topic • Note the calls to findViewById() • This has come up before, but it bears repeating: • These calls are floating in space 89 • When the app runs, an instance of the MainActivity class is what’s running on the Dalvik Virtual Machine • The calls on the implicit parameter are on this activity, the MainActivity • This is relevant to playNextCup() and the concept is important overall and will be discussed in greater depth in the future 90 playNextCup() for Wari • playNextCup() is the method that’s called if a player clicks a cup that is not empty • It is a recursive method that jumps to the next cup in line • Each successive recursive call jumps to the succeeding cup on the board • Seeds continue being dropped in each cup until handFull is empty • This is the condition that ends the recursion 91 • playNextCup() is redundant, like sendMessage() • Just like in sendMessage(), it’s necessary to know which cup is being played • Which is the next cup is determined by which is the current cup • Since the cups are found by their id, there has to be a separate case for each of the 12 cups on the board 92 • Again, the fact that the code is redundant actually makes it relatively easy to write • Figure out one of the cases • Then copy and paste and change the numbers identifying the cups 93 • The code for this method does have an additional characteristic that needs to be brought out • It’s already evident in the signature line, which is shown here: • public static void playNextCup(Activity activityIn, TextView cupIn, int handFullIn) 94 • Not only does the logic of play and recursion require that the cup and handFull be passed in • The first parameter passed to the method is an activity • If you go back to sendMessage(), the calls look like this, for example: • playNextCup(this, (TextView) findViewById(R.id.textView3), handFull); • The activity that is passed in is the MainActivity 95 • Passing in the parameter supports calls like these in the body of the method: • activityIn.findViewById(R.id.textView1) • findViewById() is called on the activity • findViewById() was also called on the activity in sendMessage(), but it was a call floating in space • It wasn’t necessary to make the call on “this” 96 • In playNextCup() you will get a compiler error if you don’t pass in the activity so you can call findViewById() on it • This is the message: • “Cannot make a static reference to the nonstatic findViewById(int) from the type Activity” 97 • This error message is more helpful than the ones previously looked at • playNextCup() is a recursive method which is declared static • The error message is telling you that in a static method there is no “this”, there is no object, which findViewById() can be called on • Therefore, in order to make the implementation work, you have to pass in MainActivity when making the call to playNextCup() 98 • The first two if cases for the playNextCup() method are shown on the following overheads • The full method just continues the pattern • If you need more knowledge in order to understand what’s going, you’ll have to figure it out • In other words, this is just like sendMessage() • Doing this is part of what you’ll do for the assignment 99 • • • • • • • • • • • • • • • • • • • • • public static void playNextCup(Activity activityIn, TextView cupIn, int handFullIn) { handFullIn--; if(cupIn == activityIn.findViewById(R.id.textView1)) { TextView nextCup = (TextView) activityIn.findViewById(R.id.textView2); int seedCount = Integer.parseInt(cupIn.getText().toString()); seedCount++; cupIn.setText(seedCount + ""); if(handFullIn != 0) { playNextCup(activityIn, nextCup, handFullIn); } else if(seedCount == 2 || seedCount == 3) { TextView capturedCup = (TextView) activityIn.findViewById(R.id.textView14); int capturedCount = Integer.parseInt(capturedCup.getText().toString()); capturedCount += seedCount; capturedCup.setText(capturedCount + ""); } } 100 • • • • • • • • • • • • • • • • • • • else if(cupIn == activityIn.findViewById(R.id.textView2)) { TextView nextCup = (TextView) activityIn.findViewById(R.id.textView3); int seedCount = Integer.parseInt(cupIn.getText().toString()); seedCount++; cupIn.setText(seedCount + ""); if(handFullIn != 0) { playNextCup(activityIn, nextCup, handFullIn); } else if(seedCount == 2 || seedCount == 3) { TextView capturedCup = (TextView) activityIn.findViewById(R.id.textView14); int capturedCount = Integer.parseInt(capturedCup.getText().toString()); capturedCount += seedCount; capturedCup.setText(capturedCount + ""); } } … 101 Summary and Assignment • • • • The summary is simple: Can you do this? The assignment is equally simple: Do this. Implement Wari or Togiz Kumalak You get to choose the number of cups per side and the initial number of seeds per cup, as long they are >= 6 and 4, respectively 102 • You will turn this in by demonstrating your app to me on your tablet or whatever Android device you’ve decided to develop on • You can come to my office to do this • You can do it any time on or before the due date • I’m begging you, please, don’t push this beyond the due date • I make this humble entreaty because it’s for your own good… 103 The End 104