Last weekend I wanted to start playing with REST on android. I set up a really simple Google App Engine server that basically runs nothing more than a Hello World server - you send it a shared User object with nothing but an emailAddress and it will give it back to you.
To recreate this you're going to need to download the following libraries: (the downloads are at the bottom - the jersey-json was a nightmare to track down the first time)
gson (i used 2.2.2):
jersey-client (i used 1.12):
jersey-core (i used 1.12):
jersey-json (i used 1.12):
The first thing I did was create a simple network handler.
There's 2 points to this that took me a minute to find:
- Android doesn't allow you to run Network Code from the main thread. In order to return anything it would be necessary to have some type of Callback method.
- Certain versions of Jersey and Android don't play well together. See this post: http://stackoverflow.com/questions/9342506/jersey-client-on-android-nullpointerexception. Basically you will have to provide your own Implementation of Jersey's ServiceIteratorProvider class - Android apparently cannot load the one in the META-INF folder of the Jersey Jar. The code to get around this is copied directly out of this post and works perfectly - it is attached below.
So - the very first thing I decided to do before creating my NetworkHandler class was to create a simple Callback interface as shown below.
package com.exposure101.example.shared;
public interface Callback<T> {
void callback(T t);
}
Next I made 2 classes that extend Android's AsyncTask class to do the actual GET and POST calls. I made a simple enum to wrap the MIME types for JSON. The source code is given below
Here's the MIMETypes enum
package com.exposure101.example.shared;
public enum MIMETypes {
APPLICATION_JSON("application/json");
private final String name;
private MIMETypes(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Here's the Get Task
package com.exposure101.example.json;
import android.os.AsyncTask;
import com.exposure101.example.shared.Callback;
import com.exposure101.example.shared.MIMETypes;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
public class GetTask extends AsyncTask<String, String, String> {
private final String url;
private final Callback<String> callback;
GetTask(String url, Callback<String> callback) {
this.url = url;
this.callback = callback;
}
@Override
protected String doInBackground(String... params) {
final Client client = Client.create();
final WebResource resource = client.resource(url);
final ClientResponse response = resource.accept(MIMETypes.APPLICATION_JSON.getName())
.get(ClientResponse.class);
return response.getEntity(String.class);
}
@Override
protected void onPostExecute(String result) {
callback.callback(result);
super.onPostExecute(result);
}
}
And here's the Post Task
package com.exposure101.example.json;
import android.os.AsyncTask;
import com.exposure101.example.shared.Callback;
import com.exposure101.example.shared.MIMETypes;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
public class PostTask extends AsyncTask<String, String, String> {
private final String url;
private final String requestBody;
private final Callback<String> callback;
PostTask(String url, String requestBody, Callback<String> callback) {
this.url = url;
this.requestBody = requestBody;
this.callback = callback;
}
@Override
protected String doInBackground(String... params) {
final Client client = Client.create();
final WebResource resource = client.resource(url);
final ClientResponse response = resource.type(MIMETypes.APPLICATION_JSON.getName())
.post(ClientResponse.class, requestBody);
if (response.getStatus() != 201 && response.getStatus() != 200) {
throw new RuntimeException("failed: http error code = " + response.getStatus());
}
final String responseEntity = response.getEntity(String.class).replaceAll("\\\\", "");
return responseEntity.substring(1, responseEntity.length() - 1);
}
@Override
protected void onPostExecute(String result) {
callback.callback(result);
super.onPostExecute(result);
}
}
With this I kind of cheated the system using the POST request - honestly I have no idea if you should do it like this. On the server side, as soon as the POST is received I query the datastore to see if a User with the given email address exists. If it's not already in the datastore I create one, either way I return it back across the wire as JSON with the google datastore id in it. There's 2 things that I found happen.
- The entire response is broken up by \ marks. An example would be {\"id"\:\1001\,\"emailAddress"\: ... \}
- The entire JSON string is wrapped in quotation marks - as a quick hack to get around this I simply took a substring.
Now that I have some simple tasks up and ready I made a really simple NetworkHandler interface to read and write requests.
package com.exposure101.example.json;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.exposure101.example.jersey.AndroidServiceIteratorProvider;
import com.exposure101.example.shared.Callback;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.sun.jersey.spi.service.ServiceFinder;
public class NetworkHandler {
private static NetworkHandler instance;
public static synchronized NetworkHandler getInstance() {
if (instance == null) {
instance = new NetworkHandler();
}
return instance;
}
@SuppressWarnings("rawtypes")
private NetworkHandler() {
ServiceFinder.setIteratorProvider(new AndroidServiceIteratorProvider());
}
public <T> void read(final String url, final Class<T> clazz, final Callback<T> callback) {
new GetTask(url, new Callback<String>() {
@Override
public void callback(String result) {
callback.callback(new GsonBuilder().create().fromJson(result, clazz));
}
}).execute();
}
public <T> void readList(final String url, final Class<T[]> clazz, final Callback<List<T>> callback) {
new GetTask(url, new Callback<String>() {
@Override
public void callback(String result) {
final T[] array = new GsonBuilder().create().fromJson(result, clazz);
callback.callback(new ArrayList<T>(Arrays.asList(array)));
}
}).execute();
}
public <T> void write(final String url, final Class<T> clazz, final T t, final Callback<T> callback) {
final Gson gson = new GsonBuilder().create();
new PostTask(url, gson.toJson(t), new Callback<String>() {
@Override
public void callback(String result) {
callback.callback(gson.fromJson(result, clazz));
}
}).execute();
}
}
That's pretty much it for the Network code.
All that's left is to create an activity to show the user accounts and send it to the server.
Basically there's only 2 things I want to happen here:
- Have all the accounts on the phone come up in a ListView on the top part of the screen. (Note: the accounts on your phone aren't unique - there may be multiple accounts with the same email - we should only show unique email addresses)
- Have a button on the bottom to send the selected email address to the server. If we receive a response we can then launch a new Intent to switch the Activity. (Note: while the server is processing we should indicate that to the user with a loading icon)
The xml layout for this is as follows:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/select_user_account_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="@string/login" />
<ListView
android:id="@+id/user_accounts_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/select_user_account_button"
android:layout_alignParentTop="true" />
</RelativeLayout>
and the actual Android Activity code is as follows:
package com.exposure101.example.activities;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Patterns;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import com.exposure101.example.R;
import com.exposure101.example.json.NetworkHandler;
import com.exposure101.example.json.UserServiceURLs;
import com.exposure101.example.shared.Callback;
import com.exposure101.example.shared.entity.User;
public class UserAccountSelectActivity extends Activity {
private ListView userAccountsListView;
private Button selectUserAccountButton;
private String selectedEmailAddress;
/**
*
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.activity_user_account_select);
userAccountsListView = (ListView) findViewById(R.id.user_accounts_list_view);
selectUserAccountButton = (Button) findViewById(R.id.select_user_account_button);
final Account[] accounts = initializeAccounts();
final String[] emailAddresses = initializeEmailAddresses(accounts);
initializeView(emailAddresses);
}
/**
* get the accounts from android's account manager
*
* @return
*/
private Account[] initializeAccounts() {
final Pattern emailPattern = Patterns.EMAIL_ADDRESS;
final List<Account> accounts = new ArrayList<Account>(Arrays.asList(AccountManager.get(this)
.getAccounts()));
for (final Iterator<Account> it = accounts.iterator(); it.hasNext();) {
final Account account = it.next();
if (emailPattern.matcher(account.name).matches() == false) {
it.remove();
}
}
return accounts.toArray(new Account[accounts.size()]);
}
/**
* get the email addresses from the accounts
*
* @param accounts
* @return
*/
private String[] initializeEmailAddresses(Account[] accounts) {
final List<String> emailAddresses = new ArrayList<String>(accounts.length);
for (final Account account : accounts) {
emailAddresses.add(account.name);
}
final Set<String> uniqueEmailAddresses = new HashSet<String>(emailAddresses);
return uniqueEmailAddresses.toArray(new String[uniqueEmailAddresses.size()]);
}
/**
*
* @param accounts
*/
private void initializeView(String[] emailAddresses) {
selectUserAccountButton.setEnabled(false);
userAccountsListView.setAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_single_choice, emailAddresses));
userAccountsListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
userAccountsListView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapter, View view, int position, long arg3) {
selectedEmailAddress = (String) userAccountsListView.getItemAtPosition(position);
selectUserAccountButton.setEnabled(true);
}
});
selectUserAccountButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
login();
}
});
}
/**
*
* @param emailAddress
*/
private void login() {
final NetworkHandler networkHandler = NetworkHandler.getInstance();
final String url = UserServiceURLs.POST.getURL();
networkHandler.write(url, User.class, new User(selectedEmailAddress), new Callback<User>() {
@Override
public void callback(User user) {
handleLogin();
}
});
}
/**
* your call was successful
*/
private void handleLogin() {
setProgressBarIndeterminateVisibility(false);
selectUserAccountButton.setEnabled(true);
/*
* startActivity(new Intent(UserAccountSelectActivity.this, TheNextExampleActivity.class));
*/
}
}
And that's about it.
Here's all the required libraries: (The jersey-json one was a little hard to track down)
gson
jersey-client-1.2
jersey-core-1.2
json-jar-1.2
And here's the AndroidServiceIteratorProvider class.
package com.exposure101.example.jersey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import android.util.Log;
import com.sun.jersey.spi.service.ServiceFinder.ServiceIteratorProvider;
public class AndroidServiceIteratorProvider<T> extends ServiceIteratorProvider<T> {
private static final String TAG = AndroidServiceIteratorProvider.class.getSimpleName();
private static final String MESSAGE = "Unable to load provider";
private static final HashMap<String, String[]> SERVICES = new HashMap<String, String[]>();
private static final String[] com_sun_jersey_spi_HeaderDelegateProvider = {
"com.sun.jersey.core.impl.provider.header.MediaTypeProvider",
"com.sun.jersey.core.impl.provider.header.StringProvider" };
private static final String[] com_sun_jersey_spi_inject_InjectableProvider = {};
private static final String[] javax_ws_rs_ext_MessageBodyReader = {
"com.sun.jersey.core.impl.provider.entity.StringProvider",
"com.sun.jersey.core.impl.provider.entity.ReaderProvider" };
private static final String[] javax_ws_rs_ext_MessageBodyWriter = {
"com.sun.jersey.core.impl.provider.entity.StringProvider",
"com.sun.jersey.core.impl.provider.entity.ReaderProvider" };
static {
SERVICES.put("com.sun.jersey.spi.HeaderDelegateProvider",
com_sun_jersey_spi_HeaderDelegateProvider);
SERVICES.put("com.sun.jersey.spi.inject.InjectableProvider",
com_sun_jersey_spi_inject_InjectableProvider);
SERVICES.put("javax.ws.rs.ext.MessageBodyReader", javax_ws_rs_ext_MessageBodyReader);
SERVICES.put("javax.ws.rs.ext.MessageBodyWriter", javax_ws_rs_ext_MessageBodyWriter);
SERVICES.put("jersey-client-components", new String[] {});
SERVICES.put("com.sun.jersey.client.proxy.ViewProxyProvider", new String[] {});
}
@SuppressWarnings("unchecked")
@Override
public Iterator<Class<T>> createClassIterator(Class<T> service, String serviceName,
ClassLoader loader, boolean ignoreOnClassNotFound) {
String[] classesNames = SERVICES.get(serviceName);
int length = classesNames.length;
ArrayList<Class<T>> classes = new ArrayList<Class<T>>(length);
for (int i = 0; i < length; i++) {
try {
classes.add((Class<T>) Class.forName(classesNames[i]));
} catch (ClassNotFoundException e) {
Log.v(TAG, MESSAGE, e);
}
}
return classes.iterator();
}
@Override
public Iterator<T> createIterator(Class<T> service, String serviceName, ClassLoader loader,
boolean ignoreOnClassNotFound) {
String[] classesNames = SERVICES.get(serviceName);
int length = classesNames.length;
ArrayList<T> classes = new ArrayList<T>(length);
for (int i = 0; i < length; i++) {
try {
classes.add(service.cast(Class.forName(classesNames[i]).newInstance()));
} catch (IllegalAccessException e) {
Log.v(TAG, MESSAGE, e);
} catch (InstantiationException e) {
Log.v(TAG, MESSAGE, e);
} catch (ClassNotFoundException e) {
Log.v(TAG, MESSAGE, e);
}
}
return classes.iterator();
}
}
Hope this helps.