com.sferadev.etic.tasks.DownloadTasksTask.java Source code

Java tutorial

Introduction

Here is the source code for com.sferadev.etic.tasks.DownloadTasksTask.java

Source

/*
 * Copyright (C) 2011 Cloudtec Pty Ltd
 * 
 * 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.sferadev.etic.tasks;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;

import com.sferadev.etic.R;
import com.sferadev.etic.listeners.TaskDownloaderListener;
import com.sferadev.etic.taskModel.TaskResponse;
import com.sferadev.etic.utilities.ManageForm;
import com.sferadev.etic.utilities.ManageFormResponse;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
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.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.odk.collect.android.application.Collect;
import org.odk.collect.android.database.Assignment;
import org.odk.collect.android.database.FileDbAdapter;
import org.odk.collect.android.database.TaskAssignment;
import org.odk.collect.android.preferences.PreferencesActivity;
import org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;

import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.util.Log;

/**
 * Background task for downloading tasks 
 * 
 * @author Neil Penman (neilpenman@gmail.com)
 */
public class DownloadTasksTask extends AsyncTask<Void, String, HashMap<String, String>> {

    private TaskDownloaderListener mStateListener;
    HashMap<String, String> results = null;
    FileDbAdapter fda = new FileDbAdapter();
    Cursor taskListCursor = null;

    // class used to store status of existing tasks in the database and their database id
    private class TaskStatus {
        public long tid;
        public String status;
        public boolean keep;

        public TaskStatus(long tid, String status) {
            this.tid = tid;
            this.status = status;
            keep = false;
        }
    }

    /*
     * Clean up after cancel
     */
    @Override
    protected void onCancelled() {
        if (fda != null) {
            fda.close();
        }
        if (taskListCursor != null) {
            taskListCursor.close();
        }
    }

    @Override
    protected HashMap<String, String> doInBackground(Void... values) {

        results = new HashMap<String, String>();

        fda.open();

        /*
         * Always remove local tasks that are no longer current
         */
        try {
            fda.deleteTasksFromSource("local", FileDbAdapter.STATUS_T_REJECTED);
            fda.deleteTasksFromSource("local", FileDbAdapter.STATUS_T_SUBMITTED);

        } catch (Exception e) {
            publishProgress("Database Error - Failed to remove local tasks");
            e.printStackTrace();
        } finally {
            fda.close();
        }

        // Check that the user has enabled task synchronisation
        SharedPreferences settings = PreferenceManager
                .getDefaultSharedPreferences(Collect.getInstance().getBaseContext());
        boolean tasksEnabled = settings.getBoolean("enable_tasks", false);
        Log.i("diag", "Tasks are enabled?" + tasksEnabled);
        synchronise(tasksEnabled);

        return results;
    }

    @Override
    protected void onPostExecute(HashMap<String, String> value) {
        synchronized (this) {
            if (mStateListener != null) {
                mStateListener.taskDownloadingComplete(value);
            }
        }
    }

    @Override
    protected void onProgressUpdate(String... values) {
        synchronized (this) {
            if (mStateListener != null && values.length > 0) {
                mStateListener.progressUpdate(values[0]);
            }
        }

    }

    public void setDownloaderListener(TaskDownloaderListener sl, Context context) {
        synchronized (this) {
            mStateListener = sl;
        }
    }

    /*
     * Synchronise the tasks stored on the phone with those on the server
     *  There is an implicit assumption in some of the code that there can be multiple task management
     *  servers (or sources).  However the current implementation takes some shortcuts and assumes
     *  that there is a single remote source that is identified by the URL of the server that host the 
     *  surveys.
     *  
    *  All database updates are within the scope of a transaction which is rolled back on an exception        
     */
    private void synchronise(boolean tasksEnabled) {
        Log.i("diag", "Synchronise()");
        int count = 0;

        String taskURL = null;

        fda.open();
        fda.beginTransaction(); // Start Transaction

        // Get the source
        SharedPreferences settings = PreferenceManager
                .getDefaultSharedPreferences(Collect.getInstance().getBaseContext());
        String serverUrl = Collect.getInstance().getString(R.string.default_server_url);
        String source = null;
        // Remove the protocol
        if (serverUrl.startsWith("http")) {
            int idx = serverUrl.indexOf("//");
            if (idx > 0) {
                source = serverUrl.substring(idx + 2);
            } else {
                source = serverUrl;
            }
        }
        String username = settings.getString(PreferencesActivity.KEY_USERNAME, null);
        String password = settings.getString(PreferencesActivity.KEY_PASSWORD, null);
        Log.i("diag", "Source:" + source);

        if (source != null) {
            try {

                /*
                 * Delete all entries in the database that are "Missed" or "Cancelled
                 * These would have had their status set by the server the last time the user synchronised.  
                 * The user has seen their new status so time to remove.
                 */
                cleanupTasks(fda, source);

                /*
                 * If tasks are enabled
                 * Get tasks for this source from the database
                 * Add to a hashmap indexed on the source's task id
                 */
                if (isCancelled()) {
                    throw new CancelException("cancelled");
                }
                ; // Return if the user cancels

                HttpResponse getResponse = null;
                DefaultHttpClient client = null;
                Gson gson = null;
                TaskResponse tr = null;
                int statusCode;
                if (tasksEnabled) {
                    HashMap<String, TaskStatus> taskMap = new HashMap<String, TaskStatus>();
                    taskListCursor = fda.fetchTasksForSource(source, false);
                    taskListCursor.moveToFirst();
                    while (!taskListCursor.isAfterLast()) {

                        if (isCancelled()) {
                            throw new CancelException("cancelled");
                        }
                        ; // Return if the user cancels

                        String status = taskListCursor
                                .getString(taskListCursor.getColumnIndex(FileDbAdapter.KEY_T_STATUS));
                        String aid = taskListCursor
                                .getString(taskListCursor.getColumnIndex(FileDbAdapter.KEY_T_ASSIGNMENTID));
                        long tid = taskListCursor.getLong(taskListCursor.getColumnIndex(FileDbAdapter.KEY_T_ID));
                        TaskStatus t = new TaskStatus(tid, status);
                        taskMap.put(aid, t);
                        Log.i(getClass().getSimpleName(), "Current task:" + aid + " status:" + status);
                        taskListCursor.moveToNext();
                    }
                    taskListCursor.close();

                    // Get the tasks for this source from the server
                    client = new DefaultHttpClient();

                    // Add credentials
                    if (username != null && password != null) {
                        client.getCredentialsProvider().setCredentials(new AuthScope(null, -1, null),
                                new UsernamePasswordCredentials(username, password));
                    }

                    if (isCancelled()) {
                        throw new CancelException("cancelled");
                    }
                    ; // Return if the user cancels

                    // Call the service
                    taskURL = serverUrl + "/surveyKPI/myassignments";
                    InputStream is = null;
                    HttpGet getRequest = new HttpGet(taskURL);
                    getResponse = client.execute(getRequest);
                    statusCode = getResponse.getStatusLine().getStatusCode();
                    if (statusCode != HttpStatus.SC_OK) {
                        Log.w(getClass().getSimpleName(), "Error:" + statusCode + " for URL " + taskURL);
                        throw new Exception("Error connecting - check username and password");
                    } else {
                        HttpEntity getResponseEntity = getResponse.getEntity();
                        is = getResponseEntity.getContent();
                    }

                    // De-serialise
                    gson = new GsonBuilder().setDateFormat("dd/MM/yyyy hh:mm").create();
                    Reader isReader = new InputStreamReader(is);
                    tr = gson.fromJson(isReader, TaskResponse.class);
                    Log.i(getClass().getSimpleName(), "Message:" + tr.message);

                    /*
                     * Loop through the entries from the source
                     *   (1) Add entries that have a status of "new", "pending" or "accepted" and are not already on the phone
                     *   (2) Update the status of database entries where the source status is set to "Missed" or "Cancelled"
                     */
                    count += addAndUpdateEntries(tr, fda, taskMap, username, source);
                }

                /*
                 * Loop through the entries in the database
                 *  (1) Update on the server all that have a status of "accepted", "rejected" or "submitted" or "cancelled" or "completed"
                 *      Note in the case of "cancelled" the client is merely acknowledging that it received the cancellation notice
                 */
                if (isCancelled()) {
                    throw new CancelException("cancelled");
                }
                ; // Return if the user cancels

                if (tasksEnabled) {
                    updateTaskStatusToServer(fda, source, username, password, serverUrl);
                }

                /*
                 * Delete all orphaned tasks (The instance has been deleted)
                 */
                taskListCursor = fda.fetchAllTasks();
                taskListCursor.moveToFirst();
                while (!taskListCursor.isAfterLast()) {

                    if (isCancelled()) {
                        throw new CancelException("cancelled");
                    }
                    ; // Return if the user cancels

                    String instancePath = taskListCursor
                            .getString(taskListCursor.getColumnIndex(FileDbAdapter.KEY_T_INSTANCE));
                    long tid = taskListCursor.getLong(taskListCursor.getColumnIndex(FileDbAdapter.KEY_T_ID));
                    Log.i(getClass().getSimpleName(), "Instance:" + instancePath);

                    // Delete the task if the instance has been deleted
                    if (!instanceExists(instancePath)) {
                        fda.deleteTask(tid);
                    }

                    taskListCursor.moveToNext();
                }
                taskListCursor.close();

                /*
                 * Delete all entries in the database that are "Submitted" or "Rejected"
                 * The user set these status values, no need to keep the tasks
                 */
                fda.deleteTasksFromSource(source, FileDbAdapter.STATUS_T_REJECTED);
                fda.deleteTasksFromSource(source, FileDbAdapter.STATUS_T_SUBMITTED);

                // Commit the transation
                fda.setTransactionSuccessful(); // Commit the transaction

            } catch (JsonSyntaxException e) {

                Log.e(getClass().getSimpleName(), "JSON Syntax Error:" + " for URL " + taskURL);
                publishProgress(e.getMessage());
                e.printStackTrace();
                results.put("Error:", e.getMessage());

            } catch (CancelException e) {

                Log.i(getClass().getSimpleName(), "Info: Download cancelled by user.");

            } catch (Exception e) {

                Log.e(getClass().getSimpleName(), "Error:" + " for URL " + taskURL);
                e.printStackTrace();
                publishProgress(e.getMessage());
                results.put("Error:", e.getMessage());

            } finally {
                if (fda != null) {
                    fda.endTransaction();
                    fda.close();
                }
                if (taskListCursor != null) {
                    taskListCursor.close();
                    taskListCursor = null;
                }
            }
        }

        if (count == 0) {
            results.put("err_no_tasks", "");
        }
    }

    /*
    * Delete all entries in the database that are "Missed" or "Cancelled
    * These would have had their status set by the server the last time the user synchronised.  
    * The user has seen their new status so time to remove.
    */
    private void cleanupTasks(FileDbAdapter fda, String source) throws Exception {

        Cursor taskListCursor = fda.fetchTasksForSource(source, false);
        taskListCursor.moveToFirst();
        while (!taskListCursor.isAfterLast()) {

            if (isCancelled()) {
                return;
            }
            ; // Return if the user cancels

            String status = taskListCursor.getString(taskListCursor.getColumnIndex(FileDbAdapter.KEY_T_STATUS));
            long id = taskListCursor.getLong(taskListCursor.getColumnIndex(FileDbAdapter.KEY_T_ID));
            Log.i("cleanupTasks", "taskid:" + id + " -- status:" + status);
            if (status.equals(FileDbAdapter.STATUS_T_MISSED) || status.equals(FileDbAdapter.STATUS_T_CANCELLED)) {
                fda.deleteTask(id);
            }
            taskListCursor.moveToNext();
        }
        taskListCursor.close();

    }

    /*
    * Loop through the entries in the database
    *  (1) Update on the server all that have a status of "accepted", "rejected" or "submitted"
    */
    private void updateTaskStatusToServer(FileDbAdapter fda, String source, String username, String password,
            String serverUrl) throws Exception {

        Log.i("updateTaskStatusToServer", "Enter");
        Cursor taskListCursor = fda.fetchTasksForSource(source, false);
        taskListCursor.moveToFirst();
        DefaultHttpClient client = new DefaultHttpClient();
        HttpResponse getResponse = null;

        // Add credentials
        if (username != null && password != null) {
            client.getCredentialsProvider().setCredentials(new AuthScope(null, -1, null),
                    new UsernamePasswordCredentials(username, password));
        }

        while (!taskListCursor.isAfterLast()) {

            if (isCancelled()) {
                return;
            }
            ; // Return if the user cancels

            String newStatus = taskListCursor.getString(taskListCursor.getColumnIndex(FileDbAdapter.KEY_T_STATUS));
            String syncStatus = taskListCursor
                    .getString(taskListCursor.getColumnIndex(FileDbAdapter.KEY_T_IS_SYNC));
            long aid = taskListCursor.getLong(taskListCursor.getColumnIndex(FileDbAdapter.KEY_T_ASSIGNMENTID));
            long tid = taskListCursor.getLong(taskListCursor.getColumnIndex(FileDbAdapter.KEY_T_ID));

            Log.i("updateTaskStatusToServer",
                    "aId:" + aid + " -- status:" + newStatus + " -- syncStatus:" + syncStatus);
            // Call the update service
            if (newStatus != null && syncStatus.equals(FileDbAdapter.STATUS_SYNC_NO)) {
                Log.i(getClass().getSimpleName(), "Updating server with status of " + aid + " to " + newStatus);
                Assignment a = new Assignment();
                a.assignment_id = (int) aid;
                a.assignment_status = newStatus;

                // Call the service
                String taskURL = serverUrl + "/surveyKPI/myassignments/" + aid;
                HttpPost postRequest = new HttpPost(taskURL);

                ArrayList<NameValuePair> postParameters = new ArrayList<NameValuePair>();
                postParameters.add(new BasicNameValuePair("assignInput", "{assignment_status: " + newStatus + "}"));

                postRequest.setEntity(new UrlEncodedFormEntity(postParameters));
                getResponse = client.execute(postRequest);

                int statusCode = getResponse.getStatusLine().getStatusCode();

                if (statusCode != HttpStatus.SC_OK) {
                    Log.w(getClass().getSimpleName(), "Error:" + statusCode + " for URL " + taskURL);
                } else {
                    Log.w("updateTaskStatusToServer", "Status updated");
                    fda.setTaskSynchronized(tid); // Mark the task status as synchronised
                }
            }

            taskListCursor.moveToNext();
        }
        taskListCursor.close();

    }

    /*
      * Loop through the entries from the source
      *   (1) Add entries that have a status of "new", "pending" or "accepted" and are not already on the phone
      *   (2) Update the status of database entries where the source status is set to "Missed" or "Cancelled"
      */
    private int addAndUpdateEntries(TaskResponse tr, FileDbAdapter fda, HashMap<String, TaskStatus> taskMap,
            String username, String source) throws Exception {
        int count = 0;
        if (tr.taskAssignments == null) {
            results.put("err_no_tasks", "");
        } else {
            for (TaskAssignment ta : tr.taskAssignments) {

                if (isCancelled()) {
                    return count;
                }
                ; // Return if the user cancels

                if (ta.task.type.equals("xform")) {
                    Assignment assignment = ta.assignment;

                    Log.i(getClass().getSimpleName(),
                            "Task: " + assignment.assignment_id + " Status:" + assignment.assignment_status
                                    + " Mode:" + ta.task.assignment_mode + " Address: " + ta.task.address
                                    + " Type: " + ta.task.type + "Assignee: " + assignment.assignee + "Username: "
                                    + username);

                    /*
                     * The key for a task is actually the tasks assignment id
                     * The same task could be assigned multiple times to a single user
                     *  each time it will have a new assignment id
                     */
                    // 
                    String uid = String.valueOf(assignment.assignment_id); // Unique identifier for task from this source

                    // Find out if this task is already on the phone
                    TaskStatus ts = taskMap.get(uid);
                    if (ts == null) {
                        Log.i(getClass().getSimpleName(), "New task: " + uid);
                        // New task
                        if (assignment.assignment_status.equals(FileDbAdapter.STATUS_T_NEW)
                                || assignment.assignment_status.equals(FileDbAdapter.STATUS_T_PENDING)
                                || assignment.assignment_status.equals(FileDbAdapter.STATUS_T_ACCEPTED)) {

                            // Ensure the form and instance data are available on the phone
                            // First make sure the initial_data url is sensible (ie null or a URL)
                            if (ta.task.initial_data != null && !ta.task.initial_data.startsWith("http")) {
                                ta.task.initial_data = null;
                            }

                            if (isCancelled()) {
                                return count;
                            }
                            ; // Return if the user cancels

                            // Download form and optionally instance data
                            ManageForm mf = new ManageForm();
                            ManageFormResponse mfr = mf.insertForm(ta.task.form_id, ta.task.url,
                                    ta.task.initial_data, uid);
                            if (!mfr.isError) {
                                // Create the task entry
                                fda.createTask(-1, source, ta, mfr.formPath, mfr.instancePath);
                                results.put(uid + ":" + ta.task.title, "Created");
                                count++;
                            } else {
                                results.put(uid + ":" + ta.task.title, "Creation failed: " + mfr.statusMsg);
                                count++;
                            }
                        }
                    } else {
                        Log.i(getClass().getSimpleName(), "Existing Task: " + uid);
                        // Existing task
                        if (assignment.assignment_status.equals(FileDbAdapter.STATUS_T_MISSED)
                                || assignment.assignment_status.equals(FileDbAdapter.STATUS_T_CANCELLED)) {
                            fda.updateTaskStatusForAssignment(Long.parseLong(uid), assignment.assignment_status,
                                    source);
                            results.put(uid + ":" + ta.task.title, assignment.assignment_status);
                            count++;
                        } else { // check and update other details
                            fda.updateTask(ta);
                        }
                    }

                } // end process for xform task
            } // end tasks loop
        }

        return count;
    }

    /*
      * Return true if the passed in instance file is in the odk instance database
      * Assume that if it has been deleted from the database then it can't be sent although
      * it is probably still on the sdcard
      */
    boolean instanceExists(String instancePath) {
        boolean exists = true;

        // Get the provider URI of the instance 
        String where = InstanceColumns.INSTANCE_FILE_PATH + "=?";
        String[] whereArgs = { instancePath };

        ContentResolver cr = Collect.getInstance().getContentResolver();
        Cursor cInstanceProvider = cr.query(InstanceColumns.CONTENT_URI, null, where, whereArgs, null);
        if (cInstanceProvider.getCount() != 1) {
            Log.e("MainListActivity:completeTask",
                    "Unique instance not found: count is:" + cInstanceProvider.getCount());
            exists = false;
        }
        cInstanceProvider.close();
        return exists;
    }
}