org.quantumbadger.redreader.reddit.api.RedditOAuth.java Source code

Java tutorial

Introduction

Here is the source code for org.quantumbadger.redreader.reddit.api.RedditOAuth.java

Source

/*******************************************************************************
 * This file is part of RedReader.
 *
 * RedReader is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * RedReader is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with RedReader.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package org.quantumbadger.redreader.reddit.api;

import android.content.Context;
import android.net.Uri;
import android.os.SystemClock;
import android.util.Base64;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.quantumbadger.redreader.R;
import org.quantumbadger.redreader.account.RedditAccount;
import org.quantumbadger.redreader.account.RedditAccountManager;
import org.quantumbadger.redreader.cache.CacheManager;
import org.quantumbadger.redreader.common.Constants;
import org.quantumbadger.redreader.common.RRError;
import org.quantumbadger.redreader.jsonwrap.JsonBufferedObject;
import org.quantumbadger.redreader.jsonwrap.JsonValue;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;

public final class RedditOAuth {

    private static final String REDIRECT_URI = "http://rr_oauth_redir";
    private static final String CLIENT_ID = "m_zCW1Dixs9WLA";
    private static final String ALL_SCOPES = "identity,edit,flair,history,modconfig,modflair,modlog,modposts,modwiki,mysubreddits,privatemessages,"
            + "read,report,save,submit,subscribe,vote,wikiedit,wikiread";

    private static final String ACCESS_TOKEN_URL = "https://www.reddit.com/api/v1/access_token";

    public static class Token {

        public final String token;

        public Token(final String token) {
            this.token = token;
        }

        @Override
        public String toString() {
            return token;
        }
    }

    public static final class AccessToken extends Token {

        private final long mMonotonicTimestamp;

        public AccessToken(final String token) {
            super(token);
            mMonotonicTimestamp = SystemClock.elapsedRealtime();
        }

        public boolean isExpired() {
            final long halfHourInMs = 30 * 60 * 1000;
            return mMonotonicTimestamp + halfHourInMs < SystemClock.elapsedRealtime();
        }
    }

    public static final class RefreshToken extends Token {
        public RefreshToken(final String token) {
            super(token);
        }
    }

    private enum FetchRefreshTokenResultStatus {
        SUCCESS, USER_REFUSED_PERMISSION, INVALID_REQUEST, INVALID_RESPONSE, CONNECTION_ERROR, UNKNOWN_ERROR
    }

    private enum FetchUserInfoResultStatus {
        SUCCESS, INVALID_RESPONSE, CONNECTION_ERROR, UNKNOWN_ERROR
    }

    private static final class FetchRefreshTokenResult {

        public final FetchRefreshTokenResultStatus status;
        public final RRError error;

        public final RefreshToken refreshToken;
        public final AccessToken accessToken;

        public FetchRefreshTokenResult(final FetchRefreshTokenResultStatus status, final RRError error) {
            this.status = status;
            this.error = error;
            this.refreshToken = null;
            this.accessToken = null;
        }

        public FetchRefreshTokenResult(final RefreshToken refreshToken, final AccessToken accessToken) {
            this.status = FetchRefreshTokenResultStatus.SUCCESS;
            this.error = null;
            this.refreshToken = refreshToken;
            this.accessToken = accessToken;
        }
    }

    private static final class FetchUserInfoResult {

        public final FetchUserInfoResultStatus status;
        public final RRError error;

        public final String username;

        public FetchUserInfoResult(final FetchUserInfoResultStatus status, final RRError error) {
            this.status = status;
            this.error = error;
            this.username = null;
        }

        public FetchUserInfoResult(final String username) {
            this.status = FetchUserInfoResultStatus.SUCCESS;
            this.error = null;
            this.username = username;
        }
    }

    public static Uri getPromptUri() {

        final Uri.Builder uri = Uri.parse("https://www.reddit.com/api/v1/authorize.compact").buildUpon();

        uri.appendQueryParameter("response_type", "code");
        uri.appendQueryParameter("duration", "permanent");
        uri.appendQueryParameter("state", "Texas");
        uri.appendQueryParameter("redirect_uri", REDIRECT_URI);
        uri.appendQueryParameter("client_id", CLIENT_ID);
        uri.appendQueryParameter("scope", ALL_SCOPES);

        return uri.build();
    }

    private static FetchRefreshTokenResult fetchRefreshTokenSynchronous(final Context context,
            final Uri redirectUri) {

        final String error = redirectUri.getQueryParameter("error");

        if (error != null) {

            if (error.equals("access_denied")) {
                return new FetchRefreshTokenResult(FetchRefreshTokenResultStatus.USER_REFUSED_PERMISSION,
                        new RRError(context.getString(R.string.error_title_login_user_denied_permission),
                                context.getString(R.string.error_message_login_user_denied_permission)));

            } else {
                return new FetchRefreshTokenResult(FetchRefreshTokenResultStatus.INVALID_REQUEST,
                        new RRError(context.getString(R.string.error_title_login_unknown_reddit_error) + error,
                                context.getString(R.string.error_unknown_message)));
            }
        }

        final String code = redirectUri.getQueryParameter("code");

        if (code == null) {
            return new FetchRefreshTokenResult(FetchRefreshTokenResultStatus.INVALID_RESPONSE,
                    new RRError(context.getString(R.string.error_unknown_title),
                            context.getString(R.string.error_unknown_message)));
        }

        final String uri = ACCESS_TOKEN_URL;
        StatusLine responseStatus = null;

        try {
            final HttpClient httpClient = CacheManager.createHttpClient(context);

            final HttpPost request = new HttpPost(uri);

            final ArrayList<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(3);
            nameValuePairs.add(new BasicNameValuePair("grant_type", "authorization_code"));
            nameValuePairs.add(new BasicNameValuePair("code", code));
            nameValuePairs.add(new BasicNameValuePair("redirect_uri", REDIRECT_URI));
            request.setEntity(new UrlEncodedFormEntity(nameValuePairs));

            request.addHeader("Authorization", "Basic "
                    + Base64.encodeToString((CLIENT_ID + ":").getBytes(), Base64.URL_SAFE | Base64.NO_WRAP));

            final HttpResponse response = httpClient.execute(request);
            responseStatus = response.getStatusLine();

            if (responseStatus.getStatusCode() != 200) {
                return new FetchRefreshTokenResult(FetchRefreshTokenResultStatus.UNKNOWN_ERROR,
                        new RRError(context.getString(R.string.error_unknown_title),
                                context.getString(R.string.message_cannotlogin), null, responseStatus,
                                request.getURI().toString()));
            }

            final JsonValue jsonValue = new JsonValue(response.getEntity().getContent());
            jsonValue.buildInThisThread();
            final JsonBufferedObject responseObject = jsonValue.asObject();

            final RefreshToken refreshToken = new RefreshToken(responseObject.getString("refresh_token"));
            final AccessToken accessToken = new AccessToken(responseObject.getString("access_token"));

            return new FetchRefreshTokenResult(refreshToken, accessToken);

        } catch (IOException e) {
            return new FetchRefreshTokenResult(FetchRefreshTokenResultStatus.CONNECTION_ERROR,
                    new RRError(context.getString(R.string.error_connection_title),
                            context.getString(R.string.error_connection_message), e, responseStatus, uri));

        } catch (Throwable t) {
            return new FetchRefreshTokenResult(FetchRefreshTokenResultStatus.UNKNOWN_ERROR,
                    new RRError(context.getString(R.string.error_unknown_title),
                            context.getString(R.string.error_unknown_message), t, responseStatus, uri));
        }
    }

    private static FetchUserInfoResult fetchUserInfoSynchronous(final Context context,
            final AccessToken accessToken) {

        final URI uri = Constants.Reddit.getUri(Constants.Reddit.PATH_ME);
        StatusLine responseStatus = null;

        try {
            final HttpClient httpClient = CacheManager.createHttpClient(context);

            final HttpGet request = new HttpGet(uri);
            request.addHeader("Authorization", "bearer " + accessToken.token);

            final HttpResponse response = httpClient.execute(request);
            responseStatus = response.getStatusLine();

            if (responseStatus.getStatusCode() != 200) {
                return new FetchUserInfoResult(FetchUserInfoResultStatus.CONNECTION_ERROR,
                        new RRError(context.getString(R.string.error_unknown_title),
                                context.getString(R.string.error_unknown_message), null, responseStatus,
                                uri.toString()));
            }

            final JsonValue jsonValue = new JsonValue(response.getEntity().getContent());
            jsonValue.buildInThisThread();
            final JsonBufferedObject responseObject = jsonValue.asObject();

            final String username = responseObject.getString("name");

            if (username == null || username.length() == 0) {
                return new FetchUserInfoResult(FetchUserInfoResultStatus.INVALID_RESPONSE,
                        new RRError(context.getString(R.string.error_unknown_title),
                                context.getString(R.string.error_unknown_message), null, responseStatus,
                                uri.toString()));
            }

            return new FetchUserInfoResult(username);

        } catch (IOException e) {
            return new FetchUserInfoResult(FetchUserInfoResultStatus.CONNECTION_ERROR,
                    new RRError(context.getString(R.string.error_connection_title),
                            context.getString(R.string.error_connection_message), e, responseStatus,
                            uri.toString()));

        } catch (Throwable t) {
            return new FetchUserInfoResult(FetchUserInfoResultStatus.UNKNOWN_ERROR,
                    new RRError(context.getString(R.string.error_unknown_title),
                            context.getString(R.string.error_unknown_message), t, responseStatus, uri.toString()));
        }
    }

    public enum LoginError {
        SUCCESS, USER_REFUSED_PERMISSION, CONNECTION_ERROR, UNKNOWN_ERROR;

        static LoginError fromFetchRefreshTokenStatus(FetchRefreshTokenResultStatus status) {
            switch (status) {
            case SUCCESS:
                return SUCCESS;
            case USER_REFUSED_PERMISSION:
                return USER_REFUSED_PERMISSION;
            case INVALID_REQUEST:
                return UNKNOWN_ERROR;
            case INVALID_RESPONSE:
                return UNKNOWN_ERROR;
            case CONNECTION_ERROR:
                return CONNECTION_ERROR;
            case UNKNOWN_ERROR:
                return UNKNOWN_ERROR;
            }

            return UNKNOWN_ERROR;
        }

        static LoginError fromFetchUserInfoStatus(FetchUserInfoResultStatus status) {
            switch (status) {
            case SUCCESS:
                return SUCCESS;
            case INVALID_RESPONSE:
                return UNKNOWN_ERROR;
            case CONNECTION_ERROR:
                return CONNECTION_ERROR;
            case UNKNOWN_ERROR:
                return UNKNOWN_ERROR;
            }

            return UNKNOWN_ERROR;
        }
    }

    public interface LoginListener {

        void onLoginSuccess(RedditAccount account);

        void onLoginFailure(LoginError error, RRError details);
    }

    public static void loginAsynchronous(final Context context, final Uri redirectUri,
            final LoginListener listener) {

        new Thread() {
            @Override
            public void run() {
                try {

                    final FetchRefreshTokenResult fetchRefreshTokenResult = fetchRefreshTokenSynchronous(context,
                            redirectUri);

                    if (fetchRefreshTokenResult.status != FetchRefreshTokenResultStatus.SUCCESS) {

                        listener.onLoginFailure(
                                LoginError.fromFetchRefreshTokenStatus(fetchRefreshTokenResult.status),
                                fetchRefreshTokenResult.error);

                        return;
                    }

                    final FetchUserInfoResult fetchUserInfoResult = fetchUserInfoSynchronous(context,
                            fetchRefreshTokenResult.accessToken);

                    if (fetchUserInfoResult.status != FetchUserInfoResultStatus.SUCCESS) {
                        listener.onLoginFailure(LoginError.fromFetchUserInfoStatus(fetchUserInfoResult.status),
                                fetchUserInfoResult.error);

                        return;
                    }

                    final RedditAccount account = new RedditAccount(fetchUserInfoResult.username,
                            fetchRefreshTokenResult.refreshToken, 0);

                    account.setAccessToken(fetchRefreshTokenResult.accessToken);

                    final RedditAccountManager accountManager = RedditAccountManager.getInstance(context);
                    accountManager.addAccount(account);
                    accountManager.setDefaultAccount(account);

                    listener.onLoginSuccess(account);

                } catch (Throwable t) {
                    listener.onLoginFailure(LoginError.UNKNOWN_ERROR,
                            new RRError(context.getString(R.string.error_unknown_title),
                                    context.getString(R.string.error_unknown_message), t));
                }
            }
        }.start();
    }

    public enum FetchAccessTokenResultStatus {
        SUCCESS, INVALID_REQUEST, INVALID_RESPONSE, CONNECTION_ERROR, UNKNOWN_ERROR
    }

    public static final class FetchAccessTokenResult {

        public final FetchAccessTokenResultStatus status;
        public final RRError error;

        public final AccessToken accessToken;

        public FetchAccessTokenResult(final FetchAccessTokenResultStatus status, final RRError error) {
            this.status = status;
            this.error = error;
            this.accessToken = null;
        }

        public FetchAccessTokenResult(final AccessToken accessToken) {
            this.status = FetchAccessTokenResultStatus.SUCCESS;
            this.error = null;
            this.accessToken = accessToken;
        }
    }

    public static FetchAccessTokenResult fetchAccessTokenSynchronous(final Context context,
            final RefreshToken refreshToken) {

        final String uri = ACCESS_TOKEN_URL;
        StatusLine responseStatus = null;

        try {
            final HttpClient httpClient = CacheManager.createHttpClient(context);

            final HttpPost request = new HttpPost(uri);

            final ArrayList<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
            nameValuePairs.add(new BasicNameValuePair("grant_type", "refresh_token"));
            nameValuePairs.add(new BasicNameValuePair("refresh_token", refreshToken.token));
            request.setEntity(new UrlEncodedFormEntity(nameValuePairs));

            request.addHeader("Authorization", "Basic "
                    + Base64.encodeToString((CLIENT_ID + ":").getBytes(), Base64.URL_SAFE | Base64.NO_WRAP));

            final HttpResponse response = httpClient.execute(request);
            responseStatus = response.getStatusLine();

            if (responseStatus.getStatusCode() != 200) {
                return new FetchAccessTokenResult(FetchAccessTokenResultStatus.UNKNOWN_ERROR,
                        new RRError(context.getString(R.string.error_unknown_title),
                                context.getString(R.string.message_cannotlogin), null, responseStatus,
                                request.getURI().toString()));
            }

            final JsonValue jsonValue = new JsonValue(response.getEntity().getContent());
            jsonValue.buildInThisThread();
            final JsonBufferedObject responseObject = jsonValue.asObject();

            final String accessTokenString = responseObject.getString("access_token");

            if (accessTokenString == null) {
                throw new RuntimeException("Null access token: " + responseObject.getString("error"));
            }

            final AccessToken accessToken = new AccessToken(accessTokenString);

            return new FetchAccessTokenResult(accessToken);

        } catch (IOException e) {
            return new FetchAccessTokenResult(FetchAccessTokenResultStatus.CONNECTION_ERROR,
                    new RRError(context.getString(R.string.error_connection_title),
                            context.getString(R.string.error_connection_message), e, responseStatus, uri));

        } catch (Throwable t) {
            return new FetchAccessTokenResult(FetchAccessTokenResultStatus.UNKNOWN_ERROR,
                    new RRError(context.getString(R.string.error_unknown_title),
                            context.getString(R.string.error_unknown_message), t, responseStatus, uri));
        }
    }

    public static FetchAccessTokenResult fetchAnonymousAccessTokenSynchronous(final Context context) {

        final String uri = ACCESS_TOKEN_URL;
        StatusLine responseStatus = null;

        try {
            final HttpClient httpClient = CacheManager.createHttpClient(context);

            final HttpPost request = new HttpPost(uri);

            final ArrayList<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
            nameValuePairs
                    .add(new BasicNameValuePair("grant_type", "https://oauth.reddit.com/grants/installed_client"));
            nameValuePairs.add(new BasicNameValuePair("device_id", "DO_NOT_TRACK_THIS_DEVICE"));
            request.setEntity(new UrlEncodedFormEntity(nameValuePairs));

            request.addHeader("Authorization", "Basic "
                    + Base64.encodeToString((CLIENT_ID + ":").getBytes(), Base64.URL_SAFE | Base64.NO_WRAP));

            final HttpResponse response = httpClient.execute(request);
            responseStatus = response.getStatusLine();

            if (responseStatus.getStatusCode() != 200) {
                return new FetchAccessTokenResult(FetchAccessTokenResultStatus.UNKNOWN_ERROR,
                        new RRError(context.getString(R.string.error_unknown_title),
                                context.getString(R.string.message_cannotlogin), null, responseStatus,
                                request.getURI().toString()));
            }

            final JsonValue jsonValue = new JsonValue(response.getEntity().getContent());
            jsonValue.buildInThisThread();
            final JsonBufferedObject responseObject = jsonValue.asObject();

            final String accessTokenString = responseObject.getString("access_token");

            if (accessTokenString == null) {
                throw new RuntimeException("Null access token: " + responseObject.getString("error"));
            }

            final AccessToken accessToken = new AccessToken(accessTokenString);

            return new FetchAccessTokenResult(accessToken);

        } catch (IOException e) {
            return new FetchAccessTokenResult(FetchAccessTokenResultStatus.CONNECTION_ERROR,
                    new RRError(context.getString(R.string.error_connection_title),
                            context.getString(R.string.error_connection_message), e, responseStatus, uri));

        } catch (Throwable t) {
            return new FetchAccessTokenResult(FetchAccessTokenResultStatus.UNKNOWN_ERROR,
                    new RRError(context.getString(R.string.error_unknown_title),
                            context.getString(R.string.message_cannotlogin), t, responseStatus, uri));
        }
    }
}