ca.mcgill.hs.serv.LogFileUploaderService.java Source code

Java tutorial

Introduction

Here is the source code for ca.mcgill.hs.serv.LogFileUploaderService.java

Source

/* 
 * Copyright (c) 2010 Jordan Frank, HumanSense Project, McGill University
 * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
 * See LICENSE for more information 
 */
package ca.mcgill.hs.serv;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Timer;
import java.util.TimerTask;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.ContentBody;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.util.EntityUtils;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.IBinder;
import android.widget.Toast;
import ca.mcgill.hs.HSAndroid;
import falldetection.analysis.fall.R;
import ca.mcgill.hs.prefs.HSAndroidPreferences;
import ca.mcgill.hs.prefs.PreferenceFactory;
import ca.mcgill.hs.util.Log;

/**
 * A service for uploading data files to a web server. When this service starts,
 * it retrieves the list of saved files. Then, if an option is set, it will
 * attempt to connect 3 times to a wifi connection. Afterwards, it will upload
 * the files, recording error codes. Finally, it will toast the user to alert
 * them of the success or failure of the operation.
 * 
 * If the service was already started, then it will simply update the list of
 * files and resume its operations.
 * 
 * TODO: This needs to be rewritten.
 * 
 * @author Jordan Frank, Cicerone Cojocaru, Jonathan Pitre
 */
public class LogFileUploaderService extends Service {
    private static final String TAG = "HSUploaderService";

    // Intent for when the auto-upload option is changed.
    public static final String AUTO_UPLOAD_CHANGED_INTENT = "ca.mcgill.hs.HSAndroidApp.AUTO_UPLOAD_CHANGED_INTENT";

    // Intent for when the wifi-only option is changed.
    public static final String WIFI_ONLY_CHANGED_INTENT = "ca.mcgill.hs.HSAndroidApp.WIFI_ONLY_CHANGED_INTENT";

    // Intent for when a new file is ready to be uploaded.
    public static final String FILE_ADDED_INTENT = "ca.mcgill.hs.HSAndroidApp.FILE_ADDED_INTENT";

    /**
     * URL for uploading data files.
     */
    public static final String UPLOAD_URL = "http://www.cs.mcgill.ca/~jfrank8/humansense/uploader.php";

    private boolean connectionReceiverRegistered = false;
    private boolean waiting = false;
    private Timer timer = new Timer();
    private final long DELAY = 30000;

    private final BroadcastReceiver connectReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, final Intent intent) {
            if (((NetworkInfo) intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO)).isConnected()) {
                networkChanged();
            }
        }
    };

    // BroadcastReceiver for Use Wifi only preference change
    private final BroadcastReceiver wifiOnlyPrefChanged = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, final Intent intent) {
            wifiPrefChanged();
        }
    };

    // Broadcast receiver for automatic upload preference changed
    private final BroadcastReceiver autoPrefChanged = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, final Intent intent) {
            autoPrefChanged();
        }
    };

    // Keep track of whether the service has been started or not.
    private static boolean started = false;

    /* LIST OF FILES */
    private static final LinkedList<String> fileList = new LinkedList<String>();
    private static final HashMap<String, Object> fileMap = new HashMap<String, Object>();

    /* ERROR CODES */
    public static final int NO_ERROR_CODE = 0x0;
    public static final int MALFORMEDURLEXCEPTION_ERROR_CODE = 0x1;
    public static final int UNKNOWNHOSTEXCEPTION_ERROR_CODE = 0x2;
    public static final int IOEXCEPTION_ERROR_CODE = 0x3;
    public static final int UPLOAD_FAILED_ERROR_CODE = 0x4;
    public static final int NO_CONNECTION_ERROR = 0x5;
    public static final int UPLOAD_FAILED_FILE_NOT_FOUND_ERROR_CODE = 0x6;
    public static final int ILLEGALSTATEEXCEPTION_ERROR_CODE = 0x7;
    private int CURRENT_ERROR_CODE;

    /* WIFI MANAGER */
    private WifiManager wifiMgr;
    private WifiInfo wifiInfo;

    // Count of files uploaded
    private int filesUploaded;

    /* CONNECTION VARIABLES */
    private HttpClient httpclient;
    private HttpPost httppost;
    private boolean wifiOnly;
    private boolean automatic;
    private ConnectivityManager connectivityMgr;

    /* NOTIFICATION VARIABLES */
    private static final String NOTIFICATION_STRING = Context.NOTIFICATION_SERVICE;
    public static final int NOTIFICATION_ID = NOTIFICATION_STRING.hashCode();
    private NotificationManager notificationMgr;

    /* UPLOAD COMPLETION INTENTS */
    public static final String UPLOAD_COMPLETE_INTENT = "ca.mcgill.hs.HSAndroidApp.UPLOAD_COMPLETE_INTENT";
    private static Intent shutdownIntent;

    private final BroadcastReceiver completionReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, final Intent intent) {
            if (intent.getAction().equals(UPLOAD_COMPLETE_INTENT)) {
                stopService(shutdownIntent);
            }
        }
    };

    /* FILE VARIABLES */
    private String UNUPLOADED_PATH;

    private static SharedPreferences prefs;

    /**
     * Called when the auto upload preference has changed.
     */
    private void autoPrefChanged() {
        automatic = prefs.getBoolean(HSAndroidPreferences.AUTO_UPLOAD_DATA_PREF, false);

        /*
         * If auto uploading has just been turned off and we were waiting, kill
         * the timer and stop the service.
         */
        if (!automatic) {
            if (waiting) {
                timerKill();
                stopService(shutdownIntent);
            }
        }
    }

    /**
     * Determines whether or not the phone is able to upload data over the
     * internet.
     * 
     * @return True if it is possible to upload, false otherwise.
     */
    private boolean canUpload() {
        if (connectivityMgr == null || connectivityMgr.getActiveNetworkInfo() == null) {
            return false;
        }
        if (!wifiOnly) {
            if (connectivityMgr.getActiveNetworkInfo().getState() != NetworkInfo.State.CONNECTED) {
                return false;
            }
        } else {
            if (connectivityMgr.getActiveNetworkInfo().getState() != NetworkInfo.State.CONNECTED
                    || connectivityMgr.getActiveNetworkInfo().getType() != ConnectivityManager.TYPE_WIFI) {
                return false;
            }
        }
        return true;
    }

    /**
     * Helper method for making toasts.
     * 
     * @param message
     *            The text to toast.
     * @param duration
     *            The duration of the toast.
     */
    private void makeToast(final String message, final int duration) {
        final Toast slice = Toast.makeText(getBaseContext(),
                getResources().getString(R.string.uploader_appname_label) + message, duration);
        slice.setGravity(slice.getGravity(), slice.getXOffset(), slice.getYOffset() + 100);
        slice.show();
    }

    /**
     * Called when there has been a change in network connectivity.
     */
    private void networkChanged() {
        unregisterConnectReceiver();
        uploadFiles();
    }

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

    @Override
    public void onCreate() {
        super.onCreate();
        prefs = PreferenceFactory.getSharedPreferences(this);
        CURRENT_ERROR_CODE = NO_ERROR_CODE;
        shutdownIntent = new Intent(this, LogFileUploaderService.class);
        UNUPLOADED_PATH = (String) getBaseContext().getResources().getText(R.string.recent_file_path);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        waiting = false;
        started = false;
        timer.cancel();
        unregisterConnectReceiver();
        unregisterReceiver(wifiOnlyPrefChanged);
        unregisterReceiver(autoPrefChanged);
        onUploadComplete();
    }

    @Override
    public synchronized void onStart(final Intent intent, final int startId) {
        // Update the file list
        updateFileList();

        // If there are no files, return.
        if (fileList.size() == 0 && !started) {
            makeToast(getResources().getString(R.string.uploader_no_new_files), Toast.LENGTH_SHORT);
            return;
        }

        // If it was already started, return. Else, continue.
        if (started) {
            return;
        }

        // At this point we consider the service to be started.
        started = true;

        registerReceiver(wifiOnlyPrefChanged, new IntentFilter(WIFI_ONLY_CHANGED_INTENT));
        registerReceiver(autoPrefChanged, new IntentFilter(AUTO_UPLOAD_CHANGED_INTENT));

        wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
        wifiInfo = wifiMgr.getConnectionInfo();

        // Register completion receiver
        registerReceiver(completionReceiver, new IntentFilter(UPLOAD_COMPLETE_INTENT));

        // Connect to a network
        setUpConnection();

        notificationMgr = (NotificationManager) getSystemService(NOTIFICATION_STRING);
        notificationMgr.cancel(NOTIFICATION_ID);

        final int icon = R.drawable.notification_icon;
        final String tickerText = getResources().getString(R.string.notification_ticker);
        final String contentTitle = getResources().getString(R.string.notification_upload_title);
        final String contentText = getResources().getString(R.string.notification_upload_text);

        final Notification n = new Notification(icon, tickerText, System.currentTimeMillis());

        final Intent i = new Intent(this, HSService.class);
        n.setLatestEventInfo(this, contentTitle, contentText,
                PendingIntent.getActivity(this.getBaseContext(), 0, i, PendingIntent.FLAG_CANCEL_CURRENT));

        notificationMgr.notify(NOTIFICATION_ID, n);

        filesUploaded = 0;
        uploadFiles();
    }

    /**
     * Called whenever the file upload is complete. Notifies user if upload was
     * manual.
     */
    private void onUploadComplete() {
        notificationMgr.cancel(NOTIFICATION_ID);
        // If we are on automatic uploading, do not notify the user.
        if (!automatic) {
            switch (CURRENT_ERROR_CODE) {
            case NO_ERROR_CODE:
                if (filesUploaded == 1) {
                    makeToast(getResources().getString(R.string.uploader_no_errors_one_file), Toast.LENGTH_SHORT);
                } else {
                    makeToast(
                            filesUploaded + " "
                                    + getResources().getString(R.string.uploader_no_errors_multiple_files),
                            Toast.LENGTH_SHORT);
                }
                break;
            case UNKNOWNHOSTEXCEPTION_ERROR_CODE:
                makeToast(getResources().getString(R.string.uploader_unable_to_connect), Toast.LENGTH_SHORT);
                break;
            case UPLOAD_FAILED_ERROR_CODE:
                makeToast(getResources().getString(R.string.uploader_upload_failed), Toast.LENGTH_SHORT);
                break;
            case IOEXCEPTION_ERROR_CODE:
                makeToast(getResources().getString(R.string.uploader_upload_failed), Toast.LENGTH_SHORT);
                break;
            case NO_CONNECTION_ERROR:
                makeToast(getResources().getString(R.string.uploader_no_connection), Toast.LENGTH_SHORT);
                break;
            case ILLEGALSTATEEXCEPTION_ERROR_CODE:
                makeToast(getResources().getString(R.string.uploader_upload_failed), Toast.LENGTH_SHORT);
                break;
            default:
                break;
            }
        }
        unregisterReceiver(completionReceiver);
    }

    /**
     * Initializes preferences and the ConnectivityManager.
     */
    private void setUpConnection() {
        wifiOnly = prefs.getBoolean(HSAndroidPreferences.UPLOAD_OVER_WIFI_ONLY_PREF, false);
        automatic = prefs.getBoolean(HSAndroidPreferences.AUTO_UPLOAD_DATA_PREF, false);

        connectivityMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    }

    /**
     * Cancels the timer.
     */
    private void timerKill() {
        timer.cancel();
        timer = new Timer();
    }

    /**
     * Starts the timer for reattempting file upload.
     */
    private void timerStart() {
        final TimerTask task = new TimerTask() {
            @Override
            public void run() {
                uploadFiles();
            }
        };
        timer.schedule(task, DELAY);
    }

    /**
     * Safely unregisters the ConnectReceiver.
     */
    private void unregisterConnectReceiver() {
        if (connectionReceiverRegistered) {
            connectionReceiverRegistered = false;
            try {
                unregisterReceiver(connectReceiver);
            } catch (final IllegalArgumentException e) {
                Log.e(TAG, e.getMessage());
            }
        }
    }

    /**
     * Goes through the recent folder and adds all the files to the list of
     * files to upload.
     */
    private synchronized void updateFileList() {

        final File path = new File(HSAndroid.getStorageDirectory(), UNUPLOADED_PATH);

        if (!path.isDirectory()) {
            if (!path.mkdirs()) {
                Log.e("Output Dir", "Could not create output directory!");
                return;
            }
        }

        final String[] files = path.list();

        if (files == null || files.length == 0) {
            return;
        } else {
            /*
             * Add all files to a map so that we can keep track of which files
             * were supposed to upload as the list is cleared.
             */
            for (final String s : files) {
                if (!fileMap.containsKey(s)) {
                    fileMap.put(s, null);
                    fileList.add(UNUPLOADED_PATH + s);
                }
            }
        }
    }

    /**
     * Uploads the specified file to the server.
     * 
     * @param fileName
     *            The file to upload
     * @return A code indicating the result of the upload.
     */
    private int uploadFile(final String fileName) {
        Log.d(TAG, "Uploading " + fileName);
        httpclient = new DefaultHttpClient();
        httpclient.getParams().setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_0);

        httppost = new HttpPost(UPLOAD_URL);
        final File file = new File(HSAndroid.getStorageDirectory(), fileName);
        if (!file.exists()) {
            // File may be deleted while in the queue for uploading
            Log.d(TAG, "Unable to upload " + fileName + ". File does not exist.");
            return UPLOAD_FAILED_FILE_NOT_FOUND_ERROR_CODE;
        }
        Log.d(TAG, "MAC ADDRESS: " + wifiInfo.getMacAddress());
        httppost.addHeader("MAC", wifiInfo.getMacAddress());

        final MultipartEntity mpEntity = new MultipartEntity();
        final ContentBody cbFile = new FileBody(file, "binary/octet-stream");
        mpEntity.addPart("uploadedfile", cbFile);

        httppost.setEntity(mpEntity);
        try {
            final HttpResponse response = httpclient.execute(httppost);
            final HttpEntity resEntity = response.getEntity();

            String responseMsg = "";
            if (resEntity != null) {
                responseMsg = EntityUtils.toString(resEntity);
                resEntity.consumeContent();
            }
            Log.i(TAG, "Server Response: " + responseMsg);
            if (responseMsg.contains("FAILURE")) {
                CURRENT_ERROR_CODE = UPLOAD_FAILED_ERROR_CODE;
                return UPLOAD_FAILED_ERROR_CODE;
            }
            // Move files to uploaded folder if successful
            else {
                Log.i(TAG, "Moving file to uploaded directory.");
                final File dest = new File(HSAndroid.getStorageDirectory(),
                        HSAndroid.getAppString(R.string.uploaded_file_path));
                if (!dest.isDirectory()) {
                    if (!dest.mkdirs()) {
                        throw new IOException("ERROR: Unable to create directory " + dest.getName());
                    }
                }

                if (!file.renameTo(new File(dest, file.getName()))) {
                    throw new IOException("ERROR: Unable to transfer file " + file.getName());
                }
            }

        } catch (final MalformedURLException e) {
            Log.e(TAG, android.util.Log.getStackTraceString(e));
            CURRENT_ERROR_CODE = MALFORMEDURLEXCEPTION_ERROR_CODE;
            return MALFORMEDURLEXCEPTION_ERROR_CODE;
        } catch (final UnknownHostException e) {
            Log.e(TAG, android.util.Log.getStackTraceString(e));
            CURRENT_ERROR_CODE = UNKNOWNHOSTEXCEPTION_ERROR_CODE;
            return UNKNOWNHOSTEXCEPTION_ERROR_CODE;
        } catch (final IOException e) {
            Log.e(TAG, android.util.Log.getStackTraceString(e));
            CURRENT_ERROR_CODE = IOEXCEPTION_ERROR_CODE;
            return IOEXCEPTION_ERROR_CODE;
        } catch (final IllegalStateException e) {
            Log.e(TAG, android.util.Log.getStackTraceString(e));
            CURRENT_ERROR_CODE = ILLEGALSTATEEXCEPTION_ERROR_CODE;
            return ILLEGALSTATEEXCEPTION_ERROR_CODE;
        }
        return NO_ERROR_CODE;
    }

    /**
     * Runs a new thread that uploads all files in the fileList.
     */
    private void uploadFiles() {
        new Thread() {
            @Override
            public void run() {
                updateFileList();
                String ff = null;
                while (!fileList.isEmpty()) {
                    if (canUpload()) {
                        final String f = fileList.remove();
                        final int retCode = uploadFile(f);
                        if (retCode != NO_ERROR_CODE) {
                            // If the file is missing, or there is some problem
                            // reading the file, then the file is skipped.
                            // Otherwise it is put back into the queue.
                            if (retCode != UPLOAD_FAILED_FILE_NOT_FOUND_ERROR_CODE
                                    && retCode != IOEXCEPTION_ERROR_CODE) {
                                fileList.addLast(f);
                                if (ff == null) {
                                    ff = f;
                                } else {
                                    // If we have gone through the whole list
                                    // and all files are giving errors, if on
                                    // automatic set the timer to try again,
                                    // else break and alert user that there were
                                    // errors.
                                    if (ff.equals(f)) {
                                        if (automatic) {
                                            waiting = true;
                                            // We are waiting to try the upload
                                            // for this file again after a set
                                            // time.
                                            timerStart();
                                            return;
                                        } else {
                                            break;
                                        }
                                    }
                                }
                            }
                        } else {
                            // If there was no error code, the file was
                            // successfully uploaded.
                            filesUploaded++;
                        }
                    } else {
                        if (automatic) {
                            waiting = true;
                            // We now wait for a connection to become available,
                            // so register a receiver for it.
                            registerReceiver(connectReceiver,
                                    new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
                            connectionReceiverRegistered = true;
                            return;
                        } else {
                            CURRENT_ERROR_CODE = NO_CONNECTION_ERROR;
                            break;
                        }
                    }
                }
                fileMap.clear();
                fileList.clear();
                if (httpclient != null) {
                    httpclient.getConnectionManager().shutdown();
                }
                final Intent i = new Intent();
                i.setAction(UPLOAD_COMPLETE_INTENT);
                sendBroadcast(i);
            }
        }.start();
    }

    /**
     * Called when the wifi only preference is changed.
     */
    private void wifiPrefChanged() {

        wifiOnly = prefs.getBoolean(HSAndroidPreferences.UPLOAD_OVER_WIFI_ONLY_PREF, false);
        // If we are on automatic uploads, wifi has been disabled, and we are
        // waiting for a connection to become available, then it's possible that
        // a 3G connection has been available the whole time so we try again.
        if (automatic && wifiOnly == false && waiting) {
            networkChanged();
        }
    }

}