com.radicaldynamic.groupinform.services.InformOnlineService.java Source code

Java tutorial

Introduction

Here is the source code for com.radicaldynamic.groupinform.services.InformOnlineService.java

Source

/*
 * Copyright (C) 2009 University of Washington
 * 
 * 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 com.radicaldynamic.groupinform.services;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Binder;
import android.os.ConditionVariable;
import android.os.IBinder;
import android.util.Log;

import com.radicaldynamic.groupinform.activities.AccountDeviceList;
import com.radicaldynamic.groupinform.activities.AccountFolderList;
import com.radicaldynamic.groupinform.application.Collect;
import com.radicaldynamic.groupinform.logic.AccountDevice;
import com.radicaldynamic.groupinform.logic.InformOnlineSession;
import com.radicaldynamic.groupinform.logic.InformOnlineState;
import com.radicaldynamic.groupinform.utilities.HttpUtils;
import com.radicaldynamic.groupinform.utilities.FileUtilsExtended;

/**
 * 
 */
public class InformOnlineService extends Service {
    private static final String t = "InformOnlineService: ";

    // Whether or not we are currently attempting to connect to the service
    private boolean mConnecting = false;

    // Assume that we are not initialized when this service starts
    private boolean mInitialized = false;

    // Assume that the "online service" is not answering when we start
    private boolean mServicePingSuccessful = false;

    // Assume that we are not signed in when this service starts
    private boolean mSignedIn = false;

    // This is the object that receives interactions from clients.  
    // See RemoteService for a more complete example.
    private final IBinder mBinder = new LocalBinder();

    private ConditionVariable mCondition;

    private Runnable mTask = new Runnable() {
        public void run() {
            for (int i = 1; i > 0; ++i) {
                if (mConnecting == false)
                    connect(false);
                // Retry connection to Inform Online service every 10 minutes
                if (mCondition.block(600 * 1000))
                    break;
            }
        }
    };

    @Override
    public void onCreate() {
        // Do some basic initialization for this service
        Collect.getInstance().setInformOnlineState(new InformOnlineState(getApplicationContext()));
        restoreSession();

        // connect() may not be run because of offline mode but metadata should still be loaded if available
        if (Collect.getInstance().getInformOnlineState().isOfflineModeEnabled()) {
            AccountDeviceList.loadDeviceList();
            AccountFolderList.loadFolderList();
        }

        Thread persistentConnectionThread = new Thread(null, mTask, "InformOnlineService");
        mCondition = new ConditionVariable(false);
        persistentConnectionThread.start();
    }

    /*
     * (non-Javadoc)
     * @see android.app.Service#onDestroy()
     * 
     * Perform cleanup here.  Note that we need to effectively reset the state of this service here
     * because there is no guarantee that the object will be destroyed before the app resumes (and
     * then assumes that the state being reported is accurate).
     */
    @Override
    public void onDestroy() {
        if (isInitialized()) {
            serializeSession();
            reinitializeService();
        }

        mCondition.open();
    }

    /**
     * Class for clients to access.  Because we know this service always
     * runs in the same process as its clients, we don't need to deal with
     * IPC.
     */
    public class LocalBinder extends Binder {
        public InformOnlineService getService() {
            return InformOnlineService.this;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (Collect.Log.DEBUG)
            Log.d(Collect.LOGTAG, t + "received start ID " + startId + ": " + intent);
        return START_STICKY;
    }

    // Triggered by UI button when the user wants to manually switch to OFFLINE mode
    public boolean goOffline() {
        if (checkout()) {
            if (Collect.Log.INFO)
                Log.i(Collect.LOGTAG, t + "went offline at users request");
            Collect.getInstance().getInformOnlineState().setOfflineModeEnabled(true);
            return true;
        } else {
            if (Collect.Log.WARN)
                Log.w(Collect.LOGTAG, t + "unable to go offline at users request");
            return false;
        }
    }

    // Triggered by UI button when the user wants to force online mode
    public boolean goOnline() {
        // Force online (but only if a connection attempt is not already underway)
        if (!mConnecting)
            connect(true);

        if (isSignedIn()) {
            if (Collect.Log.INFO)
                Log.i(Collect.LOGTAG, t + "went online at users request");
            Collect.getInstance().getInformOnlineState().setOfflineModeEnabled(false);
            return true;
        } else {
            if (Collect.Log.WARN)
                Log.w(Collect.LOGTAG, t + "unable to go online at users request");
            return false;
        }
    }

    // connect() has been run at least once
    public boolean isInitialized() {
        return mInitialized;
    }

    /*
     * Application is ready for regular operation & user interaction.
     * 
     * This does not mean that we were able to ping the service or 
     * that we are signed in.
     */
    public boolean isReady() {
        return isInitialized() && isRegistered();
    }

    // Registration info for this device is stored in the app's shared preferences
    public boolean isRegistered() {
        return Collect.getInstance().getInformOnlineState().hasRegistration();
    }

    // Is Inform Online available?
    public boolean isRespondingToPings() {
        return mServicePingSuccessful;
    }

    // Registered (implied) connected & signed in
    public boolean isSignedIn() {
        return mSignedIn;
    }

    // Bring the service back to the defaults it would have had when originally started
    public void reinitializeService() {
        if (Collect.Log.DEBUG)
            Log.d(Collect.LOGTAG, t + "service reinitialized");

        mInitialized = mServicePingSuccessful = mSignedIn = false;
    }

    /*
     * Sign in to the Inform Online service
     * Only called by connect()
     */
    private boolean checkin() {
        // Assume we are registered unless told otherwise
        boolean registered = true;

        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("deviceId", Collect.getInstance().getInformOnlineState().getDeviceId()));
        params.add(
                new BasicNameValuePair("deviceKey", Collect.getInstance().getInformOnlineState().getDeviceKey()));
        params.add(new BasicNameValuePair("fingerprint",
                Collect.getInstance().getInformOnlineState().getDeviceFingerprint()));

        try {
            params.add(new BasicNameValuePair("lastCheckinWith",
                    this.getPackageManager().getPackageInfo(this.getPackageName(), 0).versionName));
        } catch (NameNotFoundException e1) {
            params.add(new BasicNameValuePair("lastCheckinWith", "unknown"));
        }

        String checkinUrl = Collect.getInstance().getInformOnlineState().getServerUrl() + "/checkin";
        String postResult = HttpUtils.postUrlData(checkinUrl, params);
        JSONObject checkin;

        try {
            checkin = (JSONObject) new JSONTokener(postResult).nextValue();
            String result = checkin.optString(InformOnlineState.RESULT, InformOnlineState.FAILURE);

            if (result.equals(InformOnlineState.OK)) {
                if (Collect.Log.INFO)
                    Log.i(Collect.LOGTAG, t + "successful checkin");
                Collect.getInstance().getInformOnlineState().setExpired(false);

                // Update device role -- it might have changed
                Collect.getInstance().getInformOnlineState()
                        .setDeviceRole(checkin.optString("role", AccountDevice.ROLE_UNASSIGNED));
            } else if (result.equals(InformOnlineState.EXPIRED)) {
                if (Collect.Log.INFO)
                    Log.i(Collect.LOGTAG, t + "associated order is expired; marking device as expired");
                Collect.getInstance().getInformOnlineState().setExpired(true);
            } else if (result.equals(InformOnlineState.FAILURE)) {
                if (Collect.Log.WARN)
                    Log.w(Collect.LOGTAG, t + "checkin unsuccessful");
                registered = false;
            } else {
                // Something bad happened
                if (Collect.Log.ERROR)
                    Log.e(Collect.LOGTAG, t + "system error while processing postResult");
            }
        } catch (NullPointerException e) {
            // Communication error
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, t + "no postResult to parse.  Communication error with node.js server?");
            e.printStackTrace();
        } catch (JSONException e) {
            // Parse error (malformed result)
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, t + "failed to parse postResult " + postResult);
            e.printStackTrace();
        }

        // Clear the session for subsequent requests and reset stored state
        if (registered == false)
            Collect.getInstance().getInformOnlineState().resetDevice();

        return registered;
    }

    /*
     * Try and say "goodbye" to Inform Online so that we know that this client's session is no longer needed.
     * 
     * Checkouts are the result of a manual process and as such they will knock us
     * offline until the user manually puts us back into the online state.
     */
    private boolean checkout() {
        boolean saidGoodbye = false;

        String checkoutUrl = Collect.getInstance().getInformOnlineState().getServerUrl() + "/checkout";
        String getResult = HttpUtils.getUrlData(checkoutUrl);
        JSONObject checkout;

        try {
            checkout = (JSONObject) new JSONTokener(getResult).nextValue();

            String result = checkout.optString(InformOnlineState.RESULT, InformOnlineState.ERROR);

            if (result.equals(InformOnlineState.OK)) {
                if (Collect.Log.INFO)
                    Log.i(Collect.LOGTAG, t + "said goodbye to Group Complete");
            } else {
                if (Collect.Log.DEBUG)
                    Log.d(Collect.LOGTAG, t + "device checkout unnecessary");
            }

            saidGoodbye = true;
        } catch (NullPointerException e) {
            // Communication error
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, t + "no getResult to parse.  Communication error with node.js server?");
            e.printStackTrace();
        } catch (JSONException e) {
            // Parse error (malformed result)
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, t + "failed to parse getResult " + getResult);
            e.printStackTrace();
        } finally {
            // Running a checkout ALWAYS "signs us out"
            mSignedIn = false;
        }

        Collect.getInstance().getInformOnlineState().setSession(null);

        return saidGoodbye;
    }

    /*
     * Connect to the Inform Online service and if registered, attempt to sign in
     */
    private void connect(boolean forceOnline) {
        mConnecting = true;

        // Make sure that the user has not specifically requested that we be offline
        if (Collect.getInstance().getInformOnlineState().isOfflineModeEnabled() && forceOnline == false) {
            if (Collect.Log.INFO)
                Log.i(Collect.LOGTAG, t + "offline mode enabled; not auto-connecting");

            /* 
             * This is not a complete initialization (in the sense that we attempted connection) but we need 
             * to pretend that it is so that the UI can move forward to whatever state is most suitable.
             */
            mInitialized = true;
            mConnecting = false;

            return;
        }

        if (Collect.Log.INFO)
            Log.i(Collect.LOGTAG, t + "pinging " + Collect.getInstance().getInformOnlineState().getServerUrl());

        // Try to ping the service to see if it is "up" (and determine whether we are registered)
        String pingUrl = Collect.getInstance().getInformOnlineState().getServerUrl() + "/ping";
        String getResult = HttpUtils.getUrlData(pingUrl);
        JSONObject ping;

        try {
            ping = (JSONObject) new JSONTokener(getResult).nextValue();

            String result = ping.optString(InformOnlineState.RESULT, InformOnlineState.ERROR);

            // Online and registered (checked in)
            if (result.equals(InformOnlineState.OK)) {
                if (Collect.Log.INFO)
                    Log.i(Collect.LOGTAG, t + "ping successful (we are connected and checked in)");
                mServicePingSuccessful = mSignedIn = true;
            } else if (result.equals(InformOnlineState.FAILURE)) {
                if (Collect.Log.WARN)
                    Log.w(Collect.LOGTAG, t + "ping successful but not signed in (will attempt checkin)");
                mServicePingSuccessful = true;

                if (Collect.getInstance().getInformOnlineState().hasRegistration() && checkin()) {
                    if (Collect.Log.INFO)
                        Log.i(Collect.LOGTAG, t + "checkin successful (we are connected)");

                    // Fetch regardless of the fact that we're not yet marked as being signed in
                    AccountDeviceList.fetchDeviceList(true);
                    AccountFolderList.fetchFolderList(true);

                    mSignedIn = true;
                } else {
                    if (Collect.Log.WARN)
                        Log.w(Collect.LOGTAG, t + "checkin failed (registration invalid)");
                    mSignedIn = false;
                }
            } else {
                // Assume offline
                if (Collect.Log.WARN)
                    Log.w(Collect.LOGTAG, t + "ping failed (we are offline)");
                mSignedIn = false;
            }
        } catch (NullPointerException e) {
            // This usually indicates a communication error and will send us into an offline state
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, t + "ping error while communicating with service (we are offline)");
            e.printStackTrace();
            mServicePingSuccessful = mSignedIn = false;
        } catch (JSONException e) {
            // Parse errors (malformed result) send us into an offline state
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, t + "ping error while parsing getResult " + getResult + " (we are offline)");
            e.printStackTrace();
            mServicePingSuccessful = mSignedIn = false;
        } finally {
            // Load regardless of whether we are signed in
            AccountDeviceList.loadDeviceList();
            AccountFolderList.loadFolderList();

            // Unblock
            mInitialized = true;
            mConnecting = false;
        }
    }

    /*
     * Restore a serialized session from disk
     */
    private void restoreSession() {
        // Restore any serialized session information
        File sessionCache = new File(getCacheDir(), FileUtilsExtended.SESSION_CACHE_FILE);

        if (sessionCache.exists()) {
            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, t + "restoring cached session");

            try {
                InformOnlineSession session = new InformOnlineSession();

                FileInputStream fis = new FileInputStream(sessionCache);
                ObjectInputStream ois = new ObjectInputStream(fis);
                session = (InformOnlineSession) ois.readObject();
                ois.close();
                fis.close();

                Collect.getInstance().getInformOnlineState().setSession(new BasicCookieStore());
                Iterator<InformOnlineSession> cookies = session.getCookies().iterator();

                if (cookies.hasNext()) {
                    InformOnlineSession ios = cookies.next();

                    BasicClientCookie bcc = new BasicClientCookie(ios.getName(), ios.getValue());
                    bcc.setDomain(ios.getDomain());
                    bcc.setExpiryDate(ios.getExpiryDate());
                    bcc.setPath(ios.getPath());
                    bcc.setVersion(ios.getVersion());

                    Collect.getInstance().getInformOnlineState().getSession().addCookie(bcc);
                }
            } catch (Exception e) {
                if (Collect.Log.WARN)
                    Log.w(Collect.LOGTAG, t + "problem restoring cached session " + e.toString());
                e.printStackTrace();

                // Don't leave a broken file hanging
                new File(getCacheDir(), FileUtilsExtended.SESSION_CACHE_FILE).delete();

                // Clear the session
                Collect.getInstance().getInformOnlineState().setSession(null);
            }
        } else {
            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, t + "no session to restore");
        }
    }

    /*
     * Serialize the current session to disk
     */
    private void serializeSession() {
        // Attempt to serialize the session for later use
        if (Collect.getInstance().getInformOnlineState().getSession() instanceof CookieStore) {
            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, t + "serializing session");

            try {
                InformOnlineSession session = new InformOnlineSession();

                Iterator<Cookie> cookies = Collect.getInstance().getInformOnlineState().getSession().getCookies()
                        .iterator();

                while (cookies.hasNext()) {
                    Cookie c = cookies.next();
                    session.getCookies().add(new InformOnlineSession(c.getDomain(), c.getExpiryDate(), c.getName(),
                            c.getPath(), c.getValue(), c.getVersion()));
                }

                FileOutputStream fos = new FileOutputStream(
                        new File(getCacheDir(), FileUtilsExtended.SESSION_CACHE_FILE));
                ObjectOutputStream out = new ObjectOutputStream(fos);
                out.writeObject(session);
                out.close();
                fos.close();
            } catch (Exception e) {
                if (Collect.Log.WARN)
                    Log.w(Collect.LOGTAG, t + "problem serializing session " + e.toString());
                e.printStackTrace();

                // Make sure that we don't leave a broken file hanging
                new File(getCacheDir(), FileUtilsExtended.SESSION_CACHE_FILE).delete();
            }
        } else {
            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, t + "no session to serialize");
        }
    }
}