oAuth2 with Android (Part 3)

This is the fifth post in the series over oAuth with Symfony2, iOS and Android. All posts in this serie can be found here. With this post we are finishing up the android app as much as possible with the ability to add new items (Via the API) and adding items we got from the API into the list. All code from this post can be found here at Github. Because wordpress doesn’t seem to work that well with the included code in this blog, I suggest to use the code from the repository. I created a tag in the repo for the part that was at the end of this third part. Lets start with adding a new Demo. This can be done by sending a POST request to api.host/demos. Before we start with creating the demos, we need to fix a issue in our requests. Volley standard adds a Content-Type with post. We don’t want this Content-Type because we send JSON, not a form. We also added by default a Content-Type header, but that causes issues if data isn’t JSON, or if we send POST, because Volley automatically adds the Content-Type as well. AbstractRequest.java:

package sohier.me.saiod.android;

import android.util.Log;

import com.android.volley.AuthFailureError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.google.api.client.auth.oauth2.Credential;
import com.google.gson.Gson;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

public abstract class AbstractRequest extends Request {

    private final Map headers;
    private final Response.Listener listener;

    protected Object bodyObject = null;

    /**
     * Create the URL for calling the API.
     * @param HOST Hostname
     * @param cred Credentials for the oAuth api.
     * @param url The path for the API.
     * @return full URL.
     */
    private static String makeUrl(String HOST, Credential cred, String url) {
        url = HOST + url;
        url += "?access_token=";
        url += cred.getAccessToken();

        Log.d("AbstractRequest", "URL: " + url);

        return url;
    }

    /**
     *
     * @param method HTTP method
     * @param path Path of the call
     * @param host hostname
     * @param creds Credentials
     * @param headers Headers for the HTTP request
     * @param listener Response listener
     * @param errorListener error Listener
     */
    public AbstractRequest(int method, String path, String host, Credential creds, Map headers, Response.Listener listener, Response.ErrorListener errorListener) {
        super(method, makeUrl(host, creds, path), errorListener);

        if (headers == null) {
            headers = new HashMap();
        }
        headers.put("Accept", "application/json");

        this.headers = headers;
        this.listener = listener;
    }

    @Override
    public Map getHeaders() throws AuthFailureError {
        return headers != null ? headers : super.getHeaders();
    }

    @Override
    protected void deliverResponse(T response) {
        listener.onResponse(response);
    }

    @Override
    public byte[] getBody() throws AuthFailureError
    {
        if (bodyObject == null)
        {
            return super.getBody();
        }
        Gson gson = new Gson();

        try {
            Log.d("saiod", new String(gson.toJson(bodyObject).getBytes("utf-8")));

            byte[] bt = gson.toJson(bodyObject).getBytes("utf-8");
            return bt;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            throw new RuntimeException(e);

        }
    }
}

The changes here are mainly with removing the Content-Type header. We also do some encoding in the getBody method now. PostRequest.java:

package sohier.me.saiod.android;

import android.util.Log;

import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Response;
import com.android.volley.toolbox.HttpHeaderParser;
import com.google.api.client.auth.oauth2.Credential;

import java.util.Map;

public class PostRequest extends AbstractRequest {

    public PostRequest(String host, Credential creds, String url, Map headers,
                       Response.Listener listener, Response.ErrorListener errorListener, Object send) {
        super(Method.POST, url, host, creds, headers, listener, errorListener);

        this.bodyObject = send;
    }

    @Override
    public String getBodyContentType() {
        return "application/json";
    }
    @Override
    protected Response parseNetworkResponse(NetworkResponse response) {
        try {
            if (response.statusCode == 201) {
                return Response.success("", HttpHeaderParser.parseCacheHeaders(response));
            } else if (response.statusCode == 400) {
                Log.e("oauth/postrequest", "Something was wrong with the request.");
                Log.e("oauth/response", new String(
                        response.data, HttpHeaderParser.parseCharset(response.headers)));
                return Response.error(new ErrorRequest(response));
            } else {
                return Response.error(new ParseError());
            }
        } catch (Exception e) {
            e.printStackTrace();
            return Response.error(new ParseError(e));
        }
    }
}

We override getBodyContentType here to return application/json. We can now send POST requests with a content-type of application/json. Now we have fixed these issues, we can add the code that sends a request to the server to create the Demo. In MainActivity we had a call to datasource.createDemo:

final Demo d = datasource.createDemo(title.getText().toString(), desc.getText().toString());
                            Runnable r = new Runnable() {
                                @Override
                                public void run() {
                                    adapter.add(d);
                                    adapter.notifyDataSetChanged();
                                    Log.d("saiod", "changed.");
                                }
                            };
                            handler.post(r);

We replace this code with the next code:

refreshToken(new CallBackInterface() {
                                @Override
                                public void call() {

                                    Demo data = new Demo();
                                    data.setDescription(desc.getText().toString());
                                    data.setTitle(title.getText().toString());

                                    final Response.Listener rs = new Response.Listener(){
                                        @Override
                                        public void onResponse(String demoResult) {
                                            Log.d("saiod", "Got data..." + demoResult);

                                            // Lets call refreshData :)
                                            refreshData();
                                        }
                                    };
                                    PostRequest rq = new PostRequest(API_HOST, creds, "/demos", null, rs, new Response.ErrorListener(){
                                        @Override
                                        public void onErrorResponse(VolleyError error) {
                                            Log.d("saiod", "Error during request to the server: " + error);

                                            error.printStackTrace();

                                            throw new RuntimeException();
                                        }
                                    }, data);

                                    queue.add(rq);
                                }
                            });

Again, we use here refreshToken, to make sure there is a non expired token. After that we create a PostRequest. Within the response listener for the PostRequest we call refreshData, to update the list. If you run the app now, you will be able to create a brand new Demo. If you look at the log file you will see the result of the call. When something goes wrong, we just throw a runtime exception. Normally, you would of course handle this in a more elegant method. Now we have actually data in the database on our simple website, we can start by creating the list in the app. We already have the database and everything like that, so we just need to fill that with data. Before we start, we need to modify our database a small bit. Because we sync the data with the server, we will need to link the local item to the server item :). To do this, we need to update DBHelper:

package sohier.me.saiod.android;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class DBHelper extends SQLiteOpenHelper {

    public static final String TABLE_DEMO = "demo";
    public static final String DEMO_ID = "_id";
    public static final String DEMO_TITLE = "title";
    public static final String DEMO_DESCRIPTION = "description";
    public static final String DEMO_SERVER_ID = "server_id";

    private static final String DATABASE_NAME = "demo.db";
    private static final int DATABASE_VERSION = 2;

    // Database creation sql statement
    private static final String DATABASE_CREATE = "create table "
            + TABLE_DEMO + "(" + DEMO_ID
            + " integer primary key autoincrement, " + DEMO_TITLE
            + " text not null, " + DEMO_DESCRIPTION + " text not null, "
            + DEMO_SERVER_ID + " integer);";

    public DBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase database) {
        database.execSQL(DATABASE_CREATE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.w(DBHelper.class.getName(),
                "Upgrading database from version " + oldVersion + " to "
                        + newVersion + ", which will destroy all old data"
        );
        db.execSQL("DROP TABLE IF EXISTS " + TABLE_DEMO);
        onCreate(db);
    }
}

What we did here was adding the final String DEMO_SERVER ID, add this to the create table, but also increased the database version. This will automatically upgrade (In our case, drop the existing table and create it again. In normal situations this might not be the right way of doing it of course, but our data is a local copy anyways). To make updating or creating easy, I updated the createDemo to accept demos that already exists:

/**
     * Update or create a new demo in the database.
     *
     * @param demo demo to update or create
     * @return the created or updated demo
     */
    public Demo createOrUpdateDemo(Demo demo) {
        ContentValues values = new ContentValues();
        values.put(DBHelper.DEMO_TITLE, demo.getDescription());
        values.put(DBHelper.DEMO_DESCRIPTION, demo.getDescription());
        values.put(DBHelper.DEMO_SERVER_ID, demo.getId());

        long ID = 0;

        if (demo.getLocalId() == 0)
        {

            ID = database.insert(DBHelper.TABLE_DEMO, null,
                    values);
        }
        else
        {
            ID = database.update(DBHelper.TABLE_DEMO, values, "id = ?", new String[] { String.valueOf(ID) });
        }

        Cursor cursor = database.query(DBHelper.TABLE_DEMO,
                allColumns, DBHelper.DEMO_ID + " = " + ID, null,
                null, null, null);
        cursor.moveToFirst();
        Demo newDemo = cursorToDemo(cursor);
        cursor.close();
        return newDemo;
    }

We also need a method to get 1 specific demo. This is the complete DemoDataSource with the newest changes:

package sohier.me.saiod.android;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;

public class DemoDataSource {

    // Database fields
    private SQLiteDatabase database;
    private DBHelper dbHelper;
    private String[] allColumns = { DBHelper.DEMO_LOCAL_ID, DBHelper.DEMO_TITLE, DBHelper.DEMO_TITLE, DBHelper.DEMO_SERVER_ID };

    public DemoDataSource(Context context) {
        dbHelper = new DBHelper(context);
    }

    public void open()  {
        database = dbHelper.getWritableDatabase();
    }

    public void close() {
        dbHelper.close();
    }

    /**
     * Update or create a new demo in the database.
     *
     * @param demo demo to update or create
     * @return the created or updated demo
     */
    public Demo createOrUpdateDemo(Demo demo) {
        ContentValues values = new ContentValues();
        values.put(DBHelper.DEMO_TITLE, demo.getTitle());
        values.put(DBHelper.DEMO_DESCRIPTION, demo.getDescription());
        values.put(DBHelper.DEMO_SERVER_ID, demo.getId());

        long ID = 0;

        if (demo.getLocalId() == 0)
        {

            ID = database.insert(DBHelper.TABLE_DEMO, null,
                    values);
            Log.d("saiod", "created " + ID);
        }
        else
        {
            ID = database.update(DBHelper.TABLE_DEMO, values, DBHelper.DEMO_LOCAL_ID + " = ?", new String[] { String.valueOf(demo.getLocalId()) });
            Log.d("saiod", "updated " + ID);
        }

        Cursor cursor = database.query(DBHelper.TABLE_DEMO,
                allColumns, DBHelper.DEMO_LOCAL_ID + " = " + ID, null,
                null, null, null);
        cursor.moveToFirst();
        Demo newDemo = cursorToDemo(cursor);
        cursor.close();
        return newDemo;
    }

    public void deleteDemo(Demo demo) {
        deleteDemo(demo.getId());
    }
    public void deleteDemo(long id)
    {
        System.out.println("Demo deleted with id: " + id);
        database.delete(DBHelper.TABLE_DEMO, DBHelper.DEMO_SERVER_ID
                + " = " + id, null);
    }

    public Demo getDemo(long id)
    {
        Cursor cursor = database.query(DBHelper.TABLE_DEMO, allColumns, DBHelper.DEMO_SERVER_ID + " = ?", new String[] { String.valueOf(id) }, null, null, null, null);

        cursor.moveToFirst();
        while (!cursor.isAfterLast())
        {
            return cursorToDemo(cursor);
        }
        Log.d("saiod", "No item found");
        return null;
    }

    public List getAllDemos() {
        List demos = new ArrayList();

        Cursor cursor = database.query(DBHelper.TABLE_DEMO,
                allColumns, null, null, null, null, null);

        cursor.moveToFirst();
        while (!cursor.isAfterLast()) {
            Demo demo = cursorToDemo(cursor);
            demos.add(demo);
            cursor.moveToNext();
        }
        // make sure to close the cursor
        cursor.close();

        return demos;
    }

    private Demo cursorToDemo(Cursor cursor) {
        Demo demo = new Demo();
        demo.setId(cursor.getLong(3));
        demo.setTitle(cursor.getString(1));
        demo.setDescription(cursor.getString(2));
        demo.setLocalId(cursor.getLong(0));
        return demo;
    }
}

Now, lets update the refreshData in mainActivity to actually get the data:

    private void refreshData()
    {
        refreshToken(new CallBackInterface() {
            @Override
            public void call() {

                final CallBackInterface upd = new CallBackInterface() {
                    @Override
                    public void call() {

                    }
                };

                final Response.Listener rs = new Response.Listener(){

                    @Override
                    public void onResponse(DemoResult demoResult) {
                        Log.d("saiod", "Got data...");

                        Log.d("saiod", "size: " + demoResult.demos.length);

                        ArrayList shouldExists = new ArrayList();

                        for (Demo demo : demoResult.demos)
                        {
                            Demo dm = datasource.getDemo(demo.getId());

                            if (dm != null)
                            {
                                // It was found in the database, lets update it :)
                                dm.setDescription(demo.getDescription());
                                dm.setTitle(demo.getTitle());
                            }
                            else
                            {
                                dm = demo;
                            }
                            datasource.createOrUpdateDemo(dm);
                            shouldExists.add(demo.getId());
                        }

                        List ex = datasource.getAllDemos();

                        for (Demo dm : ex)
                        {
                            if (!shouldExists.contains(dm.getId()))
                            {
                                datasource.deleteDemo(dm);
                            }
                        }
                        if (handler != null)
                        {
                            final List list = datasource.getAllDemos();
                            Runnable run = new Runnable() {
                                @Override
                                public void run() {
                                    Log.d("saiod", "In runnable :D");

                                    values.clear(); // remove all existing items
                                    values.addAll(list);
                                    adapter.notifyDataSetChanged();

                                }
                            };
                            handler.post(run);
                        }
                    }
                };


                GsonRequest rq = new GsonRequest(Request.Method.GET, API_HOST, creds, "/demos", DemoResult.class, null, rs, new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        Log.d("saiod", "Error during request to the server: " + error);

                        throw new RuntimeException();
                    }
                });
                queue.add(rq);
            }
        });
    }

So, we now have finally a working application that gets data from the server, saves it locally, and updates and delete removed demos. Now it is time to do deletion itself. We already created a function that deletes locally on longClick on a listItem. Lets modify it so it deletes it on the server, and after that calls refreshData again. In a normal app, you would of course ask up front of the user really wants to delete it. We don’t do that here, it just deletes the demo on long click. So, the code for the listener:

dataList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {

                public boolean onItemLongClick(AdapterView< ?> arg0, View arg1,
                                               int pos, long id) {
                    Log.v("long clicked","pos: " + pos);

                    final Demo item = adapter.getItem(pos);

                    refreshToken(new CallBackInterface() {
                        @Override
                        public void call() {
                            final Response.Listener rs = new Response.Listener() {
                                @Override
                                public void onResponse(String string) {
                                    refreshData(); // We just call refreshData :)
                                }
                            } ;

                            DeleteRequest rq = new DeleteRequest("/demos/" + item.getId(), API_HOST, creds, null, rs, new Response.ErrorListener() {
                                @Override
                                public void onErrorResponse(VolleyError error) {
                                    Log.d("saiod", "Error during request to the server: " + error);

                                    throw new RuntimeException();
                                }
                            });
                            queue.add(rq);
                        }
                    });

                    return true;
                }
            });

The code itself is very simple. It just creates a DeleteRequest, which send a DELETE to the server for the specific demo. After that, we just hard refresh the list. The last thing we haven’t done yet is updating a item. First, lets create a short click listener with some code:

            dataList.setClickable(true);

            dataList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView< ?> parent, View view, int position, long id) {
                    CreateDemoDialog cdd = new CreateDemoDialog(adapter.getItem(position));
                    cdd.show(getFragmentManager(), "");
                }
            });

This create a CreateDemoDialog, only, within the constructor we add a demo. We modified the createDemoDialog to have a constructor that accepts a demo. When we get a demo, we pre fill the fields with the current titles, and after that we send a PUT request instead of a POST. The new CreateDemoDialog:

    public class CreateDemoDialog extends DialogFragment {
        Demo demo = null;
        public CreateDemoDialog()
        {

        }
        public CreateDemoDialog(Demo demo){
            this.demo = demo;
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            // Get the layout inflater
            LayoutInflater inflater = getActivity().getLayoutInflater();

            // Inflate and set the layout for the dialog
            // Pass null as the parent view because its going in the dialog layout
            View vw = inflater.inflate(R.layout.dialog_add_demo, null);
            final EditText title = (EditText)vw.findViewById(R.id.title);
            final EditText desc = (EditText)vw.findViewById(R.id.description);

            if (demo != null)
            {
                // Set fields :).
                title.setText(demo.getTitle());
                desc.setText(demo.getDescription());
            }

            builder.setView(vw)
                    // Add action buttons
                    .setPositiveButton(R.string.create, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int id) {
                            refreshToken(new CallBackInterface() {
                                @Override
                                public void call() {

                                    Demo data = new Demo();
                                    data.setDescription(desc.getText().toString());
                                    data.setTitle(title.getText().toString());

                                    final Response.Listener rs = new Response.Listener() {
                                        @Override
                                        public void onResponse(String demoResult) {
                                            Log.d("saiod", "Got data..." + demoResult);

                                            // Lets call refreshData :)
                                            refreshData();
                                        }
                                    };
                                    Request rq;

                                    Response.ErrorListener err = new Response.ErrorListener() {
                                        @Override
                                        public void onErrorResponse(VolleyError error) {
                                            Log.d("saiod", "Error during request to the server: " + error);

                                            error.printStackTrace();

                                            throw new RuntimeException();
                                        }
                                    };

                                    if (demo == null) {
                                        rq = new PostRequest(API_HOST, creds, "/demos", null, rs, err, data);
                                    } else {
                                        rq = new PutRequest(API_HOST, creds, "/demos/" + demo.getId(), null, rs, err, data);
                                    }
                                    queue.add(rq);
                                }
                            });
                        }
                    })
                    .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int id) {
                            CreateDemoDialog.this.getDialog().cancel();
                        }
                    });
            return builder.create();
        }
    }

With this last change, our android app is finished and fully functional. We have the ability to get, create, update and delete demos, all done via our simple app. I have uploaded a example app into the play store, which can be found here. This app uses the demo site  from www.ip-6.nl. You can login with the username/password admin/admin.

In the next post, we will start with working on the iOS app.

Leave a Reply

Your email address will not be published. Required fields are marked *