Objectives

Complete the android application to include activity sync support using the Half Sync/Half Async Pattern. Overlay this with a Mediator implementation.

Setup

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.

Registering new Users

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:

Welcome

  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_welcome);
    app = (PacemakerApp) getApplication();
    app.connectToPacemakerAPI(this);
  }

And Signup class:

Signup

  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));
  }

Creating Activities

Extend PacemakerAPI to support creating and retrieval of activities:

PacemakerAPI

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:

PacemakerApp

  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...");
  }

Controllers

The activities list controller can now retrieve/update activities from the service

ActivitiesList


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:

CreateActivity

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.

Mediator

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);
  }
}

PacemakerApp

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");
  }
}

Controllers

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:

Welcome

  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_welcome);
    app = (PacemakerApp) getApplication();
    app.syncUsers(this);
  }

Signup

  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));
  }

CreateActivity

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));
  }
}

ActivitiesList

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.

Exercises

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:

Services

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.

BroadcastRecievers

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

SyncAdapter

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: