org.opendatakit.common.android.provider.impl.InstanceProviderImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.common.android.provider.impl.InstanceProviderImpl.java

Source

/*
 * Copyright (C) 2007 The Android Open Source Project
 * Copyright (C) 2011-2013 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 org.opendatakit.common.android.provider.impl;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;

import org.apache.commons.io.FileUtils;
import org.opendatakit.aggregate.odktables.rest.KeyValueStoreConstants;
import org.opendatakit.aggregate.odktables.rest.TableConstants;
import org.opendatakit.common.android.R;
import org.opendatakit.common.android.data.ColumnDefinition;
import org.opendatakit.common.android.database.DatabaseConstants;
import org.opendatakit.common.android.database.DatabaseFactory;
import org.opendatakit.common.android.provider.DataTableColumns;
import org.opendatakit.common.android.provider.InstanceColumns;
import org.opendatakit.common.android.provider.KeyValueStoreColumns;
import org.opendatakit.common.android.utilities.ODKDatabaseUtils;
import org.opendatakit.common.android.utilities.ODKFileUtils;
import org.opendatakit.common.android.utilities.TableUtil;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;

/**
 * TODO: convert to true app-scoped instance provider
 */
public abstract class InstanceProviderImpl extends ContentProvider {

    private static final String t = "InstancesProviderImpl";

    private static final String DATA_TABLE_ID_COLUMN = DataTableColumns.ID;
    private static final String DATA_TABLE_SAVEPOINT_TIMESTAMP_COLUMN = DataTableColumns.SAVEPOINT_TIMESTAMP;
    private static final String DATA_TABLE_SAVEPOINT_TYPE_COLUMN = DataTableColumns.SAVEPOINT_TYPE;

    private static final HashMap<String, String> sInstancesProjectionMap;

    public abstract String getInstanceAuthority();

    private static class IdStruct {
        public final String idUploadsTable;
        public final String idDataTable;

        IdStruct(String idUploadsTable, String idDataTable) {
            this.idUploadsTable = idUploadsTable;
            this.idDataTable = idDataTable;
        }
    }

    @Override
    public boolean onCreate() {

        try {
            ODKFileUtils.verifyExternalStorageAvailability();
            File f = new File(ODKFileUtils.getOdkFolder());
            if (!f.exists()) {
                f.mkdir();
            } else if (!f.isDirectory()) {
                Log.e(t, f.getAbsolutePath() + " is not a directory!");
                return false;
            }
        } catch (Exception e) {
            Log.e(t, "External storage not available");
            return false;
        }

        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        List<String> segments = uri.getPathSegments();

        if (segments.size() < 2 || segments.size() > 3) {
            throw new SQLException("Unknown URI (too many segments!) " + uri);
        }

        String appName = segments.get(0);
        ODKFileUtils.verifyExternalStorageAvailability();
        ODKFileUtils.assertDirectoryStructure(appName);
        String tableId = segments.get(1);
        String instanceName = null;
        // _ID in UPLOADS_TABLE_NAME
        String instanceId = (segments.size() == 3 ? segments.get(2) : null);

        SQLiteDatabase db = null;
        String fullQuery;
        String filterArgs[];
        Cursor c = null;

        String dbTableName;
        List<ColumnDefinition> orderedDefns;

        StringBuilder b = new StringBuilder();

        try {
            db = DatabaseFactory.get().getDatabase(getContext(), appName);
            db.beginTransaction();

            boolean success = false;
            try {
                success = ODKDatabaseUtils.get().hasTableId(db, tableId);
            } catch (Exception e) {
                e.printStackTrace();
                throw new SQLException("Unknown URI (exception testing for tableId) " + uri);
            }
            if (!success) {
                throw new SQLException("Unknown URI (missing data table for tableId) " + uri);
            }

            dbTableName = "\"" + tableId + "\"";

            try {
                c = db.query(DatabaseConstants.KEY_VALUE_STORE_ACTIVE_TABLE_NAME,
                        new String[] { KeyValueStoreColumns.VALUE },
                        KeyValueStoreColumns.TABLE_ID + "=? AND " + KeyValueStoreColumns.PARTITION + "=? AND "
                                + KeyValueStoreColumns.ASPECT + "=? AND " + KeyValueStoreColumns.KEY + "=?",
                        new String[] { tableId, KeyValueStoreConstants.PARTITION_TABLE,
                                KeyValueStoreConstants.ASPECT_DEFAULT, KeyValueStoreConstants.XML_INSTANCE_NAME },
                        null, null, null);

                if (c.getCount() == 1) {
                    c.moveToFirst();
                    int idxInstanceName = c.getColumnIndex(KeyValueStoreColumns.VALUE);
                    instanceName = c.getString(idxInstanceName);
                }
            } finally {
                c.close();
            }

            // ARGH! we must ensure that we have records in our UPLOADS_TABLE_NAME
            // for every distinct instance in the data table.
            b.setLength(0);
            //@formatter:off
            b.append("INSERT INTO ").append(DatabaseConstants.UPLOADS_TABLE_NAME).append("(")
                    .append(InstanceColumns.DATA_INSTANCE_ID).append(",")
                    .append(InstanceColumns.DATA_TABLE_TABLE_ID).append(") ").append("SELECT ")
                    .append(InstanceColumns.DATA_INSTANCE_ID).append(",")
                    .append(InstanceColumns.DATA_TABLE_TABLE_ID).append(" FROM (").append("SELECT DISTINCT ")
                    .append(DATA_TABLE_ID_COLUMN).append(" as ").append(InstanceColumns.DATA_INSTANCE_ID)
                    .append(",").append("? as ").append(InstanceColumns.DATA_TABLE_TABLE_ID).append(" FROM ")
                    .append(dbTableName).append(" EXCEPT SELECT DISTINCT ").append(InstanceColumns.DATA_INSTANCE_ID)
                    .append(",").append(InstanceColumns.DATA_TABLE_TABLE_ID).append(" FROM ")
                    .append(DatabaseConstants.UPLOADS_TABLE_NAME).append(")");
            //@formatter:on

            // TODO: should we collapse across FORM_ID or leave it this way?
            String[] args = { tableId };
            db.execSQL(b.toString(), args);

            // Can't get away with dataTable.* because of collision with _ID column
            // get map of (elementKey -> ColumnDefinition)
            try {
                orderedDefns = TableUtil.get().getColumnDefinitions(db, appName, tableId);
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
                throw new SQLException("Unable to retrieve column definitions for tableId " + tableId);
            }

            db.setTransactionSuccessful();
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }

        ////////////////////////////////////////////////////////////////
        // OK we have the info we need -- now build the query we want...

        // We can now join through and access the data table rows

        b.setLength(0);
        // @formatter:off
        b.append("SELECT ");
        b.append(DatabaseConstants.UPLOADS_TABLE_NAME).append(".").append(InstanceColumns._ID).append(" as ")
                .append(InstanceColumns._ID).append(",").append(DatabaseConstants.UPLOADS_TABLE_NAME).append(".")
                .append(InstanceColumns.DATA_INSTANCE_ID).append(" as ").append(InstanceColumns.DATA_INSTANCE_ID)
                .append(",").append(DatabaseConstants.UPLOADS_TABLE_NAME).append(".")
                .append(InstanceColumns.SUBMISSION_INSTANCE_ID).append(" as ")
                .append(InstanceColumns.SUBMISSION_INSTANCE_ID).append(",");
        // add the dataTable metadata except for _ID (which conflicts with InstanceColumns._ID)
        b.append(dbTableName).append(".").append(DataTableColumns.ROW_ETAG).append(" as ")
                .append(DataTableColumns.ROW_ETAG).append(",").append(dbTableName).append(".")
                .append(DataTableColumns.SYNC_STATE).append(" as ").append(DataTableColumns.SYNC_STATE).append(",")
                .append(dbTableName).append(".").append(DataTableColumns.CONFLICT_TYPE).append(" as ")
                .append(DataTableColumns.CONFLICT_TYPE).append(",").append(dbTableName).append(".")
                .append(DataTableColumns.FILTER_TYPE).append(" as ").append(DataTableColumns.FILTER_TYPE)
                .append(",").append(dbTableName).append(".").append(DataTableColumns.FILTER_VALUE).append(" as ")
                .append(DataTableColumns.FILTER_VALUE).append(",").append(dbTableName).append(".")
                .append(DataTableColumns.FORM_ID).append(" as ").append(DataTableColumns.FORM_ID).append(",")
                .append(dbTableName).append(".").append(DataTableColumns.LOCALE).append(" as ")
                .append(DataTableColumns.LOCALE).append(",").append(dbTableName).append(".")
                .append(DataTableColumns.SAVEPOINT_TYPE).append(" as ").append(DataTableColumns.SAVEPOINT_TYPE)
                .append(",").append(dbTableName).append(".").append(DataTableColumns.SAVEPOINT_TIMESTAMP)
                .append(" as ").append(DataTableColumns.SAVEPOINT_TIMESTAMP).append(",").append(dbTableName)
                .append(".").append(DataTableColumns.SAVEPOINT_CREATOR).append(" as ")
                .append(DataTableColumns.SAVEPOINT_CREATOR).append(",");
        // add the user-specified data fields in this dataTable
        for (ColumnDefinition cd : orderedDefns) {
            if (cd.isUnitOfRetention()) {
                b.append(dbTableName).append(".").append(cd.getElementKey()).append(" as ")
                        .append(cd.getElementKey()).append(",");
            }
        }
        b.append("CASE WHEN ").append(DATA_TABLE_SAVEPOINT_TIMESTAMP_COLUMN).append(" IS NULL THEN null")
                .append(" WHEN ").append(InstanceColumns.XML_PUBLISH_TIMESTAMP).append(" IS NULL THEN null")
                .append(" WHEN ").append(DATA_TABLE_SAVEPOINT_TIMESTAMP_COLUMN).append(" > ")
                .append(InstanceColumns.XML_PUBLISH_TIMESTAMP).append(" THEN null").append(" ELSE ")
                .append(InstanceColumns.XML_PUBLISH_TIMESTAMP).append(" END as ")
                .append(InstanceColumns.XML_PUBLISH_TIMESTAMP).append(",");
        b.append("CASE WHEN ").append(DATA_TABLE_SAVEPOINT_TIMESTAMP_COLUMN).append(" IS NULL THEN null")
                .append(" WHEN ").append(InstanceColumns.XML_PUBLISH_TIMESTAMP).append(" IS NULL THEN null")
                .append(" WHEN ").append(DATA_TABLE_SAVEPOINT_TIMESTAMP_COLUMN).append(" > ")
                .append(InstanceColumns.XML_PUBLISH_TIMESTAMP).append(" THEN null").append(" ELSE ")
                .append(InstanceColumns.XML_PUBLISH_STATUS).append(" END as ")
                .append(InstanceColumns.XML_PUBLISH_STATUS).append(",");
        b.append("CASE WHEN ").append(DATA_TABLE_SAVEPOINT_TIMESTAMP_COLUMN).append(" IS NULL THEN null")
                .append(" WHEN ").append(InstanceColumns.XML_PUBLISH_TIMESTAMP).append(" IS NULL THEN null")
                .append(" WHEN ").append(DATA_TABLE_SAVEPOINT_TIMESTAMP_COLUMN).append(" > ")
                .append(InstanceColumns.XML_PUBLISH_TIMESTAMP).append(" THEN null").append(" ELSE ")
                .append(InstanceColumns.DISPLAY_SUBTEXT).append(" END as ").append(InstanceColumns.DISPLAY_SUBTEXT)
                .append(",");
        if (instanceName == null) {
            b.append(DataTableColumns.SAVEPOINT_TIMESTAMP);
        } else {
            b.append(instanceName);
        }
        b.append(" as ").append(InstanceColumns.DISPLAY_NAME);
        b.append(" FROM ");
        b.append("( SELECT * FROM ").append(dbTableName).append(" AS T WHERE T.")
                .append(DATA_TABLE_SAVEPOINT_TIMESTAMP_COLUMN).append("=(SELECT MAX(V.")
                .append(DATA_TABLE_SAVEPOINT_TIMESTAMP_COLUMN).append(") FROM ").append(dbTableName)
                .append(" AS V WHERE V.").append(DATA_TABLE_ID_COLUMN).append("=T.").append(DATA_TABLE_ID_COLUMN)
                .append(" AND V.").append(DATA_TABLE_SAVEPOINT_TYPE_COLUMN).append(" IS NOT NULL").append(")")
                .append(") as ").append(dbTableName);
        b.append(" JOIN ").append(DatabaseConstants.UPLOADS_TABLE_NAME).append(" ON ").append(dbTableName)
                .append(".").append(DATA_TABLE_ID_COLUMN).append("=").append(DatabaseConstants.UPLOADS_TABLE_NAME)
                .append(".").append(InstanceColumns.DATA_INSTANCE_ID).append(" AND ").append("? =")
                .append(DatabaseConstants.UPLOADS_TABLE_NAME).append(".")
                .append(InstanceColumns.DATA_TABLE_TABLE_ID);
        b.append(" WHERE ").append(DATA_TABLE_SAVEPOINT_TYPE_COLUMN).append("=?");
        // @formatter:on

        if (instanceId != null) {
            b.append(" AND ").append(DatabaseConstants.UPLOADS_TABLE_NAME).append(".").append(InstanceColumns._ID)
                    .append("=?");
            String tempArgs[] = { tableId, InstanceColumns.STATUS_COMPLETE, instanceId };
            filterArgs = tempArgs;
        } else {
            String tempArgs[] = { tableId, InstanceColumns.STATUS_COMPLETE };
            filterArgs = tempArgs;
        }

        if (selection != null) {
            b.append(" AND (").append(selection).append(")");
        }

        if (selectionArgs != null) {
            String[] tempArgs = new String[filterArgs.length + selectionArgs.length];
            for (int i = 0; i < filterArgs.length; ++i) {
                tempArgs[i] = filterArgs[i];
            }
            for (int i = 0; i < selectionArgs.length; ++i) {
                tempArgs[filterArgs.length + i] = selectionArgs[i];
            }
            filterArgs = tempArgs;
        }

        if (sortOrder != null) {
            b.append(" ORDER BY ").append(sortOrder);
        }

        fullQuery = b.toString();

        db = null;
        boolean success = false;
        try {
            db = DatabaseFactory.get().getDatabase(getContext(), appName);
            c = db.rawQuery(fullQuery, filterArgs);
            // Tell the cursor what uri to watch, so it knows when its source data
            // changes
            c.setNotificationUri(getContext().getContentResolver(), uri);
            success = true;
            return c;
        } finally {
            if (db != null && !success) {
                // leave database open for cursor...
                db.close();
            }
        }
    }

    @Override
    public String getType(Uri uri) {
        // don't see the point of trying to implement this call...
        return null;
        // switch (sUriMatcher.match(uri)) {
        // case INSTANCES:
        // return InstanceColumns.CONTENT_TYPE;
        //
        // case INSTANCE_ID:
        // return InstanceColumns.CONTENT_ITEM_TYPE;
        //
        // default:
        // throw new IllegalArgumentException("Unknown URI " + uri);
        // }
    }

    @Override
    public Uri insert(Uri uri, ContentValues initialValues) {
        throw new IllegalArgumentException("Insert not implemented!");
    }

    private String getDisplaySubtext(String xmlPublishStatus, Date xmlPublishDate) {
        if (xmlPublishDate == null) {
            return getContext().getString(R.string.not_yet_sent);
        } else if (InstanceColumns.STATUS_SUBMITTED.equalsIgnoreCase(xmlPublishStatus)) {
            return new SimpleDateFormat(getContext().getString(R.string.sent_on_date_at_time), Locale.getDefault())
                    .format(xmlPublishDate);
        } else if (InstanceColumns.STATUS_SUBMISSION_FAILED.equalsIgnoreCase(xmlPublishStatus)) {
            return new SimpleDateFormat(getContext().getString(R.string.sending_failed_on_date_at_time),
                    Locale.getDefault()).format(xmlPublishDate);
        } else {
            throw new IllegalStateException("Unrecognized xmlPublishStatus: " + xmlPublishStatus);
        }
    }

    /**
     * This method removes the entry from the content provider, and also removes
     * any associated files. files: form.xml, [formmd5].formdef, formname
     * {directory}
     */
    @Override
    public int delete(Uri uri, String where, String[] whereArgs) {
        List<String> segments = uri.getPathSegments();

        if (segments.size() < 2 || segments.size() > 3) {
            throw new SQLException("Unknown URI (too many segments!) " + uri);
        }

        String appName = segments.get(0);
        ODKFileUtils.verifyExternalStorageAvailability();
        ODKFileUtils.assertDirectoryStructure(appName);
        String tableId = segments.get(1);
        // _ID in UPLOADS_TABLE_NAME
        String instanceId = (segments.size() == 3 ? segments.get(2) : null);

        SQLiteDatabase db = null;
        List<IdStruct> idStructs = new ArrayList<IdStruct>();
        try {
            db = DatabaseFactory.get().getDatabase(getContext(), appName);
            db.beginTransaction();

            boolean success = false;
            try {
                success = ODKDatabaseUtils.get().hasTableId(db, tableId);
            } catch (Exception e) {
                e.printStackTrace();
                throw new SQLException("Unknown URI (exception testing for tableId) " + uri);
            }
            if (!success) {
                throw new SQLException("Unknown URI (missing data table for tableId) " + uri);
            }

            String dbTableName = "\"" + tableId + "\"";

            if (segments.size() == 2) {
                where = "(" + where + ") AND (" + InstanceColumns.DATA_INSTANCE_ID + "=? )";
                if (whereArgs != null) {
                    String[] args = new String[whereArgs.length + 1];
                    for (int i = 0; i < whereArgs.length; ++i) {
                        args[i] = whereArgs[i];
                    }
                    args[whereArgs.length] = instanceId;
                    whereArgs = args;
                } else {
                    whereArgs = new String[] { instanceId };
                }
            }

            Cursor del = null;
            try {
                del = this.query(uri, null, where, whereArgs, null);
                del.moveToPosition(-1);
                while (del.moveToNext()) {
                    String iId = ODKDatabaseUtils.get().getIndexAsString(del,
                            del.getColumnIndex(InstanceColumns._ID));
                    String iIdDataTable = ODKDatabaseUtils.get().getIndexAsString(del,
                            del.getColumnIndex(InstanceColumns.DATA_INSTANCE_ID));
                    idStructs.add(new IdStruct(iId, iIdDataTable));
                    String path = ODKFileUtils.getInstanceFolder(appName, tableId, iIdDataTable);
                    File f = new File(path);
                    if (f.exists()) {
                        if (f.isDirectory()) {
                            FileUtils.deleteDirectory(f);
                        } else {
                            f.delete();
                        }
                    }

                }
            } catch (IOException e) {
                e.printStackTrace();
                throw new IllegalArgumentException("Unable to delete instance directory: " + e.toString());
            } finally {
                if (del != null) {
                    del.close();
                }
            }

            for (IdStruct idStruct : idStructs) {
                db.delete(DatabaseConstants.UPLOADS_TABLE_NAME, InstanceColumns.DATA_INSTANCE_ID + "=?",
                        new String[] { idStruct.idUploadsTable });
                db.delete(dbTableName, DATA_TABLE_ID_COLUMN + "=?", new String[] { idStruct.idDataTable });
            }
            db.setTransactionSuccessful();
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return idStructs.size();
    }

    @Override
    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
        List<String> segments = uri.getPathSegments();

        if (segments.size() != 3) {
            throw new SQLException("Unknown URI (does not specify instance!) " + uri);
        }

        String appName = segments.get(0);
        ODKFileUtils.verifyExternalStorageAvailability();
        ODKFileUtils.assertDirectoryStructure(appName);

        String tableId = segments.get(1);
        // _ID in UPLOADS_TABLE_NAME
        String instanceId = segments.get(2);

        SQLiteDatabase db = null;
        int count = 0;
        try {
            db = DatabaseFactory.get().getDatabase(getContext(), appName);

            boolean success = false;
            try {
                success = ODKDatabaseUtils.get().hasTableId(db, tableId);
            } catch (Exception e) {
                e.printStackTrace();
                throw new SQLException("Unknown URI (exception testing for tableId) " + uri);
            }
            if (!success) {
                throw new SQLException("Unknown URI (missing data table for tableId) " + uri);
            }

            String dbTableName = "\"" + tableId + "\"";

            // run the query to get all the ids...
            List<IdStruct> idStructs = new ArrayList<IdStruct>();
            Cursor ref = null;
            try {
                // use this provider's query interface to get the set of ids that
                // match (if any)
                ref = this.query(uri, null, where, whereArgs, null);
                if (ref.getCount() != 0) {
                    ref.moveToFirst();
                    do {
                        String iId = ODKDatabaseUtils.get().getIndexAsString(ref,
                                ref.getColumnIndex(InstanceColumns._ID));
                        String iIdDataTable = ODKDatabaseUtils.get().getIndexAsString(ref,
                                ref.getColumnIndex(InstanceColumns.DATA_INSTANCE_ID));
                        idStructs.add(new IdStruct(iId, iIdDataTable));
                    } while (ref.moveToNext());
                }
            } finally {
                if (ref != null) {
                    ref.close();
                }
            }

            // update the values string...
            if (values.containsKey(InstanceColumns.XML_PUBLISH_STATUS)) {
                Date xmlPublishDate = new Date();
                values.put(InstanceColumns.XML_PUBLISH_TIMESTAMP,
                        TableConstants.nanoSecondsFromMillis(xmlPublishDate.getTime()));
                String xmlPublishStatus = values.getAsString(InstanceColumns.XML_PUBLISH_STATUS);
                if (values.containsKey(InstanceColumns.DISPLAY_SUBTEXT) == false) {
                    String text = getDisplaySubtext(xmlPublishStatus, xmlPublishDate);
                    values.put(InstanceColumns.DISPLAY_SUBTEXT, text);
                }
            }

            db.beginTransaction();
            String[] args = new String[1];
            for (IdStruct idStruct : idStructs) {
                args[0] = idStruct.idUploadsTable;
                count += db.update(DatabaseConstants.UPLOADS_TABLE_NAME, values, InstanceColumns._ID + "=?", args);
            }
            db.setTransactionSuccessful();
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    static {

        sInstancesProjectionMap = new HashMap<String, String>();
        sInstancesProjectionMap.put(InstanceColumns._ID, InstanceColumns._ID);
        sInstancesProjectionMap.put(InstanceColumns.DATA_INSTANCE_ID, InstanceColumns.DATA_INSTANCE_ID);
        sInstancesProjectionMap.put(InstanceColumns.XML_PUBLISH_TIMESTAMP, InstanceColumns.XML_PUBLISH_TIMESTAMP);
        sInstancesProjectionMap.put(InstanceColumns.XML_PUBLISH_STATUS, InstanceColumns.XML_PUBLISH_STATUS);
    }

}