MP3: Refactoring and More JSON

In MP2, you connected the app to our game server to get and respond to invitations. In Checkpoint 3, you’ll make it possible to invite people to new multiplayer games and refactor some of your old code to improve it.

When you first started working on the MP, we hadn’t covered object-oriented programming yet, so the target mode functionality had to be implemented using only arrays and imperative programming. As you discovered, that was not ideal. Programmers often learn or realize better approaches to problems already solved and revisit old code to improve it. This process is called refactoring. In this checkpoint we’ll refactor some of the code you wrote in MP0 and organize some relevant logic into a class that can more easily be used in the next checkpoint.

Deadlines for Checkpoint 3 vary by your deadline group. MP3 is due at:

  • 8 PM on Sunday, 11/3/2019 for the Blue Group: all labs starting at 2 PM or earlier

  • 8 PM on Monday, 11/4/2019 for the Orange Group: all labs starting at 3 PM or later

Additionally, 10% of your grade is for submitting code that earns at least 40 points by 8 PM on Sunday 10/27/2019 (Blue) or Monday 10/28/2019 (Orange). Late submissions are subject to the MP late submission policy.

1. Learning Objectives

On MP3 you will:

  • Work with and improve older code

  • Make web requests that deliver additional information to the web API

We will continue to exercise your class design and Android UI skills (including repeatable UI) like in previous checkpoints.

2. Assignment Structure

Like in previous checkpoints, we distribute the new MP3 files in a patch to your existing repository.

For information on all classes present after the completion of Checkpoint 3, refer to our official Javadoc.

2.1. Obtaining MP3

To start working on Checkpoint 3, follow these steps:

  1. Commit your work just in case you run into any problems applying the patch.

  2. Download the MP3 patch file.

  3. In Android Studio, do VCS | Apply Patch.

  4. Select the downloaded patch file and press OK to apply it.

  5. Change the checkpoint setting in grade.yaml to 3.

  6. Do File | Sync Project with Gradle Files.

A new run configuration called Test Checkpoint 3 will run the Checkpoint 3 test suite. If you run into a problem obtaining MP3, please get help immediately.

Because the new test suite refers to a class that you need to add, your project will not be able to compile immediately after applying the patch. This will be fixed as you start working on the checkpoint as described in Your Goal.

2.2. Late Submissions

Like in previous checkpoints, setting useProvided in grade.yaml to true causes the app to use our provided components rather than your implementations. For Checkpoint 3, we provide TargetVisitChecker (with both MP0-style and refactored signatures), AreaDivider, the launch activity, the main activity, the local game setup UI in NewGameActivity, and the gameplay logic in GameActivity.

If you didn’t finish the game setup activity in MP1, you can still do MP3. Our provided components will try to fuse the game setup UI you have with our version of the missing parts. However, it would be ideal to get testSetupUI from MP1 done as soon as possible so that you can set useProvided back to false. If you’re curious about how the installation of provided components works, you can post on the forum.

As usual, you can always go back and make submissions to previous checkpoints by changing checkpoint in grade.yaml.

3. Tools and Resources

This section provides you with tools and information that you will need to complete the features in Your Goal.

3.1. Lists

Java arrays are useful for storing multiple values of the same type, but an array’s length cannot be changed after its creation. Often it’s not known in advance how many entries a collection will have, or the collection has items added to or removed from it. In those situations, arrays are inconvenient.

Fortunately, Java provides lists: objects that hold dynamically resizable lists of things. All lists are instances of List, but List is an interface rather than a concrete type. To create a list you create an instance of a List implementation, the most common of which is ArrayList. This is an example of polymorphism.

When you declared arrays, you put the element type before square brackets, like String[] for an array of strings. List variables are declared like List<String>, with the element type in angle brackets. To declare a list-of-strings variable and initialize it to an empty ArrayList, you would use code like this:

List<String> names = new ArrayList<>();

The angle brackets on the right side of the assignment can be empty, which means that the actual list object created holds the same type of values as the variable is declared to contain.

The List type defines many functions that are usable on all lists. The most useful ones are:

  • add, which adds one element to the end of the list (increasing its size by one)

  • size, which returns the current length of the list

  • get, which gets the element at the specified position in the list

  • remove, which removes an element from the list (decreasing its size by one)

For example:

System.out.println(names.size()); // Prints 0 - the list is empty initially
names.add("Chuchu");
System.out.println(names.size()); // Prints 1 - now the list has one element
names.add("Xyz");
System.out.println(names.size()); // Prints 2 - now the list has two elements
System.out.println(names.get(0)); // Prints "Chuchu", the first element in the list
names.remove("Chuchu");
System.out.println(names.get(0)); // Prints "Xyz", the first element now that Chuchu was removed
System.out.println(names.size()); // Prints 1 - the list has one element anymore

Lists can be iterated over with the enhanced for loop:

for (String name : names) {
    // Do something with name?
}

Types like List and ArrayList will need to be imported before you can use them. Android Studio can help with this: if you press Tab to autocomplete a type name, any needed import statement will automatically be added at the top of the file.

3.2. Spinners

Dropdown lists are useful when the user has a handful of possible choices. Android provides a Spinner control in the Containers tab of the Palette that produces a dropdown list. It is possible to populate the dropdown dynamically at runtime, but it is much easier to set the entries attribute in the Attributes pane to a string array resource. Then each string in that array resource will be one entry in the dropdown.

Some useful methods of Spinners:

  • To get the current selection, you can call the getSelectedItemPosition function, which returns the index of the selected item. For example, 0 means that the user selected the first entry.

  • To programmatically change which item is selected, use the setSelection function, which takes the index of the item to select.

  • To register a handler that will be run when the user changes the selected item, use setOnItemSelectedListener:

// Suppose spinner is a Spinner variable
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    @Override
    public void onItemSelected(final AdapterView<?> parent, final View view,
                               final int position, final long id) {
        // Called when the user selects a different item in the dropdown
        // The position parameter is the selected index
        // The other parameters can be ignored
    }
    @Override
    public void onNothingSelected(final AdapterView<?> parent) {
        // Called when the selection becomes empty
        // Not relevant to the MP - can be left blank
    }
});

If you’d like more information, you can see Android’s guide to spinners.

3.3. Writing JSON with Gson

In Checkpoint 2, you used Gson to read data from parsed JSON. In this checkpoint, you’ll need to create JSON objects to send to the server.

Gson can help with this too. To create a new JSON object, use new JsonObject(). To add a single, simple value like a string or number as a property on an object, call the object’s addProperty function, passing the property name and value. For example, this code builds a JsonObject corresponding to the first MP2 JSON example:

JsonObject point = new JsonObject();
point.addProperty("latitude", 40.109187);
point.addProperty("longitude", -88.227213);

To add a more complicated value like an array or other object as a property of an object, use add instead.

Likewise, to create a JSON array, use new JsonArray(). Its add function will add an entry to the end of the array.

This code reconstitutes the more complicated JSON object from the MP2 writeup:

JsonObject cs125 = new JsonObject();
cs125.addProperty("name", "CS 125");
cs125.addProperty("enrollment", 800);

JsonObject location = new JsonObject();
location.addProperty("name", "Foellinger Auditorium");
location.addProperty("allows_food", false);
location.addProperty("latitude", 40.105952);
location.addProperty("longitude", -88.227204);
cs125.add("location", location);

JsonArray lectureDays = new JsonArray();
lectureDays.add("Monday");
lectureDays.add("Wednesday");
lectureDays.add("Friday");
cs125.add("lecture_days", lectureDays);

Gson objects stringify to the JSON text they represent, so you can pass them to System.out.println to see what JSON you’ve built. It will be condensed onto one line and difficult to read, so you may find it helpful to paste that into a JSON formatter to see its structure more easily.

3.4. Our API Documentation

To create a multiplayer game, your app will need to make a POST request to the /games/create endpoint. Since there is a lot of game information rather than just a game ID, the game configuration will need to be uploaded to the server as the body (payload) of the request. The body should be a JSON object (Gson JsonObject instance) with these properties:

  • mode (string) is the game mode, either "target" or "area"

  • invitees (array of objects) is the list of players invited to the game, including the user; each object should have these properties:

    • email (string) is the invitee’s email address

    • team (integer) is the TeamID code for the role/team the user is invited to

  • For target mode only, proximityThreshold (integer) is the proximity threshold in meters

  • For target mode only, targets (array of objects) is the list of targets in the game; each object should have these properties:

    • latitude (double) is the latitude of the target

    • longitude (double) is the longitude of the target

  • For area mode only, cellSize (integer) is the cell size in meters

  • For area mode only, areaNorth, areaEast, areaSouth, and areaWest (all doubles) are the latitude/longitude bounds of the area

You may find the example target mode body and example area mode body helpful.

If the game is created successfully, the server’s response will be a JSON object with a single game property whose value is the (string) game ID.

If the game cannot be created, your error handler will be run. The getMessage function on the error object returns a human-readable string describing the problem.

3.5. Extra Credit API Documentation

If you are attempting the extra credit feature to allow the user to load a predefined set of targets, your app will need to be able to fetch the preset targets lists from the server. Those are accessible by a GET request to the /presets endpoint. The server’s response will be a JSON object containing this property:

  • presets (array of objects) is the list of preset options; each object has these properties:

    • name (string) is the human-readable name of the preset

    • targets (array of objects) is the list of targets in the preset; each has at least these properties:

      • latitude (double) is the target’s latitude

      • longitude (double) is the target’s longitude

You may find this example response helpful. Do not assume that the note property will always be present on target objects, but feel free to do anything you like with it if it’s there. You can always ignore it completely.

3.6. Reverting Changes with Git

Version control systems like Git make it possible to retrieve older versions of your code, which is very useful if you accidentally damage a file. Android Studio integrates with Git to allow you to undo (revert) changes with its UI.

If you would like to put a file back to how it was at the last commit, right-click it in the Project pane and choose Git | Revert. This brings up the Revert Changes dialog, where you can select any additional files you would like to revert. Reverting a file throws away all changes to it since the last commit and is usually not reversible.

For a more surgical approach, Android Studio highlights changed regions of files with colored bars or gray triangles in the left margin of the code editor. Clicking one of these decorations produces a toolbar with a back arrow (Rollback Lines) button that reverts just the highlighted lines to how they were in the last commit. This rollback method may sometimes be reversible with Ctrl+Z, but you should still be certain that you want to throw away your changes.

4. Your Goal

When you’re done with Checkpoint 3, the game setup activity will allow the user to invite other people and assign their roles/teams. The user will be able to press locations on a map to specify the targets of a target mode game. Creating a game will upload its configuration to the server and make it visible to the invitees, who can then accept or decline the invitation using their app.

While setting up a target mode game, the user might see UI like this:

completed games lists UI

A video tour of MP3 created by the CA captains 1 is available:

Unless otherwise noted, you can do these sections in any order.

4.1. Target Class

The new test suite, Checkpoint3Test, is initially unable to compile because it refers to a Target class which does not exist, so this must be fixed first. We will be using the Target class primarily in the next checkpoint to help manage a target marker on the map, since the Checkpoint 0 approach of passing coordinates to a changeMarkerColor function is unwieldy 2.

Create the class by right-clicking the package containing all your other Java source files, choosing New | Java Class, entering Target in the Name field, and clicking OK.

Like in previous checkpoints, make sure that the file was created inside our package in the main source set and that it was added to Git.

To see the needed public members of this class, refer to our official Javadoc. You will need to store a Google Maps Marker object in a private instance variable.

To place a marker on a Google map, use the map’s addMarker function 3:

// Suppose position is a LatLng variable
MarkerOptions options = new MarkerOptions().position(position);
// Set any other options you like?
Marker marker = map.addMarker(options);

To change the color of a marker after it has been created, use its setIcon function 4:

// Suppose hue is a hue value like the constants defined on BitmapDescriptorFactory
BitmapDescriptor icon = BitmapDescriptorFactory.defaultMarker(hue);
marker.setIcon(icon);

After completing this task, testTargetClass will pass. You may optionally rework your target mode logic in GameActivity to take advantage of this new class, but otherwise you will not need it again in this checkpoint.

4.2. LatLng Refactor

Functions that take eight parameters, especially all of the same type, can be difficult to use. This is even more unfortunate when some of the parameters really belong together, packaged up into objects. Now that you know how to use objects like the Google Maps SDK’s LatLng, we’ve rewritten LinesCrossDetector.linesCross to accept the lines' endpoints as LatLng objects. 5

Download the LineCrossDetector patch and apply it with VCS | Apply Patch like you do with the checkpoint patches. This will introduce compilation errors in your functions that use linesCross! You need to adjust those to use the improved version’s signature, calling it with four LatLng positions.

If Android Studio is unable to apply the patch due to how you fixed the checkstyle errors in Checkpoint 0, or if there are compilation errors inside LineCrossDetector, you can instead copy-paste the updated class from this Gist.

Similarly refactor the addLine function in GameActivity to take two LatLng endpoints rather than four double coordinates. You will need to update the function’s callers to be compatible with its new signature.

If you make a mistake while refactoring and want to put a file back to how it was at the last commit, see the section on reverting changes.

After completing these tasks, testLatLngRefactor will pass.

4.2.1. Extra Refactoring Practice

You may optionally refactor your TargetVisitChecker methods to take a LatLng[] in place of the two double[]s. Updated Javadoc is available. The Checkpoint 0 tests are forward-compatible with this change. After doing that, you’ll probably want to use the getPositions function of DefaultTargets rather than getLatitudes and getLongitudes in your GameActivity target mode setup.

Better yet, you may take advantage of your new list skills to keep track of the target mode game state entirely inside GameActivity. If TargetVisitChecker is removed, the Checkpoint 0 test results will be all-or-nothing based on the result of testTargetModeGameplay.

TargetVisitChecker will be removed entirely in the next checkpoint and GameActivity will be significantly remodeled then, so don’t get too attached to either.

4.3. Targets Map

In Checkpoint 1 you made it possible for the user to select the area for area mode by panning and zooming a Google Maps control. Now you’ll add a similar map control that allows the user to choose the targets for a target mode game by pressing to add a target and clicking a target to remove it.

To add another map to the game setup activity, open activity_new_game.xml in the UI designer, copy the areaSizeMap fragment, paste it inside the target mode settings layout 6, and change the copy’s ID to targetsMap.

In NewGameActivity's onCreate we provided this chunk of code:

SupportMapFragment areaMapFragment = (SupportMapFragment) getSupportFragmentManager()
        .findFragmentById(R.id.areaSizeMap);
areaMapFragment.getMapAsync(newMap -> {
    areaMap = newMap;
    centerMap(areaMap);
});

This gets a reference to the areaSizeMap fragment and registers a handler that will be run when Google Maps creates the map. When the map is available (in getMapAsync), it is stored in the instance variable areaMap and passed to our centerMap function for centering on Campustown.

Declare another instance variable to store the targets map, then duplicate the above section of code to similarly prepare your targetsMap fragment. Make sure to change the findFragmentById parameter to operate on your new fragment. You’ll then be able to see in the app that the map that appears for target mode setup is centered on Campustown just like the area mode setup one.

To keep track of the targets added so far, declare an instance variable to hold a list of Google Maps markers: a List<Marker>. Initialize it to a new, empty list. Add some code to your new getMapAsync handler to make the user’s actions on the map add targets:

  • Register a long-press handler on the targets map by calling setOnMapLongClickListener. The handler receives a LatLng object specifying the point that was pressed. When that happens, create a marker at that position and add it to the list. Again see the implementation of the placeMarker function in GameActivity for how to place a marker on a map.

  • Likewise register a setOnMarkerClickListener handler, which is passed a Marker that the user clicked. Remove that marker from the map by calling its remove function and remove it from the targets list you declared as an instance variable.

In summary, you’ll want code like this inside the new getMapAsync:

targetMap.setOnMapLongClickListener(location -> {
    // Code here runs whenever the user presses on the map.
    // location is the LatLng position where the user pressed.
    // 1. Create a Google Maps Marker at the provided coordinates.
    // 2. Add it to your targets list instance variable.
});

targetMap.setOnMarkerClickListener(clickedMarker -> {
    // Code here runs whenever the user taps a marker.
    // clickedMarker is the Marker object the user clicked.
    // 1. Remove the marker from the map with its remove function.
    // 2. Remove it from your targets list.
    return true; // This makes Google Maps not pan the map again
});

After completing this task, testTargetMap will pass.

4.4. Invitees UI

When setting up a game, the user should be able to decide who is invited to the game and what roles they have. An invitee can be added by entering their email into a text box and pressing the Add button. All players (users involved in or invited to the game, including the app’s user), should be shown in a list with a dropdown to set the role, which defaults to observer. It should be possible to remove an invitee—but not the user—by pressing the Remove button in their row.

To make this possible, we will need three new UI elements in activity_new_game.xml:

  1. An email text box (from the Text tab) with ID newInviteeEmail to allow the user to enter an invitee’s email

  2. A button with ID addInvitee to actually add the invitee to the list

  3. An initially empty vertical LinearLayout with ID playersList to hold the list of players

To display one player entry, we have provided chunk_invitee.xml in the patch. You do not need to modify it, though you may customize it if you like. To store information about one player, we have provided the Invitee class. These will be used in NewGameActivity to make the invitees UI work.

Add an instance variable to NewGameActivity to store the list of players, that is, a List<Invitee>. onCreate should initialize it to an empty list and then add an Invitee representing the user with the role of observer. You should create two helper functions to make the players list UI work:

4.4.1. Update Players UI

This function is responsible for repopulating the players list in the UI with the information stored in the players list instance variable.

First it should removeAllViews from the players LinearLayout. Then, much like in Checkpoint 2, it should go through the list variable and add a chunk to the LinearLayout for each player. We provided three views in that chunk:

  • inviteeEmail is a TextView whose text should be set to the player’s email.

  • inviteeTeam is a Spinner to let the user see and change the player’s role. Its selection should be set to the player object’s team ID. When its selection is changed by the user, the player object’s team ID should be updated to match. See our guide to the relevant spinner functions.

  • removeButton is a Button that removes the invitee. It should be gone for the first entry in the list (the user, since the user shouldn’t able to leave their own game). When clicked, it should remove the player object from the list variable and refresh the players UI list.

When accessing these views, make sure to call findViewById on the chunk you inflated inside your loop so that you get a reference to the view from the specific chunk you’re currently building.

This function should be called by onCreate after adding the user to the players list variable so that the initial UI is set up and the user can choose their own role.

4.4.2. Add Invitee

This function should be called when the user presses the addInvitee button.

If the newInviteeEmail text box is not empty, a new invitee object with the entered email address and the role of observer should be added to the players list instance variable. The email text box should be made empty so that the user can enter the next invitee. Then the players UI should be updated by calling the other helper function so that the change is visible.

After completing this task, testInvitees will pass.

4.5. Game Creation API Request

The targets map and invitees UI can be done in either order. Once they’re both ready, the data they solicit from the user can be submitted to the server to create a multiplayer game.

When the Create Game button is clicked, build a JsonObject according to our API documentation for the game creation endpoint. You will need to include:

  • the configuration you made possible in Checkpoint 1, plus

  • data from the targets map (if in target mode) and

  • players/invitees list.

Much of your Checkpoint 1 logic can still be used; you’re just putting the data in a JSON object rather than an intent.

When your game JSON object is ready, POST it to the game creation endpoint. If the request succeeds, launch GameActivity passing the game ID from the response as the game extra of the intent, then finish NewGameActivity. If the request fails, show a toast (like in the example web request's error "handler") that displays the error’s message 7 so the user knows what went wrong.

Note that GameActivity should only be launched once the request completes, not immediately when the user presses the button. The Checkpoint 1 data no longer needs to go in the intent, though you can put it there if you’d like the game to keep working in the meantime before Checkpoint 4 fixes everything.

After completing this task, testApiRequest will pass. Nice work! If you’re up for a challenge, you can continue on to the extra credit section below.

4.6. Extra Credit: Target Presets

Challenge problem! This is extra credit because it takes a bit more work and tinkering. It can be done before the game creation API request, but you will need to have the targets map working first.

Many users won’t want to spend a lot of time picking out enough targets for an interesting target mode game. To make it easier to add a set of targets, the app could have several suggested lists of targets and allow the user to add an entire suggested list at once.

Inside the target mode settings group, add a button with text "Load Preset" and ID loadPresetTargets. When it is clicked, fetch the list of presets from the server. When the request completes, create and show an AlertDialog to list the options. Refer to Android’s AlertDialog guide for details.

This patch provides a chunk_presets_list.xml layout resource which you can inflate with a null parent 8 and pass to the dialog builder’s setView function. For each preset option, add a RadioButton inside the provided RadioGroup (ID presetOptions), with the radio button’s text set to the preset’s name. This is the one place in the MP where you should create an individual view dynamically using new. The constructors for most Android views take a context, which can be the activity: this.

The alert dialog’s positive button should be labeled "Load"; its negative button should be labeled "Cancel." The dialog might look like this:

a list of preset options

If the positive button (Load) is pressed with a preset selected, all existing targets should be removed and all the targets from the selected preset should be added. There are multiple ways to associate a preset with a radio button; you may find getTag and setTag helpful. If the user presses Cancel or presses Load without selecting a preset, do nothing and the dialog will be dismissed by default.

If you complete this task, testTargetPresets_extraCredit will pass and you’ll have earned 20% extra credit!

5. Grading

As always, 100 points is full credit on the checkpoint. But on MP3 there are 120 points available, broken down as follows:

  • 15 points for the Target class

  • 10 points for refactoring addLine and the uses of LineCrossDetector

  • 15 points for the targets map

  • 20 points for the invitees UI

  • 20 points for the create-game web request

  • 20 points of extra credit for the optional Load Preset feature

  • 10 points for passing checkstyle inspection

  • 10 points for submitting 9 code that earns 40 points by 8 PM on your early deadline day

If you missed a deadline in a previous checkpoint, doing the extra credit here is a great way to earn some of those points back!

Your app will be tested by Checkpoint3Test. Feel free to look through that class’s code to see what the test suite tries to do with your app. Post on the forum for clarifications about what exactly is expected.

6. Cliffhanger

Because the game setup screen submits the game configuration to the server instead of passing it to the game activity, gameplay is probably pretty broken at the moment. In the next and final checkpoint, we’ll finish the app by connecting the game activity to the server!

7. Cheating

By now you should be familiar with the cheating policies from the syllabus. Collaborating in a human language about how to approach the problems is encouraged, but sharing your code with anyone not currently on the course staff constitutes cheating.

CS 125 is now CS 124

This site is no longer maintained, may contain incorrect information, and may not function properly.


Created 10/24/2021
Updated 10/24/2021
Commit a44ff35 // History // View
Built 10/24/2021 @ 21:29 EDT