oAuth2 with Android (Part 2)

This is the fourth 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 continuing to work on the Android app, and start adding the needed oAuth libraries and adding network functionality.

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 second part.

I assume within this post that you are at the same stage with your code as we ended in part 1.

To handle all oAuth related requests we are going to use a library that does all the hard work for us. This library can be found on Github.

Lets start with adding this library to our app. Because I am using Android Studio, dependencies and things like that are not handled by maven, but with Gradle. You should edit the build.gradle which is located in the android directory, not in the root!
Add in the dependencies section the next line:

compile 'com.wu-man:android-oauth-client:0.0.3'

Android Studio now says the gradle build file is modified and it needs to re-sync, so lets do that. This will download the required libraries from maven central (Even when we don’t use maven, in the background it is still used to download the dependencies), when this is finished, you will see that there are under External Libraries a few extra libs. Most of these libraries are required by the just added oAuth library, and thats why they aren’t in the gradle file.
When we try running the application we will receive a error about conflicting libraries/versions. The reason for this is that the oAuth library we are trying to use depend on a different version of the support library as which we currently include.
This can be solved by changing our dependency:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    // You must install or update the Support Repository through the SDK manager to use this dependency.
    //compile 'com.android.support:appcompat-v7:19.+'
    // Because the android-oauth library has a dependency on v4-r7 we need to use that instead...
    compile 'com.google.android:support-v4:r7'
    compile 'com.wu-man:android-oauth-client:0.0.3'
}

With the new dependency the app should run again.

Because we are going to do network calls, we need to ask for permissions for this from Android. If you forget this call, you will get weird exceptions like this:

E/Oauth error(23513): java.io.IOException: User authorization failed (net::ERR_CACHE_MISS)
E/Oauth error(23513): 	at com.wuman.android.auth.DialogFragmentController.waitForExplicitCode(DialogFragmentController.java:168)
E/Oauth error(23513): 	at com.wuman.android.auth.OAuthManager$3.doWork(OAuthManager.java:205)
E/Oauth error(23513): 	at com.wuman.android.auth.OAuthManager$BaseFutureTask.startTask(OAuthManager.java:420)
E/Oauth error(23513): 	at com.wuman.android.auth.OAuthManager$Future2Task.start(OAuthManager.java:443)
E/Oauth error(23513): 	at com.wuman.android.auth.OAuthManager$5.run(OAuthManager.java:399)
E/Oauth error(23513): 	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:422)
E/Oauth error(23513): 	at java.util.concurrent.FutureTask.run(FutureTask.java:237)
E/Oauth error(23513): 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
E/Oauth error(23513): 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
E/Oauth error(23513): 	at java.lang.Thread.run(Thread.java:841)

Adding the permission is pretty simple, in the AndroidManifest.xml you should add the next line after </application>:

<uses-permission android:name="android.permission.INTERNET"></uses-permission>

Before we continue in the Android app we go back first to the symfony site. To use oAuth we will need some configuration parameters, which are application specific, namely the client and client_secret. Because we will need to generate these settings more then once, I create a console command within symfony. This can be done very easy by adding a file to the Command directory in the bundle.
File (Orignal @ Github):

<?php namespace Oauth\DemoBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class CreateClientCommand extends ContainerAwareCommand {     protected function configure()     {         $this             ->setName('oauth-example:oauth-server:client:create')
            ->setDescription('Creates a new client')
            ->addOption(
                'redirect-uri',
                null,
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
                'Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs.',
                null
            )
            ->addOption(
                'grant-type',
                null,
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
                'Sets allowed grant type for client. Use this option multiple times to set multiple grant types..',
                null
            )
            ->setHelp(
                < <%command.name%command creates a new client.
 
php %command.full_name% [--redirect-uri=...] [--grant-type=...] name
 
EOT
            );
    }
 
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $clientManager = $this->getContainer()->get('fos_oauth_server.client_manager.default');
        $client = $clientManager->createClient();
        $client->setRedirectUris($input->getOption('redirect-uri'));
        $client->setAllowedGrantTypes($input->getOption('grant-type'));
        $clientManager->updateClient($client);
        $output->writeln(
            sprintf(
                'Added a new client with public id %s, secret %s',
                $client->getPublicId(),
                $client->getSecret()
            )
        );
    }
}

When running the command you will get something like this (Normally, you won’t publish these settings of course!):

ipv6:/var/ip6/SymfonyOauthDemo# php app/console oauth-example:oauth-server:client:create --redirect-uri="http://android.local" --grant-type="authorization_code" --grant-type="password" --grant-type="refresh_token" --grant-type="token" --grant-type="client_credentials"
Added a new client with public id 4_93m61sebqts8s4os0wc0884w4cw88kks00so84gkcccw00ks8, secret 4toqis1xbg8w0oswg440g40cgo0cg8c084cw0w0s88s8wgs0g8

Now we can start with creating the code that is required for the oauth connection.
First, we need a few fields in our mainActivity class:

    private static OAuthManager manager;
    private static Credential creds;

    private static final String CLIENT_ID = "4_93m61sebqts8s4os0wc0884w4cw88kks00so84gkcccw00ks8";
    private static final String CLIENT_SECRET = "4toqis1xbg8w0oswg440g40cgo0cg8c084cw0w0s88s8wgs0g8";
    private static final String HOST = "http://ip-6.nl/app_dev.php";
    private static final String API_HOST = "http://api.ip-6.nl/app_dev.php";
    private static final String AUTHORIZE = "/oauth/v2/auth";
    private static final String REQUEST_TOKEN = "/oauth/v2/token";

The first two variables are just the fields with the Manager that handles all oAuth stuff and the actual user data that we later get.
The final fields are the client and secret we generated above.
Lastly, we have the URLs where the oauth login stuff is, and the API host (Which is in our case a different hostname).

All OAuth related things are handled via the OAuthManager class. This class needs configuration before we can use it, so we create a simple getter that created it when the field is still null, if it isn’t null it will return the existing manager.
Getter:

    /**
     * Create a OAuthManager based on our configuration. The manager is only created once, and after
     * that the same manager is returned.
     *
     * Because we don't use the support fragment manager, we need to use the android build in
     * manager for creating the DialogFragmentController.
     *
     * The getRedirectUri is the URI we used when creating the client.
     *
     * @return OAuthManager manager
     */
    public OAuthManager getManager() {
        if (manager == null) {
            AuthorizationDialogController controller;

            controller = new DialogFragmentController(this.getFragmentManager()) {
                @Override
                public boolean isJavascriptEnabledForWebView() {
                    return true;
                }
                @Override
                public String getRedirectUri() throws IOException {
                    return "http://android.local/";
                }
            };

            SharedPreferencesCredentialStore credentialStore =
                    new SharedPreferencesCredentialStore(this.getApplication(),
                            "saiod", new JacksonFactory());

            Log.d("oauthdebug", "HOST" + HOST + " token " + REQUEST_TOKEN);
            GenericUrl url = new GenericUrl(HOST + REQUEST_TOKEN);

            AuthorizationFlow.Builder builder = new AuthorizationFlow.Builder(
                    BearerToken.authorizationHeaderAccessMethod(),
                    AndroidHttp.newCompatibleTransport(),
                    new JacksonFactory(),
                    url,
                    new ClientParametersAuthentication(CLIENT_ID, CLIENT_SECRET),
                    CLIENT_ID,
                    HOST + AUTHORIZE);
            builder.setCredentialStore(credentialStore);
            //builder.setScopes(Arrays.asList("scope1", "scope2"));

            AuthorizationFlow flow = builder.build();

            manager = new OAuthManager(flow, controller);
        }
        return manager;
    }

And some code we will need later on when doing actual requests to the API. When we want to do any call to the API, we need to have a token. This token is only valid for a certain time, so if that time is over, we will need to refresh this token before we can do the actual API call. This method helps with that. Because network related stuff should not happen on the main thread, we create a new thread within the refreshToken method. The call method from the callbackInterface will also be called in this separate thread. So if you want to do UI stuff, you need to use a handler.

    /**
     * This is used with refreshToken to make sure the token is still valid, and if it needs to be
     * refreshed that it will be refreshed before making the actual call to the API.
     *
     * Please note that if you want to update UI stuff in the call method, that you need to use a
     * handler because refreshToken runs in a seperate Thread.
     */
    public interface CallBackInterface {
        /**
         * This method is called when there is a valid token.
         */
        public void call();

    }

    /**
     * This methods checks if the token needs to be refreshed, and if so it refreshes the token.
     * After that, it calls the callback that is provided.
     *
     * This method runs in a separate thread, it is not required to start it separately yourself :)
     *
     * @param cb Callback with the actual API call.
     */
    public void refreshToken(final CallBackInterface cb) {
        new Thread() {
            @Override
            public void run() {
                if (creds.getExpiresInSeconds() < 0) {
                    Log.d("refreshToken", "Token expired: " + creds.getExpiresInSeconds());
                    try {
                        boolean rs = creds.refreshToken();
                        Log.d("refreshToken", "Result: " + rs);
                    } catch (IOException e) {
                        Log.e("refreshToken", "Refresh token failed", e);
                        return;
                    }
                } else {
                    Log.d("refreshToken", "Token not expired: " + creds.getExpiresInSeconds());
                }
                cb.call();
            }
        }.start();
    }

And lastly, we need to add the authorizeExpectly call to the onCreate:

        OAuthManager.OAuthCallback callback = new OAuthManager.OAuthCallback() {
            @Override
            public void run(OAuthManager.OAuthFuture future) {
                try {
                    creds = future.getResult();

                } catch (IOException e) {
                    Log.e("Oauth error", "IO error during oauth", e);
                }
            }
        };

        getManager().authorizeExplicitly("userId", callback, null);

This will make sure there is a user logged in, and in the callback we assign the user to our local Creds.
Now we have this, we can actually start testing the oAuth stuff we created in symfony. So lets run it 🙂
Because we aren’t logged in yet, we get a webpage in our app with a login screen from our website, see the image left. Login screen in the appNow lets try to login. After this, you should see a white screen with a Allow or Deny button, like the image left.Allow or deny something?!?Ofcourse, you want to have something more information, which is very simple in the fos_oauth2_server bundle, but we will look at that in a later part of this series.
After clicking “Allow” you will get a short progress bar, and after that the window is gone. We are logged in now, and we have the User details in our Creds var.
Next time when we start the app, we won’t get the window to login or to allow. The app will remember you, we will just need to refresh the token if it is expired.

Now we have the ability to login into the API, we can actually start calling the API. Doing HTTP requests yourself is kinda boring. Because of that, I always try to use Volley within my Android projects. Volley is a library created by Google, and used in several of them projects. Badly enough is there no maven build for volley (yet), so I created a jar that can be used instead. This jar can be found here. Please note that I am (of course ;)) not responsible for using this jar. 

If you want to build it yourself, first clone the volley repo. After that, run these two commands:

android update project -p . 
ant jar

This will result in a volley.jar in the bin directory. If you place this jar in the libs/ directory of your project it will be included when building the apk.

Before we can use Volley, we will need to have a few types of Requests. In our app, we will need something for GET, POST, PUT and DELETE. All of these requests will be based on a AbstractRequest class.
Because I am going to use GSON for parsing and creating JSON, we will need to include this as dependency:

compile 'com.google.code.gson:gson:2.2.4'

After that, we create the next classes (Like with all code, I suggest to copy the code from github instead from the blog here):
AbstractRequest:

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

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

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

    @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();
        return gson.toJson(bodyObject).getBytes();
    }

}

DeleteRequest:

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 DeleteRequest extends AbstractRequest {

    /**
     * @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 DeleteRequest(String path, String host, Credential creds, Map headers, Response.Listener listener, Response.ErrorListener errorListener) {
        super(Method.DELETE, path, host, creds, headers, listener, errorListener);
    }

    @Override
    protected Response parseNetworkResponse(NetworkResponse response) {
        try {
            if (response.statusCode == 204) {
                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));
        }
    }
}

GsonRequest (This is basically a GET, but can also be used for other methods :))

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.Response.ErrorListener;
import com.android.volley.Response.Listener;
import com.android.volley.toolbox.HttpHeaderParser;
import com.google.api.client.auth.oauth2.Credential;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;

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

/**
 * Volley adapter for JSON requests that will be parsed into Java objects by Gson.
 */
public class GsonRequest extends AbstractRequest {
    private final Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create();
    private final Class clazz;


    /**
     * Make a GET request and return a parsed object from JSON.
     *
     * @param method HTTP method
     * @param host hostname
     * @param cred oAuth credentials
     * @param url     URL of the request to make
     * @param clazz   Relevant class object, for Gson's reflection
     * @param headers Map of request headers
     * @param listener listener
     * @param errorListener listener
     */
    public GsonRequest(int method, String host, Credential cred, String url, Class clazz, Map headers,
                       Listener listener, ErrorListener errorListener) {

        super(method, url, host, cred, headers, listener, errorListener);

        this.clazz = clazz;
    }

    @Override
    /**
     * Parse the network response from the server. Because we use GSON, we will call gson.fromJson.
     * @param response NetworkResponse
     * @return Response
     */
    protected Response parseNetworkResponse(NetworkResponse response) {
        try {
            String json = new String(
                    response.data, HttpHeaderParser.parseCharset(response.headers));

            Log.d("GsonRequest", "Got back from server: " + json);

            return Response.success(
                    gson.fromJson(json, clazz), HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (JsonSyntaxException e) {
            return Response.error(new ParseError(e));
        }
    }
}

PostRequest:

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.HashMap;
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
    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));
        }
    }
}

PutRequest:

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 PutRequest  extends AbstractRequest{
    public PutRequest(String host, Credential creds, String url, Map headers,
                       Response.Listener listener, Response.ErrorListener errorListener, Object send) {
        super(Method.PUT, url, host, creds, headers, listener, errorListener);

        this.bodyObject = send;
    }

    @Override
    protected Response parseNetworkResponse(NetworkResponse response) {
        try {
            if (response.statusCode == 204) {
                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));
        }
    }
}

Some of this code is kinda duplicated and should use some extending instead of this duplication.

Now we can start using Volley. First of all, we need a RequestQueue to add requests to. So lets add a new field:

private RequestQueue queue;

And initialize in the onCreate:

queue = Volley.newRequestQueue(this);

Now we call a new method refreshData in the callback from the login:

OAuthManager.OAuthCallback callback = new OAuthManager.OAuthCallback() {
            @Override
            public void run(OAuthManager.OAuthFuture future) {
                try {
                    creds = future.getResult();

                    Log.d("oauth login", "Login succesfull. Token: "  + creds.getAccessToken() + " Time: " + creds.getExpiresInSeconds());

                    refreshData();

                } catch (IOException e) {
                    Log.e("Oauth error", "IO error during oauth", e);
                }
            }
        };

And the refreshData function:

private void refreshData()
    {
        refreshToken(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);
                    }
                };

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

With this method you can see how we call the refreshToken method, and create a callback class.
When you run this app, you will see something like this in the log:

05-25 11:51:07.799  25303-25317/? D/GsonRequest﹕ Got back from server: {"demos":[]}
05-25 11:51:07.819  25303-25303/? D/saiod﹕ Got data...
05-25 11:51:07.819  25303-25303/? D/saiod﹕ size: 0

Because we have no data yet in our database, we don’t get anything back yet.

In the next, and last about Android, part we will be adding functions to add, delete and edit Demo’s. We will also actually get them added to the database.

5 thoughts on “oAuth2 with Android (Part 2)

  1. Could you clarify how the redirect after obtaining the token works? It doesn’t appear that you have an intent to handle it in your source.

  2. Very useful article Paul, it helped me a lot!
    Any idea about how to implement a logout? I’m loofing for a way to “erase” creds, but I’m not finding it.
    Thanks!

Leave a Reply

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