Complete the android application to include activity sync support
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.
We need re-orient how we connect to the service, initiating contact with the service form the welcome activity:
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:
app.registerUser(this, user);
Extend PacemakerAPI to support activities:
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:
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...");
}
The activities list controller can now retrieve/update activities from the service
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:
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.
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);
}
}
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");
}
}
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.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;
}
}
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
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:
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.
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.
Supplimenting the above, the SyncAdapter framework introduces a more comprehensive solution to sync issues: