Objectives

Complete the android application to include activity sync support

Setup

This is v4 of the pacemaker-android project from the last android 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, initiating contact with the service form the welcome activity:

Welcome

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

This approach requires these 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>());
  }

In addition, the Sigup controller is also using the application object:

Signup

    app.registerUser(this, user);

Creating Activities

Extend PacemakerAPI to support activities:

PacemakerAPI

public class PacemakerAPI
{
  //...
  public static void getActivities(Context context, User user, Response<Activity> response, String dialogMesssage)
  {
    new GetActivities(context, user, response, dialogMesssage).execute();
  }

  public static void createActivity(Context context, User user, Response<Activity> response, String dialogMesssage, Activity donation)
  {
    new CreateActivity(context, user, response, dialogMesssage).execute(donation);
  }
}

//...

class GetActivities extends Request
{
  private User user;

  public GetActivities(Context context, User user, Response<Activity> callback, String message)
  {
    super(context, callback, message);
    this.user = user;
  }

  @Override
  protected List<Activity> doRequest(Object... params) throws Exception
  {
    String response =  Rest.get("/api/users/" + user.id + "/activities");
    List<Activity> ActivityList = JsonParser.json2Activities(response);
    return ActivityList;
  }
}

class CreateActivity extends Request
{
  private User user;

  public CreateActivity(Context context, User user, Response<Activity> callback, String message)
  {
    super(context, callback, message);
    this.user = user;
  }

  @Override
  protected Activity 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 class:

PacemakerApp

  public void createActivity (Context context, Activity activity, Response<Activity> responder)
  {
    if (loggedInUser != null)
    {
      PacemakerAPI.createActivity(context, loggedInUser, responder, "Creating activity...", activity);
    }
  }

  public void getActivities(Context context, Response<Activity> 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 <Activity>
{
  private PacemakerApp     app;
  private ListView         activitiesListView;
  private ActivityAdapter  activitiesAdapter;
  private List<Activity>   activities = new ArrayList<Activity>();

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.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<Activity> aList)
  {
    activitiesAdapter.activities = aList;
    activitiesAdapter.notifyDataSetChanged();
  }

  @Override
  public void setResponse(Activity anObject)
  {
  }

  @Override
  public void errorOccurred(Exception e)
  {
    Toast toast = Toast.makeText(this, "Error Retrieving Activities...", Toast.LENGTH_SHORT);
    toast.show();
  }
  //...
}

Similiarly, we can create an activity using the new features on the app class:

CreateActivity

public class CreateActivity extends android.app.Activity implements Response <Activity>
{
  //...
  //...

  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<Activity> aList)
  {}

  @Override
  public void setResponse(Activity anObject)
  {}

  @Override
  public void errorOccurred(Exception e)
  {
    Toast toast = Toast.makeText(this, "Failed to create Activity", Toast.LENGTH_SHORT);
    toast.show();
  }

This will fail due to mismatch in names used n Activity model. Rename type to kind :

public class Activity
{
  public Long id;
  public String kind;
  public String location;
  public double distance;
  //...
}

Type is a valid Scala reserved word, and is disturbing the scala templates compilation process.

Test this now and verify that web and android are in sync. Make sure you can follow the flow of information to/from the service.

Mediator

We could attempt to rationalise 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.Activity;
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<Activity>> activities        = new HashMap<String, List<Activity>>();
  UserResponder               userResponder     = new UserResponder(this);
  ActivityResponder           activityResponder = new ActivityResponder(this);  

  public User getUser(String email)
  {
    return users.get(email);
  }

  public List<Activity> 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, Activity 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<Activity>());
    mediator.syncUpdate.userSyncComplete();
  }

  @Override
  public void errorOccurred(Exception e)
  {
    mediator.error(e);
  }
}

class ActivityResponder implements Response<Activity>
{
  PacemakerMediator mediator;
  User              user;

  ActivityResponder (PacemakerMediator mediator)
  {
    this.mediator = mediator;
  }

  @Override
  public void setResponse(List<Activity> activities)
  {
    mediator.activities.put(user.email, activities);
    mediator.syncUpdate.activitiesSyncComplete();
  }

  @Override
  public void setResponse(Activity 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.Activity;
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<Activity> 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, Activity 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.Activity;

import android.os.Bundle;
import android.content.Intent;
import android.view.Menu;
import android.view.MenuItem;
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.distancePicker);

    distancePicker.setMinValue(0);
    distancePicker.setMaxValue(20);
  }

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

  @Override
  public boolean onCreateOptionsMenu(Menu menu)
  {
    getMenuInflater().inflate(R.menu.activities_create, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item)
  {
    switch (item.getItemId())
    {
      case R.id.action_list_actvities : startActivity (new Intent(this, ActivitiesList.class));
                                        break;
      case R.id.action_logout         : startActivity (new Intent(this, Welcome.class));
                                        break;
    }
    return true;
  }
}

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.Activity;

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<Activity>   activities = new ArrayList<Activity>();

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.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();
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu)
  {
    getMenuInflater().inflate(R.menu.activities_list, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item)
  {
    switch (item.getItemId())
    {
      case R.id.action_create_actvities : startActivity (new Intent(this, CreateActivity.class));
                                          break;
      case R.id.action_logout           : startActivity (new Intent(this, Welcome.class));
                                          break;
    }
    return true;
  }
}

// Adapter unchanged

Exercises

Archive of the app so far:

This lab has introduce the Mediator pattern in an attempt to intorduce a generic 'sync' feature. There are a range of features in the Andorid SDK which could be exploited by the Mediator implementation - or which could replace the mediator completely, implementing a more reobust sync facility. These are concepts that can usefully implemented in your assiignment. 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 andrid client, we will not revieve any updates. We could initiatie 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.

Supplimenting the above, the SyncAdapter framework introduces a more comprehensive solution to sync issues: