net.niyonkuru.koodroid.service.SessionService.java Source code

Java tutorial

Introduction

Here is the source code for net.niyonkuru.koodroid.service.SessionService.java

Source

/*
 * Copyright 2012 Mike Niyonkuru
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.niyonkuru.koodroid.service;

import android.app.IntentService;
import android.app.backup.BackupManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.text.format.DateUtils;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.WebView;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

import net.niyonkuru.koodroid.App;
import net.niyonkuru.koodroid.R;
import net.niyonkuru.koodroid.html.ErrorHandler;
import net.niyonkuru.koodroid.html.HtmlHandler.HandlerException;
import net.niyonkuru.koodroid.html.LinksHandler;
import net.niyonkuru.koodroid.html.SubscribersHandler;
import net.niyonkuru.koodroid.provider.AccountContract.Subscribers;
import net.niyonkuru.koodroid.provider.SettingsContract.Settings;
import net.niyonkuru.koodroid.security.EasySSLSocketFactory;
import net.niyonkuru.koodroid.util.Config;
import net.niyonkuru.koodroid.util.IntentUtils;
import net.niyonkuru.koodroid.util.NetworkUtils;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

import com.crittercism.app.Crittercism;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.util.EntityUtils;

import static net.niyonkuru.koodroid.BuildConfig.DEBUG;

/**
 * Authenticates a username and password with the Koodo web server and sends back a result to the caller.
 */
public class SessionService extends IntentService {
    private static final String TAG = "SessionService";

    public static final String LOGIN_FINISHED = "net.niyonkuru.koodroid.action.LOGIN_FINISHED";

    public static final String EXTRA_REQUEST = "request";
    public static final String EXTRA_EMAIL = "email";
    public static final String EXTRA_PASSWORD = "password";
    public static final String EXTRA_STATUS_RECEIVER = "statusReceiver";
    public static final String EXTRA_BROADCAST = "broadcast";

    public static final int REQUEST_LOGIN = 0x1;
    public static final int REQUEST_LOGOUT = 0x2;

    public static final int STATUS_RUNNING = 0x1;
    public static final int STATUS_ERROR = 0x2;
    public static final int STATUS_FINISHED = 0x3;

    private Settings mSettings;

    private DefaultHttpClient mHttpClient;

    private HttpPost mPostRequest;

    private final Thread mDisconnect;

    public SessionService() {
        super(TAG);

        /* do not make network requests on the UI Thread */
        mDisconnect = new Thread(new Runnable() {
            @Override
            public void run() {
                if (mPostRequest != null) {
                    mPostRequest.abort();
                }
                if (mHttpClient != null) {
                    mHttpClient.getConnectionManager().shutdown();
                }
            }
        });
    }

    @Override
    public void onCreate() {
        super.onCreate();

        mSettings = App.getSettings();

        mHttpClient = getHttpClient(this);

        setProvinceCookie(mHttpClient);
    }

    /**
     * Adds a province cookie to the {@link CookieStore} of the {@link DefaultHttpClient} object. This will stop the
     * Koodo Servers from prompting us with region selection on login.
     */
    private static void setProvinceCookie(DefaultHttpClient httpClient) {
        CookieStore cookieStore = httpClient.getCookieStore();

        BasicClientCookie cookie = new BasicClientCookie("prov", "on");
        cookie.setDomain("." + Config.DOMAIN);
        cookie.setPath("/");
        cookieStore.addCookie(cookie);
    }

    @Override
    public void onHandleIntent(Intent intent) {
        if (DEBUG)
            Log.d(TAG, "onHandleIntent(intent=" + intent.toString() + ")");

        int request = intent.getIntExtra(EXTRA_REQUEST, REQUEST_LOGIN);
        if (request == REQUEST_LOGOUT) {
            logout();
            return;
        }

        final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_STATUS_RECEIVER);

        final String email = intent.getStringExtra(EXTRA_EMAIL);
        final String password = intent.getStringExtra(EXTRA_PASSWORD);
        boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false);

        /* totally ignore this request until full credentials are provided */
        if (email == null || password == null)
            return;

        final long startLogin = System.currentTimeMillis();
        final long lastLogin = mSettings.lastLogin();

        /* if the last successful login is within the last 15 minutes */
        if (startLogin - lastLogin <= DateUtils.MINUTE_IN_MILLIS * 15) {

            /* do a credentials check again the local data store */
            if (email.equals(mSettings.email()) && password.equals(mSettings.password())) {
                if (broadcast)
                    IntentUtils.callWidget(this, LOGIN_FINISHED);
                announce(receiver, STATUS_FINISHED);
                return;
            }
        }

        try {
            if (NetworkUtils.isConnected(this)) {
                /* announce to the caller that we are now running */
                announce(receiver, STATUS_RUNNING);

            } else
                throw new ServiceException(getString(R.string.error_network_down));

            if (mSettings.email() == null) {
                /* this is likely a new user */
                Crittercism.leaveBreadcrumb(TAG + ": first_time_login");
            }

            login(email, password);
            saveCookies();

            if (DEBUG)
                Log.d(TAG, "login took " + (System.currentTimeMillis() - startLogin) + "ms");

        } catch (IOException e) {
            if (DEBUG)
                Log.e(TAG, "Problem while logging in", e);

            /* if the exception was simply from an abort */
            if (mPostRequest != null && mPostRequest.isAborted())
                return;

            if (receiver != null) {
                // Pass back error to surface listener
                final Bundle bundle = new Bundle();
                bundle.putString(Intent.EXTRA_TEXT, e.toString());
                receiver.send(STATUS_ERROR, bundle);
            }
            return; /* do not announce success below */
        }

        if (broadcast)
            IntentUtils.callWidget(this, LOGIN_FINISHED);
        announce(receiver, STATUS_FINISHED);
    }

    /**
     * Announce success to the available receiver
     */
    private void announce(ResultReceiver receiver, int status) {
        if (receiver != null) {
            receiver.send(status, Bundle.EMPTY);
        }
    }

    @Override
    public void onDestroy() {
        if (DEBUG)
            Log.d(TAG, "onDestroy()");
        super.onDestroy();

        mDisconnect.start();
    }

    private void login(String email, String password) throws IOException {
        try {
            mPostRequest = new HttpPost(new URI(Config.LOGIN_URL));
        } catch (URISyntaxException e) {
            throw new ServiceException(e.getMessage());
        }

        List<NameValuePair> formData = buildFormData(email, password, getString(R.string.locale));
        try {
            /* fill the login request with form values */
            mPostRequest.setEntity(new UrlEncodedFormEntity(formData, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new ServiceException(e.getMessage());
        }

        Document doc;

        try {
            final HttpResponse response = mHttpClient.execute(mPostRequest);
            final Integer status = response.getStatusLine().getStatusCode();

            /* scumbag server did not return a 200 code */
            if (status != HttpStatus.SC_OK)
                throw new ServiceException(status.toString());

            HttpEntity entity = response.getEntity();

            doc = Jsoup.parse(EntityUtils.toString(response.getEntity()));

            if (entity != null) {
                entity.consumeContent();
            }
        } catch (UnknownHostException e) {
            throw new ServiceException(e.getMessage());
        } catch (ConnectTimeoutException e) {
            throw new ServiceException(getString(R.string.error_connection_timeout));
        } catch (ClientProtocolException e) {
            throw new ServiceException(e.getMessage());
        } catch (ParseException e) {
            throw new ServiceException(e.getMessage());
        } catch (SocketTimeoutException e) {
            throw new ServiceException(getString(R.string.error_response_timeout));
        } catch (IOException e) {
            // This could be caused by closing the httpclient connection manager
            throw new ServiceException(e.getMessage());
        }

        final Resources resources = getResources();
        final ContentResolver resolver = getContentResolver();

        /* this is a new user logging in */
        if (!email.equalsIgnoreCase(mSettings.email())) {
            /* clear old preferences */
            resolver.delete(Settings.CONTENT_URI, null, null);
        }

        try {
            new SubscribersHandler(resources, email).parseAndApply(doc, resolver);
            new LinksHandler(resources).parseAndApply(doc, resolver);

            ContentValues values = new ContentValues(3);
            values.put(Settings.EMAIL, email);
            values.put(Settings.PASSWORD, password);
            values.put(Settings.LAST_LOGIN, System.currentTimeMillis());

            resolver.insert(Settings.CONTENT_URI, values);

            new BackupManager(this).dataChanged();

        } catch (HandlerException e) {
            /* check if these errors could be caused by invalid pages */
            new ErrorHandler(resources).parseAndThrow(doc);
            throw e;
        }
    }

    private List<NameValuePair> buildFormData(String username, String password, String language) {
        List<NameValuePair> formData = new ArrayList<NameValuePair>();

        formData.add(new BasicNameValuePair("IDToken1", username));
        formData.add(new BasicNameValuePair("IDToken2", password));
        formData.add(new BasicNameValuePair("service", "koodo"));
        formData.add(new BasicNameValuePair("realm", "koodo"));
        formData.add(new BasicNameValuePair("portal", "koodo"));
        formData.add(new BasicNameValuePair("locale", language));
        formData.add(new BasicNameValuePair("encoded", "false"));
        formData.add(new BasicNameValuePair("goto", null));

        return formData;
    }

    private void logout() {
        final ContentResolver cr = getContentResolver();
        final String email = mSettings.email();

        /* clear application settings */
        cr.delete(Settings.CONTENT_URI, null, null);

        try {
            /* allow a 5 second window for batch operations to finish */
            Thread.sleep(5 * DateUtils.SECOND_IN_MILLIS);
        } catch (InterruptedException ignored) {
        }
        /* clear database data */
        cr.delete(Subscribers.CONTENT_URI, Subscribers.SUBSCRIBER_EMAIL + "='" + email + "'", null);

        CookieManager cookieManager = CookieManager.getInstance();
        if (cookieManager != null) {
            cookieManager.removeAllCookie();
        }

        stopSelf();
    }

    /**
     * Save cookies for later use by a {@link WebView}
     */
    private void saveCookies() {
        List<Cookie> cookies = mHttpClient.getCookieStore().getCookies();
        if (cookies.isEmpty())
            return;

        CookieSyncManager.createInstance(this);
        CookieManager cookieManager = CookieManager.getInstance();

        for (Cookie cookie : cookies) {
            String url = (cookie.isSecure() ? "https://" : "http://") + Config.DOMAIN + cookie.getPath();

            String value = cookie.getName() + "=" + cookie.getValue();

            if (cookie.getDomain().startsWith(".")) {
                value += "; domain=" + cookie.getDomain();
            }

            cookieManager.setCookie(url, value);
        }
    }

    /**
     * Create a DefaultHttpClient Object with several parameters set
     *
     * @return a new DefaultHttpClient Object
     */
    private static DefaultHttpClient getHttpClient(Context context) {
        HttpParams params = new BasicHttpParams();

        // Use generous timeouts for slow mobile networks
        int timeout = (int) (25 * DateUtils.SECOND_IN_MILLIS);

        HttpConnectionParams.setConnectionTimeout(params, timeout);
        HttpConnectionParams.setSoTimeout(params, timeout);

        /* Inexplicably speeds up POST requests? */
        HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
        HttpProtocolParams.setUserAgent(params, buildUserAgent(context));
        HttpConnectionParams.setSocketBufferSize(params, 8192);

        SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        schemeRegistry.register(new Scheme("https", new EasySSLSocketFactory(), 443));

        ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);

        return new DefaultHttpClient(cm, params);
    }

    /**
     * Build and return a user-agent string that can identify this application to remote servers. Contains the package
     * name and version code.
     */
    private static String buildUserAgent(Context context) {
        try {
            final PackageManager manager = context.getPackageManager();
            final PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0);

            return info.packageName + "/" + info.versionName + " (" + info.versionCode + ")";
        } catch (NameNotFoundException e) {
            return null;
        }
    }
}