Introduction
After watching the great talk by Virgil Dobjanschi from Google IO 2010 on Restful Android applications I searched the net for implementations of his patterns ending up with very little results.
This is my attempt at implementing Pattern A from his presentation.
If you use the code I would be interested in how you are using it, i.e. are you using it for a personal project or a commercial one, is the app going on the market? I would be keen to see any apps which use the code.
I welcome any comments including any suggested improvements.
Background
I recommend that if you have not already watched Dobjanschi's presentation, that you do so, it is found here:
http://www.youtube.com/watch?v=xHXn3Kg2IQE
The main reason I chose Pattern A from the talk is that you can give the REST methods whatever interface you want, you are not restricted to the ContentProvider API alone.
Implementation
I will explain the code starting from the database and the REST calls and then move up to the UI.
Rest
I have an abstract class which exposes Post, Put, Get, Delete methods to the sub classes.
I have a number of sub classes which will call these methods on the base and parse their results to data objects.
For each Rest method I wish to use I have a method something like this:
public RowDTO[] getRows()
{
}
I perform HTTP calls synchronously at this level as this is running on it's own thread, as we will see when we get to the ProcessorService
Processor
I have basically implemented a processor for each table in the database.
The processor's job is to make a call to Rest and to update the SQL as unnecessary.
I pass a reference to the Context from the service so that the Processor can access the database using:
Context.getContentResolver()
I will not provide any code here as I think the implementation will change substantially for each application and, in his video, Dobjanschi gives a good description of how to implement this.
ServiceProvider
I have an IServiceProvider
interface which provides a common interface to the processor from the
ProcessorService
.
The main purpose for this class is to translate an integer constant to a specific method on the processor and to parse the arguments from a Bundle
to typed arguments for the processor method.
import android.os.Bundle;
public interface IServiceProvider
{
boolean RunTask(int methodId, Bundle extras);
}
An example implementation of this interface is:
import android.content.Context;
import android.os.Bundle;
public class RowsServiceProvider implements IServiceProvider
{
private final Context mContext;
public RowsServiceProvider(Context context)
{
mContext = context;
}
public static class Methods
{
public static final int REFRESH_ROWS_METHOD = 1;
public static final int DELETE_ROW_METHOD = 2;
public static final String DELETE_ROW_PARAMETER_ID = "id";
}
@Override
public boolean RunTask(int methodId, Bundle extras)
{
switch(methodId)
{
case Methods.REFRESH_ROWS_METHOD:
return refreshRows();
case Methods.DELETE_ROW_METHOD:
return deleteRow(extras);
}
return false;
}
private boolean refreshRows()
{
return new RowsProcessor(mContext).resfreshRows();
}
private boolean deleteRow(Bundle extras)
{
int id = extras.getInt(Methods.DELETE_ROW_PARAMETER_ID);
return new RowsProcessor(mContext).deleteRow(id);
}
}
ProcessorService
This seems to me to be the most complex part of the pattern.
This service takes care of running each call to a Processor on its own thread.
It also ensures that if a method is currently running and it is called again with the same parameters, then instead of running the method multiple times in parallel, a single call will just notify both callers when it is complete.
To start a method call an Intent should be sent to the onStart
method of this service, this will start the service if the service is not already running.
The intent will contain the following details:
- Which processor does the intended method exist on.
- The method to call.
- The parameters for the method.
- A tag to be used for the result Intent.
The result tag is used by the caller to identify a result intent to send when the method call completes. In a case where two calls are made to the same method, the method is only called once, however each caller may specify it's own result tag so that it can individually be notified of completion and whether the call completed successfully.
The result intents contains all the extras passed to start the service (including the processor called, method called, any parameters passed) and also a boolean result indicating if the call was successful.
When all methods complete this service will shut itself down.
import java.util.ArrayList;
import java.util.HashMap;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
public class ProcessorService extends Service
{
private Integer lastStartId;
private final Context mContext = this;
public static class Extras
{
public static final String PROVIDER_EXTRA = "PROVIDER_EXTRA";
public static final String METHOD_EXTRA = "METHOD_EXTRA";
public static final String RESULT_ACTION_EXTRA = "RESULT_ACTION_EXTRA";
public static final String RESULT_EXTRA = "RESULT_EXTRA";
}
private final HashMap<String, AsyncServiceTask> mTasks = new HashMap<String, AsyncServiceTask>();
public static class Providers
{
public static final int ROWS_PROVIDER = 1;
}
private IServiceProvider GetProvider(int providerId)
{
switch(providerId)
{
case Providers.ROWS_PROVIDER:
return new RowsServiceProvider(this);
}
return null;
}
private String getTaskIdentifier(Bundle extras)
{
String[] keys = extras.keySet().toArray(new String[0]);
java.util.Arrays.sort(keys);
StringBuilder identifier = new StringBuilder();
for (int keyIndex = 0; keyIndex < keys.length; keyIndex++)
{
String key = keys[keyIndex];
if (key.equals(Extras.RESULT_ACTION_EXTRA))
{
continue;
}
identifier.append("{");
identifier.append(key);
identifier.append(":");
identifier.append(extras.get(key).toString());
identifier.append("}");
}
return identifier.toString();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
synchronized (mTasks)
{
lastStartId = startId;
Bundle extras = intent.getExtras();
String taskIdentifier = getTaskIdentifier(extras);
Log.i("ProcessorService", "starting " + taskIdentifier);
AsyncServiceTask task = mTasks.get(taskIdentifier);
if (task == null)
{
task = new AsyncServiceTask(taskIdentifier, extras);
mTasks.put(taskIdentifier, task);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
}
String resultAction = extras.getString(Extras.RESULT_ACTION_EXTRA);
if (resultAction != "")
{
task.addResultAction(extras.getString(Extras.RESULT_ACTION_EXTRA));
}
}
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent)
{
return null;
}
public class AsyncServiceTask extends AsyncTask<Void, Void, Boolean>
{
private final Bundle mExtras;
private final ArrayList<String> mResultActions = new ArrayList<String>();
private final String mTaskIdentifier;
public AsyncServiceTask(String taskIdentifier, Bundle extras)
{
mTaskIdentifier = taskIdentifier;
mExtras = extras;
}
public void addResultAction(String resultAction)
{
if (!mResultActions.contains(resultAction))
{
mResultActions.add(resultAction);
}
}
@Override
protected Boolean doInBackground(Void... params)
{
Log.i("ProcessorService", "working " + mTaskIdentifier);
Boolean result = false;
final int providerId = mExtras.getInt(Extras.PROVIDER_EXTRA);
final int methodId = mExtras.getInt(Extras.METHOD_EXTRA);
if (providerId != 0 && methodId != 0)
{
final IServiceProvider provider = GetProvider(providerId);
if (provider != null)
{
try
{
result = provider.RunTask(methodId, mExtras);
} catch (Exception e)
{
result = false;
}
}
}
return result;
}
@Override
protected void onPostExecute(Boolean result)
{
synchronized (mTasks)
{
Log.i("ProcessorService", "finishing " + mTaskIdentifier);
for (int i = 0; i < mResultActions.size(); i++)
{
Intent resultIntent = new Intent(mResultActions.get(i));
resultIntent.putExtra(Extras.RESULT_EXTRA, result.booleanValue());
resultIntent.putExtras(mExtras);
resultIntent.setPackage(mContext.getPackageName());
mContext.sendBroadcast(resultIntent);
}
mTasks.remove(mTaskIdentifier);
if (mTasks.size() < 1)
{
stopSelf(lastStartId);
}
}
}
}
}
ServiceHelper
The service helper is simply provides a nice interface for upper layers as well as 'helping' with creating intents and starting the ProcessService.
The abstract class looks like this:
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
public abstract class ServiceHelperBase
{
private final Context mcontext;
private final int mProviderId;
private final String mResultAction;
public ServiceHelperBase(Context context, int providerId, String resultAction)
{
mcontext = context;
mProviderId = providerId;
mResultAction = resultAction;
}
protected void RunMethod(int methodId)
{
RunMethod(methodId, null);
}
protected void RunMethod(int methodId, Bundle bundle)
{
Intent service = new Intent(mcontext, ProcessorService.class);
service.putExtra(ProcessorService.Extras.PROVIDER_EXTRA, mProviderId);
service.putExtra(ProcessorService.Extras.METHOD_EXTRA, methodId);
service.putExtra(ProcessorService.Extras.RESULT_ACTION_EXTRA, mResultAction);
if (bundle != null)
{
service.putExtras(bundle);
}
mcontext.startService(service);
}
}
An example sub class:
import android.content.Context;
public class RowsServiceHelper extends ServiceHelperBase
{
public RowsServiceHelper(Context context, String resultAction)
{
super(context, ProcessorService.Providers.ROWS_PROVIDER, resultAction);
}
public void refreshRows()
{
RunMethod(RowsServiceProvider.Methods.REFRESH_ROWS_METHOD);
}
public void deleteRow(int id)
{
Bundle extras = new Bundle();
extras.putInt(RowsServiceProviderMethods.DELETE_ROW_PARAMETER_ID, id);
RunMethod(RowsServiceProvider.Methods.DELETE_ROW_METHOD, extras);
}
}
Using The RowsProcessor
Now for the upper layer, usually this will be in an activity.
To receive result intents use the following code:
Create an Intent filter in you code for the return intents:
private final static String RETURN_ACTION = "com.MyApp.RowsActivity.ActionResult";
private final IntentFilter mFilter = new IntentFilter(RETURN_ACTION);
Create a Broadcast receiver to handle return intents:
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
Bundle extras = intent.getExtras();
boolean success = extras.getBoolean(ProcessorService.Extras.RESULT_EXTRA);
int method = extras.getInt(ProcessorService.Extras.METHOD_EXTRA);
String text;
if (success)
{
text = "Method " + method + " passed!";
}
else
{
text = "Method " + method + " failed!";
}
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, text, duration);
toast.show();
}
};
In your activities onStart method:
registerReceiver(mBroadcastReceiver, mFilter);
mServiceHelper = new RowsServiceHelper(mActivity, RETURN_ACTION);
In onStop:
unregisterReceiver(mBroadcastReceiver);
Now you can simply call any method on mServiceHelper
in your activity to make REST calls on their own thread and update the database, and you will be notified via mBroadcastReceiver
.
History
28 July 2012 - Initial post