org.odk.collect.android.tasks.InstanceSyncTask.java Source code

Java tutorial

Introduction

Here is the source code for org.odk.collect.android.tasks.InstanceSyncTask.java

Source

/*
 * Copyright (C) 2017 Nyoman Ribeka
 *
 * 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 org.odk.collect.android.tasks;

import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;

import org.apache.commons.io.FileUtils;
import org.odk.collect.android.R;
import org.odk.collect.android.application.Collect;
import org.odk.collect.android.dao.FormsDao;
import org.odk.collect.android.dao.InstancesDao;
import org.odk.collect.android.exception.EncryptionException;
import org.odk.collect.android.listeners.DiskSyncListener;
import org.odk.collect.android.logic.FormController;
import org.odk.collect.android.preferences.GeneralKeys;
import org.odk.collect.android.provider.FormsProviderAPI.FormsColumns;
import org.odk.collect.android.provider.InstanceProviderAPI;
import org.odk.collect.android.utilities.EncryptionUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import timber.log.Timber;

import static org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns;

/**
 * Background task for syncing form instances from the instances folder to the instances table.
 * Returns immediately if it detects an error.
 */
public class InstanceSyncTask extends AsyncTask<Void, String, String> {

    private static int counter;

    private String currentStatus = "";
    private DiskSyncListener diskSyncListener;

    public String getStatusMessage() {
        return currentStatus;
    }

    public void setDiskSyncListener(DiskSyncListener diskSyncListener) {
        this.diskSyncListener = diskSyncListener;
    }

    @Override
    protected String doInBackground(Void... params) {
        int instance = ++counter;
        Timber.i("[%d] doInBackground begins!", instance);

        try {
            List<String> candidateInstances = new LinkedList<String>();
            File instancesPath = new File(Collect.INSTANCES_PATH);
            if (instancesPath.exists() && instancesPath.isDirectory()) {
                File[] instanceFolders = instancesPath.listFiles();
                if (instanceFolders.length == 0) {
                    Timber.i("[%d] Empty instance folder. Stopping scan process.", instance);
                    Timber.d(Collect.getInstance().getString(R.string.instance_scan_completed));
                    return currentStatus;
                }

                // Build the list of potential path that we need to add to the content provider
                for (File instanceDir : instanceFolders) {
                    File instanceFile = new File(instanceDir, instanceDir.getName() + ".xml");
                    if (!instanceFile.exists()) {
                        // Look for submission file that might have been manually copied from e.g. Briefcase
                        File submissionFile = new File(instanceDir, "submission.xml");
                        if (submissionFile.exists()) {
                            submissionFile.renameTo(instanceFile);
                        }
                    }
                    if (instanceFile.exists() && instanceFile.canRead()) {
                        candidateInstances.add(instanceFile.getAbsolutePath());
                    } else {
                        Timber.i("[%d] Ignoring: %s", instance, instanceDir.getAbsolutePath());
                    }
                }
                Collections.sort(candidateInstances);

                List<String> filesToRemove = new ArrayList<>();

                // Remove all the path that's already in the content provider
                Cursor instanceCursor = null;
                InstancesDao instancesDao = new InstancesDao();
                try {
                    String sortOrder = InstanceColumns.INSTANCE_FILE_PATH + " ASC ";
                    instanceCursor = instancesDao.getSavedInstancesCursor(sortOrder);
                    if (instanceCursor == null) {
                        Timber.e("[%d] Instance content provider returned null", instance);
                        return currentStatus;
                    }

                    instanceCursor.moveToPosition(-1);

                    while (instanceCursor.moveToNext()) {
                        String instanceFilename = instanceCursor
                                .getString(instanceCursor.getColumnIndex(InstanceColumns.INSTANCE_FILE_PATH));
                        String instanceStatus = instanceCursor
                                .getString(instanceCursor.getColumnIndex(InstanceColumns.STATUS));
                        if (candidateInstances.contains(instanceFilename)
                                || instanceStatus.equals(InstanceProviderAPI.STATUS_SUBMITTED)) {
                            candidateInstances.remove(instanceFilename);
                        } else {
                            filesToRemove.add(instanceFilename);
                        }
                    }

                } finally {
                    if (instanceCursor != null) {
                        instanceCursor.close();
                    }
                }

                instancesDao.deleteInstancesFromIDs(filesToRemove);

                final boolean instanceSyncFlag = PreferenceManager
                        .getDefaultSharedPreferences(Collect.getInstance().getApplicationContext())
                        .getBoolean(GeneralKeys.KEY_INSTANCE_SYNC, true);

                int counter = 0;
                // Begin parsing and add them to the content provider
                for (String candidateInstance : candidateInstances) {
                    String instanceFormId = getFormIdFromInstance(candidateInstance);
                    // only process if we can find the id from the instance file
                    if (instanceFormId != null) {
                        Cursor formCursor = null;
                        try {
                            String selection = FormsColumns.JR_FORM_ID + " = ? ";
                            String[] selectionArgs = new String[] { instanceFormId };
                            // retrieve the form definition
                            formCursor = new FormsDao().getFormsCursor(selection, selectionArgs);
                            // TODO: optimize this by caching the previously found form definition
                            // TODO: optimize this by caching unavailable form definition to skip
                            if (formCursor != null && formCursor.moveToFirst()) {
                                String submissionUri = null;
                                if (!formCursor.isNull(formCursor.getColumnIndex(FormsColumns.SUBMISSION_URI))) {
                                    submissionUri = formCursor
                                            .getString(formCursor.getColumnIndex(FormsColumns.SUBMISSION_URI));
                                }
                                String jrFormId = formCursor
                                        .getString(formCursor.getColumnIndex(FormsColumns.JR_FORM_ID));
                                String jrVersion = formCursor
                                        .getString(formCursor.getColumnIndex(FormsColumns.JR_VERSION));
                                String formName = formCursor
                                        .getString(formCursor.getColumnIndex(FormsColumns.DISPLAY_NAME));

                                // add missing fields into content values
                                ContentValues values = new ContentValues();
                                values.put(InstanceColumns.INSTANCE_FILE_PATH, candidateInstance);
                                values.put(InstanceColumns.SUBMISSION_URI, submissionUri);
                                values.put(InstanceColumns.DISPLAY_NAME, formName);
                                values.put(InstanceColumns.JR_FORM_ID, jrFormId);
                                values.put(InstanceColumns.JR_VERSION, jrVersion);
                                values.put(InstanceColumns.STATUS,
                                        instanceSyncFlag ? InstanceProviderAPI.STATUS_COMPLETE
                                                : InstanceProviderAPI.STATUS_INCOMPLETE);
                                values.put(InstanceColumns.CAN_EDIT_WHEN_COMPLETE, Boolean.toString(true));
                                // save the new instance object

                                instancesDao.saveInstance(values);
                                counter++;

                                encryptInstanceIfNeeded(formCursor, candidateInstance, values, instancesDao);
                            }
                        } catch (IOException | EncryptionException e) {
                            Timber.w(e);
                        } finally {
                            if (formCursor != null) {
                                formCursor.close();
                            }
                        }
                    }
                }
                if (counter > 0) {
                    currentStatus += String.format(Collect.getInstance().getString(R.string.instance_scan_count),
                            counter);
                }
            }
        } finally {
            Timber.i("[%d] doInBackground ends!", instance);
        }
        return currentStatus;
    }

    private String getFormIdFromInstance(final String instancePath) {
        String instanceFormId = null;
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(new File(instancePath));
            Element element = document.getDocumentElement();
            instanceFormId = element.getAttribute("id");
        } catch (IOException | ParserConfigurationException | SAXException e) {
            Timber.w("Unable to read form id from %s", instancePath);
        }
        return instanceFormId;
    }

    private String getInstanceIdFromInstance(final String instancePath) {
        String instanceId = null;
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(new File(instancePath));
            Element element = document.getDocumentElement();
            instanceId = element.getAttribute("instanceID");
        } catch (IOException | ParserConfigurationException | SAXException e) {
            Timber.w("Unable to read form instanceID from %s", instancePath);
        }
        return instanceId;
    }

    private void encryptInstanceIfNeeded(Cursor formCursor, String candidateInstance, ContentValues values,
            InstancesDao instancesDao) throws EncryptionException, IOException {

        Cursor instanceCursor = new InstancesDao().getInstancesCursorForFilePath(candidateInstance);
        if (instanceCursor != null && instanceCursor.moveToFirst()) {
            if (shouldInstanceBeEncrypted(formCursor)) {
                encryptInstance(instanceCursor, candidateInstance, values, instancesDao);
            }
        }
    }

    private void encryptInstance(Cursor instanceCursor, String candidateInstance, ContentValues values,
            InstancesDao instancesDao) throws EncryptionException, IOException {

        File instanceXml = new File(candidateInstance);
        if (!new File(instanceXml.getParentFile(), "submission.xml.enc").exists()) {
            Uri uri = Uri.parse(InstanceColumns.CONTENT_URI + "/"
                    + instanceCursor.getInt(instanceCursor.getColumnIndex(BaseColumns._ID)));
            FormController.InstanceMetadata instanceMetadata = new FormController.InstanceMetadata(
                    getInstanceIdFromInstance(candidateInstance), null, null);
            EncryptionUtils.EncryptedFormInformation formInfo = EncryptionUtils.getEncryptedFormInformation(uri,
                    instanceMetadata);

            if (formInfo != null) {
                File submissionXml = new File(instanceXml.getParentFile(), "submission.xml");
                FileUtils.copyFile(instanceXml, submissionXml);

                EncryptionUtils.generateEncryptedSubmission(instanceXml, submissionXml, formInfo);

                values.put(InstanceColumns.CAN_EDIT_WHEN_COMPLETE, Boolean.toString(false));
                instancesDao.updateInstance(values, InstanceColumns.INSTANCE_FILE_PATH + "=?",
                        new String[] { candidateInstance });

                SaveToDiskTask.manageFilesAfterSavingEncryptedForm(instanceXml, submissionXml);
                if (!EncryptionUtils.deletePlaintextFiles(instanceXml)) {
                    Timber.e("Error deleting plaintext files for %s", instanceXml.getAbsolutePath());
                }
            }
        }
    }

    private boolean shouldInstanceBeEncrypted(Cursor formCursor) {
        String base64RSAPublicKey = formCursor
                .getString(formCursor.getColumnIndex(FormsColumns.BASE64_RSA_PUBLIC_KEY));
        return base64RSAPublicKey != null && !base64RSAPublicKey.isEmpty();
    }

    @Override
    protected void onPostExecute(String result) {
        super.onPostExecute(result);
        if (diskSyncListener != null) {
            diskSyncListener.syncComplete(result);
        }
    }
}