Complete the android application to include activity sync support using the Half Sync/Half Async Pattern. Overlay this with a Mediator implementation.
This is v5 of the pacemaker-android project from the last lab:
You can import this for reference purposes, or keep working with your own version.
We need re-orient how we connect to the service and synchronise with the user list maintained there.
This approach requires these new methods in PacemakerApp:
public void connectToPacemakerAPI(Context context)
{
PacemakerAPI.getUsers(context, this, "Retrieving list of users");
}
public void registerUser(Context context, User user)
{
PacemakerAPI.createUser(context, this, "Registering new user", user);
}
@Override
public void setResponse(User user)
{
connected = true;
users.put(user.email, user);
activities.put(user.email, new ArrayList<Activity>());
}
And then consequent changes in Welcome:
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
app = (PacemakerApp) getApplication();
app.connectToPacemakerAPI(this);
}
And Signup class:
public void registerPressed (View view)
{
TextView firstName = (TextView) findViewById(R.id.firstName);
TextView lastName = (TextView) findViewById(R.id.lastName);
TextView email = (TextView) findViewById(R.id.Email);
TextView password = (TextView) findViewById(R.id.Password);
User user = new User(firstName.getText().toString(), lastName.getText().toString(), email.getText().toString(), password.getText().toString());
app.registerUser(this, user);
startActivity (new Intent(this, Login.class));
}
Extend PacemakerAPI to support creating and retrieval of activities:
public class PacemakerAPI
{
//...
public static void getActivities(Context context, User user, Response<MyActivity> response, String dialogMesssage)
{
new GetActivities(context, user, response, dialogMesssage).execute();
}
public static void createActivity(Context context, User user, Response<MyActivity> response, String dialogMesssage, MyActivity activity)
{
new CreateActivity(context, user, response, dialogMesssage).execute(activity);
}
}
//...
class GetActivities extends Request
{
private User user;
public GetActivities(Context context, User user, Response<MyActivity> callback, String message)
{
super(context, callback, message);
this.user = user;
}
@Override
protected List<MyActivity> doRequest(Object... params) throws Exception
{
String response = Rest.get("/api/users/" + user.id + "/activities");
List<MyActivity> ActivityList = JsonParser.json2Activities(response);
return ActivityList;
}
}
class CreateActivity extends Request
{
private User user;
public CreateActivity(Context context, User user, Response<MyActivity> callback, String message)
{
super(context, callback, message);
this.user = user;
}
@Override
protected MyActivity doRequest(Object... params) throws Exception
{
String response = Rest.post ("/api/users/" + user.id + "/activities", JsonParser.activity2Json(params[0]));
return JsonParser.json2Activity(response);
}
}
We now encapsulate access to this feature in the application Facade:
public void createActivity (Context context, MyActivity activity, Response<MyActivity> responder)
{
if (loggedInUser != null)
{
PacemakerAPI.createActivity(context, loggedInUser, responder, "Creating activity...", activity);
}
}
public void getActivities(Context context, Response<MyActivity> responder)
{
PacemakerAPI.getActivities(context, loggedInUser, responder, "Retrieving Activities...");
}
The activities list controller can now retrieve/update activities from the service
public class ActivitiesList extends android.app.Activity implements Response <MyActivity>
{
private PacemakerApp app;
private ListView activitiesListView;
private ActivityAdapter activitiesAdapter;
private List<MyActivity> activities = new ArrayList<MyActivity>();
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_activities_list);
app = (PacemakerApp) getApplication();
activitiesListView = (ListView) findViewById(R.id.activitiesListView);
activitiesAdapter = new ActivityAdapter(this, activities);
activitiesListView.setAdapter(activitiesAdapter);
app.getActivities(this, this);
}
@Override
public void setResponse(List<MyActivity> aList)
{
activitiesAdapter.activities = aList;
activitiesAdapter.notifyDataSetChanged();
}
@Override
public void setResponse(MyActivity anObject)
{
}
@Override
public void errorOccurred(Exception e)
{
Toast toast = Toast.makeText(this, "Error Retrieving Activities...", Toast.LENGTH_SHORT);
toast.show();
}
}
//...
Similarly, we can create an activity using the new features on the app class:
public class CreateActivity extends android.app.Activity implements Response <MyActivity>
{
//...
//...
public void createActivityButtonPressed (View view)
{
double distance = distancePicker.getValue();
Activity activity = new Activity (activityType.getText().toString(), activityLocation.getText().toString(), distance);
app.createActivity(this, activity, this);
}
@Override
public void setResponse(List<MyActivity> aList)
{}
@Override
public void setResponse(MyActivity anObject)
{}
@Override
public void errorOccurred(Exception e)
{
Toast toast = Toast.makeText(this, "Failed to create Activity", Toast.LENGTH_SHORT);
toast.show();
}
Test this now and verify that web and android are in sync. You should be able to create activities in either the web app or the android app, and both lists should be visible on each platform.
This is the app at this stage:
Make sure you can follow the flow of information to/from the service.
We could attempt to rationalize the syncing - by introducing a mediator
to encapsulate thte sync behaviour.
First, introduce a new interface to encapsulate more generic sync events:
package org.pacemaker.main;
public interface SyncUpdate
{
void userSyncComplete();
void activitiesSyncComplete();
void syncError (Exception e);
}
Then we introduce a Mediator class to hide access to the API:
package org.pacemaker.main;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.pacemaker.http.Response;
import org.pacemaker.models.MyActivity;
import org.pacemaker.models.MyActivity;
import org.pacemaker.models.User;
import android.content.Context;
import android.widget.Toast;
public class PacemakerMediator
{
Context context;
SyncUpdate syncUpdate;
Map<String, User> users = new HashMap<String, User>();
Map<String, List<MyActivity>> activities = new HashMap<String, List<MyActivity>>();
UserResponder userResponder = new UserResponder(this);
ActivityResponder activityResponder = new ActivityResponder(this);
public User getUser(String email)
{
return users.get(email);
}
public List<MyActivity> getActivities(User user)
{
return activities.get(user.email);
}
public void registerUser(Context context, User user, SyncUpdate syncUpdate)
{
this.syncUpdate = syncUpdate;
PacemakerAPI.createUser(context, userResponder, "Creating new User...", user);
}
public void createActivity(User user, MyActivity activity, SyncUpdate syncUpdate)
{
this.syncUpdate = syncUpdate;
PacemakerAPI.createActivity(context, user, activityResponder, "Creating new Activity...", activity);
}
void syncUsers(Context context, SyncUpdate syncUpdate)
{
this.context = context;
this.syncUpdate = syncUpdate;
PacemakerAPI.getUsers(context, userResponder, "Syncing Users");
}
void syncActivities(User user, SyncUpdate syncUpdate)
{
this.syncUpdate = syncUpdate;
activityResponder.user = user;
PacemakerAPI.getActivities(context, user, activityResponder, "Syncing Activities");
}
void error(Exception e)
{
Toast toast = Toast.makeText(context, "Error in communicating with Pacemaker", Toast.LENGTH_SHORT);
toast.show();
}
}
class UserResponder implements Response<User>
{
PacemakerMediator mediator;
UserResponder (PacemakerMediator pacemakerMediator)
{
this.mediator = pacemakerMediator;
}
@Override
public void setResponse(List<User> users)
{
for (User user : users)
{
mediator.users.put(user.email, user);
}
mediator.syncUpdate.userSyncComplete();
}
@Override
public void setResponse(User user)
{
mediator.users.put(user.email, user);
mediator.activities.put(user.email, new ArrayList<MyActivity>());
mediator.syncUpdate.userSyncComplete();
}
@Override
public void errorOccurred(Exception e)
{
mediator.error(e);
}
}
class ActivityResponder implements Response<MyActivity>
{
PacemakerMediator mediator;
User user;
ActivityResponder (PacemakerMediator mediator)
{
this.mediator = mediator;
}
@Override
public void setResponse(List<MyActivity> activities)
{
mediator.activities.put(user.email, activities);
mediator.syncUpdate.activitiesSyncComplete();
}
@Override
public void setResponse(MyActivity activity)
{
mediator.activities.get(user.email).add(activity);
mediator.syncUpdate.activitiesSyncComplete();
}
@Override
public void errorOccurred(Exception e)
{
mediator.error(e);
}
}
With the mediator in place, refactor PacemakerApp to use the mediator:
package org.pacemaker.main;
import java.util.List;
import org.pacemaker.models.MyActivity;
import org.pacemaker.models.User;
import android.app.Application;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
public class PacemakerApp extends Application implements SyncUpdate
{
private User loggedInUser;
private PacemakerMediator mediator = new PacemakerMediator();
public void syncUsers(Context context)
{
mediator.syncUsers(context, this);
}
public void registerUser(User user, Context context)
{
mediator.registerUser(context, user, this);
}
public List<MyActivity> getActivities()
{
return mediator.getActivities(loggedInUser);
}
public boolean loginUser(String email, String password)
{
loggedInUser = mediator.getUser(email);
if (loggedInUser != null && !loggedInUser.password.equals(password))
{
loggedInUser = null;
}
mediator.syncActivities(loggedInUser, this);
return loggedInUser != null;
}
public void logout()
{
loggedInUser = null;
}
public void createActivity (Context context, MyActivity activity, SyncUpdate syncUpdate)
{
if (loggedInUser != null)
{
mediator.createActivity(loggedInUser, activity, syncUpdate);
}
}
@Override
public void userSyncComplete()
{
Toast toast = Toast.makeText(this, "Pacemaker Sync Successful", Toast.LENGTH_SHORT);
toast.show();
}
@Override
public void activitiesSyncComplete()
{
Toast toast = Toast.makeText(this, "Pacemaker Sync Successful", Toast.LENGTH_SHORT);
toast.show();
}
@Override
public void syncError(Exception e)
{
Toast toast = Toast.makeText(this, "Failed to connect to Pacemaker Service", Toast.LENGTH_SHORT);
toast.show();
}
@Override
public void onCreate()
{
super.onCreate();
Log.v("Pacemaker", "Pacemaker App Started");
}
}
The controllers can now be relieved for responsibility for accessing the API in a fine grained manner, and use the Mediator we have just introduced:
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
app = (PacemakerApp) getApplication();
app.syncUsers(this);
}
public void registerPressed (View view)
{
TextView firstName = (TextView) findViewById(R.id.firstName);
TextView lastName = (TextView) findViewById(R.id.lastName);
TextView email = (TextView) findViewById(R.id.Email);
TextView password = (TextView) findViewById(R.id.Password);
User user = new User (firstName.getText().toString(), lastName.getText().toString(), email.getText().toString(), password.getText().toString());
app.registerUser(user, this);
startActivity (new Intent(this, Login.class));
}
package org.pacemaker.controllers;
import org.pacemaker.R;
import org.pacemaker.main.PacemakerApp;
import org.pacemaker.main.SyncUpdate;
import org.pacemaker.models.MyActivity;
import org.pacemaker.models.MyActivity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.NumberPicker;
import android.widget.TextView;
import android.widget.Toast;
public class CreateActivity extends android.app.Activity implements SyncUpdate
{
private PacemakerApp app;
private Button createActivityButton;
private TextView activityType;
private TextView activityLocation;
private NumberPicker distancePicker;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create);
app = (PacemakerApp) getApplication();
createActivityButton = (Button) findViewById(R.id.createActivityButton);
activityType = (TextView) findViewById(R.id.activityType);
activityLocation = (TextView) findViewById(R.id.activityLocation);
distancePicker = (NumberPicker) findViewById(R.id.numberPicker);
distancePicker.setMinValue(0);
distancePicker.setMaxValue(20);
}
public void createActivityButtonPressed (View view)
{
double distance = distancePicker.getValue();
MyActivity activity = new MyActivity (activityType.getText().toString(), activityLocation.getText().toString(), distance);
app.createActivity(this, activity, this);
}
@Override
public void userSyncComplete()
{ }
@Override
public void activitiesSyncComplete()
{
Toast toast = Toast.makeText(this, "Activity Created", Toast.LENGTH_SHORT);
toast.show();
}
@Override
public void syncError(Exception e)
{
Toast toast = Toast.makeText(this, "Failed to create Activity", Toast.LENGTH_SHORT);
toast.show();
}
public void listActivityButtonPressed (View view)
{
Log.v("Pacemaker", "List Activities Button Pressed");
startActivity (new Intent(this, ActivitiesList.class));
}
}
package org.pacemaker.controllers;
import java.util.ArrayList;
import java.util.List;
import org.pacemaker.R;
import org.pacemaker.main.PacemakerApp;
import org.pacemaker.main.SyncUpdate;
import org.pacemaker.models.MyActivity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
public class ActivitiesList extends android.app.Activity implements SyncUpdate
{
private PacemakerApp app;
private ListView activitiesListView;
private ActivityAdapter activitiesAdapter;
private List<MyActivity> activities = new ArrayList<MyActivity>();
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_activities_list);
app = (PacemakerApp) getApplication();
activitiesListView = (ListView) findViewById(R.id.activitiesListView);
activitiesAdapter = new ActivityAdapter(this, activities);
activitiesListView.setAdapter(activitiesAdapter);
activitiesAdapter.activities = app.getActivities();
activitiesAdapter.notifyDataSetChanged();
}
@Override
public void userSyncComplete()
{ }
@Override
public void activitiesSyncComplete()
{
activitiesAdapter.activities = app.getActivities();
activitiesAdapter.notifyDataSetChanged();
}
@Override
public void syncError(Exception e)
{
Toast toast = Toast.makeText(this, "Error Retrieving Activities...", Toast.LENGTH_SHORT);
toast.show();
}
}
class ActivityAdapter extends ArrayAdapter<MyActivity>
{
private Context context;
public List<MyActivity> activities;
public ActivityAdapter(Context context, List<MyActivity> activities)
{
super(context, R.layout.activity_row_layout, activities);
this.context = context;
this.activities = activities;
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.activity_row_layout, parent, false);
MyActivity activity = activities.get(position);
TextView type = (TextView) view.findViewById(R.id.type);
TextView location = (TextView) view.findViewById(R.id.location);
TextView distance = (TextView) view.findViewById(R.id.distance);
type.setText(activity.kind);
location.setText(activity.location);
distance.setText("" + activity.distance);
return view;
}
@Override
public int getCount()
{
return activities.size();
}
}
Try the andorid app again now - it should work as before. However, the interaction with the Facade now shields the Activities from the mechanics of the REST access.
Archive of the app so far:
This lab has introduce the Mediator pattern in an attempt to introduce a generic 'sync' feature. There are a range of features in the Android SDK which could be exploited by the Mediator implementation - or which could replace the mediator completely, implementing a more robust sync facility. These are concepts that can usefully implemented in your assignment. Among these are:
A Service is an application component that can perform long-running operations in the background and does not provide a user interface. Another application component can start a service and it will continue to run in the background even if the user switches to another application
Currently, for every command a new thread is being created vie the AsyncTask
execute method:
new GetActivities(context, user, response, dialogMesssage).execute();
(Note the '.execute' above). Unless we specifically initiate a command like this from the android client, we will not revieve any updates. We could initiate a service on application launch, which would periodically reach out to the service and download any updates. In this way, if a user updated activities using the web app, then should appear on the android client in due course.
If you implement the services above, then your activities will need to be updated periodically if a change is noticed. BroadcastRecievers are one way of achieving this. It may even be possible for multiple applications to 'register' for activity updates using this mechanism
The sync adapter component in your app encapsulates the code for the tasks that transfer data between the device and a server. Based on the scheduling and triggers you provide in your app, the sync adapter framework runs the code in the sync adapter component.
Supplementing the above, the SyncAdapter framework introduces a more comprehensive solution to sync issues: