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

Java tutorial

Introduction

Here is the source code for org.opendatakit.common.android.provider.impl.FormsProviderImpl.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.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.opendatakit.common.android.R;
import org.opendatakit.common.android.database.DatabaseFactory;
import org.opendatakit.common.android.database.DatabaseConstants;
import org.opendatakit.common.android.provider.FormsColumns;
import org.opendatakit.common.android.utilities.ODKDatabaseUtils;
import org.opendatakit.common.android.utilities.ODKFileUtils;
import org.opendatakit.common.android.utilities.WebLogger;

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.text.TextUtils;
import android.util.Log;

/**
 *
 */
public abstract class FormsProviderImpl extends ContentProvider {
    static final String t = "FormsProvider";

    public abstract String getFormsAuthority();

    @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 where, String[] whereArgs, String sortOrder) {
        List<String> segments = uri.getPathSegments();

        if (segments.size() < 1 || segments.size() > 2) {
            throw new IllegalArgumentException("Unknown URI (incorrect number of segments!) " + uri);
        }

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

        String uriFormId = ((segments.size() == 2) ? segments.get(1) : null);
        boolean isNumericId = StringUtils.isNumeric(uriFormId);

        // Modify the where clause to account for the presence of
        // a form id. Accept either:
        // (1) numeric _ID value
        // (2) string FORM_ID value.
        String whereId;
        String[] whereIdArgs;

        if (uriFormId == null) {
            whereId = where;
            whereIdArgs = whereArgs;
        } else {
            if (TextUtils.isEmpty(where)) {
                whereId = (isNumericId ? FormsColumns._ID : FormsColumns.FORM_ID) + "=?";
                whereIdArgs = new String[1];
                whereIdArgs[0] = uriFormId;
            } else {
                whereId = (isNumericId ? FormsColumns._ID : FormsColumns.FORM_ID) + "=? AND (" + where + ")";
                whereIdArgs = new String[whereArgs.length + 1];
                whereIdArgs[0] = uriFormId;
                for (int i = 0; i < whereArgs.length; ++i) {
                    whereIdArgs[i + 1] = whereArgs[i];
                }
            }
        }

        // Get the database and run the query
        SQLiteDatabase db = null;
        boolean success = false;
        Cursor c = null;
        try {
            db = DatabaseFactory.get().getDatabase(getContext(), appName);
            c = db.query(DatabaseConstants.FORMS_TABLE_NAME, projection, whereId, whereIdArgs, null, null,
                    sortOrder);
            success = true;
        } catch (Exception e) {
            log.w(t, "Unable to query database for appName: " + appName);
            return null;
        } finally {
            if (!success && db != null) {
                db.close();
            }
        }

        if (c == null) {
            log.w(t, "Unable to query database for appName: " + appName);
            return null;
        }
        // Tell the cursor what uri to watch, so it knows when its source data
        // changes
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }

    @Override
    public String getType(Uri uri) {
        List<String> segments = uri.getPathSegments();

        if (segments.size() < 1 || segments.size() > 2) {
            throw new IllegalArgumentException("Unknown URI (incorrect number of segments!) " + uri);
        }
        String uriFormId = ((segments.size() == 2) ? segments.get(1) : null);

        if (uriFormId == null) {
            return FormsColumns.CONTENT_TYPE;
        } else {
            return FormsColumns.CONTENT_ITEM_TYPE;
        }
    }

    private void patchUpValues(String appName, ContentValues values) {
        // don't let users put in a manual FORM_FILE_PATH
        if (values.containsKey(FormsColumns.APP_RELATIVE_FORM_FILE_PATH)) {
            values.remove(FormsColumns.APP_RELATIVE_FORM_FILE_PATH);
        }

        // don't let users put in a manual FORM_PATH
        if (values.containsKey(FormsColumns.FORM_PATH)) {
            values.remove(FormsColumns.FORM_PATH);
        }

        // don't let users put in a manual DATE
        if (values.containsKey(FormsColumns.DATE)) {
            values.remove(FormsColumns.DATE);
        }

        // don't let users put in a manual md5 hash
        if (values.containsKey(FormsColumns.MD5_HASH)) {
            values.remove(FormsColumns.MD5_HASH);
        }

        // don't let users put in a manual json md5 hash
        if (values.containsKey(FormsColumns.JSON_MD5_HASH)) {
            values.remove(FormsColumns.JSON_MD5_HASH);
        }

        // if we are not updating FORM_MEDIA_PATH, we don't need to recalc any
        // of the above
        if (!values.containsKey(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH)) {
            return;
        }

        // Normalize path...

        // First, construct the full file path...
        String path = values.getAsString(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH);
        File mediaPath;
        if (path.startsWith(File.separator)) {
            mediaPath = new File(path);
        } else {
            mediaPath = ODKFileUtils.asAppFile(appName, path);
        }

        // require that the form directory actually exists
        if (!mediaPath.exists()) {
            throw new IllegalArgumentException(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH
                    + " directory does not exist: " + mediaPath.getAbsolutePath());
        }

        if (!ODKFileUtils.isPathUnderAppName(appName, mediaPath)) {
            throw new IllegalArgumentException(
                    "Form definition is not contained within the application: " + appName);
        }

        values.put(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH, ODKFileUtils.asRelativePath(appName, mediaPath));

        // require that it contain a formDef file
        File formDefFile = new File(mediaPath, ODKFileUtils.FORMDEF_JSON_FILENAME);
        if (!formDefFile.exists()) {
            throw new IllegalArgumentException(
                    ODKFileUtils.FORMDEF_JSON_FILENAME + " does not exist in: " + mediaPath.getAbsolutePath());
        }

        // date is the last modification date of the formDef file
        Long now = formDefFile.lastModified();
        values.put(FormsColumns.DATE, now);

        // ODK2: FILENAME_XFORMS_XML may not exist if non-ODK1 fetch path...
        File xformsFile = new File(mediaPath, ODKFileUtils.FILENAME_XFORMS_XML);
        if (xformsFile.exists()) {
            values.put(FormsColumns.APP_RELATIVE_FORM_FILE_PATH, ODKFileUtils.asRelativePath(appName, xformsFile));
        }

        // compute FORM_PATH...
        String formPath = ODKFileUtils.getRelativeFormPath(appName, formDefFile);
        values.put(FormsColumns.FORM_PATH, formPath);

        String md5;
        if (xformsFile.exists()) {
            md5 = ODKFileUtils.getMd5Hash(appName, xformsFile);
        } else {
            md5 = "-none-";
        }
        values.put(FormsColumns.MD5_HASH, md5);

        md5 = ODKFileUtils.getMd5Hash(appName, formDefFile);
        values.put(FormsColumns.JSON_MD5_HASH, md5);
    }

    @Override
    public synchronized Uri insert(Uri uri, ContentValues initialValues) {
        List<String> segments = uri.getPathSegments();

        if (segments.size() != 1) {
            throw new IllegalArgumentException("Unknown URI (too many segments!) " + uri);
        }

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

        ContentValues values;
        if (initialValues != null) {
            values = new ContentValues(initialValues);
        } else {
            values = new ContentValues();
        }

        // ODK2: require FORM_MEDIA_PATH (different behavior -- ODK1 and
        // required FORM_FILE_PATH)
        if (!values.containsKey(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH)) {
            throw new IllegalArgumentException(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH + " must be specified.");
        }

        // Normalize path...
        File mediaPath = ODKFileUtils.asAppFile(appName,
                values.getAsString(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH));

        // require that the form directory actually exists
        if (!mediaPath.exists()) {
            throw new IllegalArgumentException(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH
                    + " directory does not exist: " + mediaPath.getAbsolutePath());
        }

        patchUpValues(appName, values);

        if (values.containsKey(FormsColumns.DISPLAY_SUBTEXT) == false) {
            Date today = new Date();
            String ts = new SimpleDateFormat(getContext().getString(R.string.added_on_date_at_time),
                    Locale.getDefault()).format(today);
            values.put(FormsColumns.DISPLAY_SUBTEXT, ts);
        }

        if (values.containsKey(FormsColumns.DISPLAY_NAME) == false) {
            values.put(FormsColumns.DISPLAY_NAME, mediaPath.getName());
        }

        // first try to see if a record with this filename already exists...
        String[] projection = { FormsColumns.FORM_ID, FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH };
        String[] selectionArgs = { ODKFileUtils.asRelativePath(appName, mediaPath) };
        String selection = FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH + "=?";
        Cursor c = null;

        SQLiteDatabase db = null;
        try {
            db = DatabaseFactory.get().getDatabase(getContext(), appName);
            db.beginTransaction();
            try {
                c = db.query(DatabaseConstants.FORMS_TABLE_NAME, projection, selection, selectionArgs, null, null,
                        null);
                if (c == null) {
                    throw new SQLException("FAILED Insert into " + uri
                            + " -- unable to query for existing records: " + mediaPath.getAbsolutePath());
                }
                if (c.getCount() > 0) {
                    // already exists
                    throw new SQLException("FAILED Insert into " + uri
                            + " -- row already exists for form directory: " + mediaPath.getAbsolutePath());
                }
            } catch (Exception e) {
                log.w(t, "FAILED Insert into " + uri + " -- query for existing row failed: " + e.toString());

                if (e instanceof SQLException) {
                    throw (SQLException) e;
                } else {
                    throw new SQLException(
                            "FAILED Insert into " + uri + " -- query for existing row failed: " + e.toString());
                }
            } finally {
                if (c != null) {
                    c.close();
                }
            }

            try {
                long rowId = db.insert(DatabaseConstants.FORMS_TABLE_NAME, null, values);
                db.setTransactionSuccessful();
                if (rowId > 0) {
                    Uri formUri = Uri.withAppendedPath(
                            Uri.withAppendedPath(Uri.parse("content://" + getFormsAuthority()), appName),
                            values.getAsString(FormsColumns.FORM_ID));
                    getContext().getContentResolver().notifyChange(formUri, null);
                    Uri idUri = Uri.withAppendedPath(
                            Uri.withAppendedPath(Uri.parse("content://" + getFormsAuthority()), appName),
                            Long.toString(rowId));
                    getContext().getContentResolver().notifyChange(idUri, null);

                    return formUri;
                }
            } catch (Exception e) {
                log.w(t, "FAILED Insert into " + uri + " -- insert of row failed: " + e.toString());

                if (e instanceof SQLException) {
                    throw (SQLException) e;
                } else {
                    throw new SQLException(
                            "FAILED Insert into " + uri + " -- insert of row failed: " + e.toString());
                }
            }
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }

        throw new SQLException("Failed to insert row into " + uri);
    }

    /** used only within moveDirectory */
    static enum DirType {
        FORMS, FRAMEWORK, OTHER
    };

    private void moveDirectory(String appName, DirType mediaType, File mediaDirectory) throws IOException {
        WebLogger log = WebLogger.getLogger(appName);

        if (mediaDirectory.exists() && mediaType != DirType.OTHER) {
            // it is a directory under our control
            // -- move it to the stale forms or framework path...
            // otherwise, it is not where we will look for it,
            // so we can ignore it (once the record is gone
            // from our FormsProvider, we will not accidentally
            // detect it).
            String rootName = mediaDirectory.getName();
            int rev = 2;
            String staleMediaPathBase;
            if (mediaType == DirType.FORMS) {
                staleMediaPathBase = ODKFileUtils.getStaleFormsFolder(appName) + File.separator;
            } else {
                staleMediaPathBase = ODKFileUtils.getStaleFrameworkFolder(appName) + File.separator;
            }

            String staleMediaPathName = staleMediaPathBase + rootName;
            File staleMediaPath = new File(staleMediaPathName);

            while (staleMediaPath.exists()) {
                try {
                    if (staleMediaPath.exists()) {
                        FileUtils.deleteDirectory(staleMediaPath);
                        if (!staleMediaPath.exists()) {
                            // we successfully deleted an older directory -- reuse it...
                            break;
                        }
                    }
                    log.i(t, "Successful delete of stale directory: " + staleMediaPathName);
                } catch (IOException ex) {
                    ex.printStackTrace();
                    log.i(t, "Unable to delete stale directory: " + staleMediaPathName);
                }
                staleMediaPathName = staleMediaPathBase + rootName + "_" + rev;
                staleMediaPath = new File(staleMediaPathName);
                rev++;
            }
            FileUtils.moveDirectory(mediaDirectory, staleMediaPath);
        }
    }

    /**
     * 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() < 1 || segments.size() > 2) {
            throw new IllegalArgumentException("Unknown URI (incorrect number of segments!) " + uri);
        }

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

        String uriFormId = ((segments.size() == 2) ? segments.get(1) : null);
        boolean isNumericId = StringUtils.isNumeric(uriFormId);

        // Modify the where clause to account for the presence of
        // a form id. Accept either:
        // (1) numeric _ID value
        // (2) string FORM_ID value.
        String whereId;
        String[] whereIdArgs;

        if (uriFormId == null) {
            whereId = where;
            whereIdArgs = whereArgs;
        } else {
            if (TextUtils.isEmpty(where)) {
                whereId = (isNumericId ? FormsColumns._ID : FormsColumns.FORM_ID) + "=?";
                whereIdArgs = new String[1];
                whereIdArgs[0] = uriFormId;
            } else {
                whereId = (isNumericId ? FormsColumns._ID : FormsColumns.FORM_ID) + "=? AND (" + where + ")";
                whereIdArgs = new String[whereArgs.length + 1];
                whereIdArgs[0] = uriFormId;
                for (int i = 0; i < whereArgs.length; ++i) {
                    whereIdArgs[i + 1] = whereArgs[i];
                }
            }
        }

        Cursor del = null;
        Integer idValue = null;
        String tableIdValue = null;
        String formIdValue = null;
        HashMap<File, DirType> mediaDirs = new HashMap<File, DirType>();
        try {
            del = this.query(uri, null, whereId, whereIdArgs, null);
            if (del == null) {
                throw new SQLException("FAILED Delete into " + uri + " -- unable to query for existing records");
            }
            del.moveToPosition(-1);
            while (del.moveToNext()) {
                idValue = ODKDatabaseUtils.get().getIndexAsType(del, Integer.class,
                        del.getColumnIndex(FormsColumns._ID));
                tableIdValue = ODKDatabaseUtils.get().getIndexAsString(del,
                        del.getColumnIndex(FormsColumns.TABLE_ID));
                formIdValue = ODKDatabaseUtils.get().getIndexAsString(del,
                        del.getColumnIndex(FormsColumns.FORM_ID));
                File mediaDir = ODKFileUtils.asAppFile(appName, ODKDatabaseUtils.get().getIndexAsString(del,
                        del.getColumnIndex(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH)));
                mediaDirs.put(mediaDir, (tableIdValue == null) ? DirType.FRAMEWORK : DirType.FORMS);
            }
        } catch (Exception e) {
            log.w(t, "FAILED Delete from " + uri + " -- query for existing row failed: " + e.toString());

            if (e instanceof SQLException) {
                throw (SQLException) e;
            } else {
                throw new SQLException(
                        "FAILED Delete from " + uri + " -- query for existing row failed: " + e.toString());
            }
        } finally {
            if (del != null && !del.isClosed()) {
                del.close();
            }
        }

        SQLiteDatabase db = null;
        int count;
        try {
            db = DatabaseFactory.get().getDatabase(getContext(), appName);
            db.beginTransaction();
            count = db.delete(DatabaseConstants.FORMS_TABLE_NAME, whereId, whereIdArgs);
            db.setTransactionSuccessful();
        } catch (Exception e) {
            e.printStackTrace();
            log.w(t, "Unable to perform deletion " + e.toString());
            return 0;
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }

        // and attempt to move these directories to the stale forms location
        // so that they do not immediately get rescanned...

        for (HashMap.Entry<File, DirType> entry : mediaDirs.entrySet()) {
            try {
                moveDirectory(appName, entry.getValue(), entry.getKey());
            } catch (IOException e) {
                e.printStackTrace();
                log.e(t, "Unable to move directory " + e.toString());
            }
        }

        if (count == 1) {
            Uri formUri = Uri.withAppendedPath(
                    Uri.withAppendedPath(Uri.parse("content://" + getFormsAuthority()), appName), formIdValue);
            getContext().getContentResolver().notifyChange(formUri, null);
            Uri idUri = Uri.withAppendedPath(
                    Uri.withAppendedPath(Uri.parse("content://" + getFormsAuthority()), appName),
                    Long.toString(idValue));
            getContext().getContentResolver().notifyChange(idUri, null);
        } else {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return count;
    }

    static class FormIdVersion {
        final String tableId;
        final String formId;
        final String formVersion;

        FormIdVersion(String tableId, String formId, String formVersion) {
            this.tableId = tableId;
            this.formId = formId;
            this.formVersion = formVersion;
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof FormIdVersion))
                return false;
            FormIdVersion that = (FormIdVersion) o;

            // identical if id and version matches...
            return tableId.equals(that.tableId) && formId.equals(that.formId)
                    && ((formVersion == null) ? (that.formVersion == null)
                            : (that.formVersion != null && formVersion.equals(that.formVersion)));
        }
    }

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

        if (segments.size() < 1 || segments.size() > 2) {
            throw new IllegalArgumentException("Unknown URI (incorrect number of segments!) " + uri);
        }

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

        String uriFormId = ((segments.size() == 2) ? segments.get(1) : null);
        boolean isNumericId = StringUtils.isNumeric(uriFormId);

        // Modify the where clause to account for the presence of
        // a form id. Accept either:
        // (1) numeric _ID value
        // (2) string FORM_ID value.
        String whereId;
        String[] whereIdArgs;

        if (uriFormId == null) {
            whereId = where;
            whereIdArgs = whereArgs;
        } else {
            if (TextUtils.isEmpty(where)) {
                whereId = (isNumericId ? FormsColumns._ID : FormsColumns.FORM_ID) + "=?";
                whereIdArgs = new String[1];
                whereIdArgs[0] = uriFormId;
            } else {
                whereId = (isNumericId ? FormsColumns._ID : FormsColumns.FORM_ID) + "=? AND (" + where + ")";
                whereIdArgs = new String[whereArgs.length + 1];
                whereIdArgs[0] = uriFormId;
                for (int i = 0; i < whereArgs.length; ++i) {
                    whereIdArgs[i + 1] = whereArgs[i];
                }
            }
        }

        /*
         * First, find out what records match this query, and if they refer to two
         * or more (formId,formVersion) tuples, then be sure to remove all
         * FORM_MEDIA_PATH references. Otherwise, if they are all for the same
         * tuple, and the update specifies a FORM_MEDIA_PATH, move all the
         * non-matching directories elsewhere.
         */
        Integer idValue = null;
        String tableIdValue = null;
        String formIdValue = null;
        HashMap<File, DirType> mediaDirs = new HashMap<File, DirType>();
        boolean multiset = false;
        Cursor c = null;
        try {
            c = this.query(uri, null, whereId, whereIdArgs, null);
            if (c == null) {
                throw new SQLException(
                        "FAILED Update of " + uri + " -- query for existing row did not return a cursor");
            }
            if (c.getCount() >= 1) {
                FormIdVersion ref = null;
                c.moveToPosition(-1);
                while (c.moveToNext()) {
                    idValue = ODKDatabaseUtils.get().getIndexAsType(c, Integer.class,
                            c.getColumnIndex(FormsColumns._ID));
                    tableIdValue = ODKDatabaseUtils.get().getIndexAsString(c,
                            c.getColumnIndex(FormsColumns.TABLE_ID));
                    formIdValue = ODKDatabaseUtils.get().getIndexAsString(c,
                            c.getColumnIndex(FormsColumns.FORM_ID));
                    String tableId = ODKDatabaseUtils.get().getIndexAsString(c,
                            c.getColumnIndex(FormsColumns.TABLE_ID));
                    String formId = ODKDatabaseUtils.get().getIndexAsString(c,
                            c.getColumnIndex(FormsColumns.FORM_ID));
                    String formVersion = ODKDatabaseUtils.get().getIndexAsString(c,
                            c.getColumnIndex(FormsColumns.FORM_VERSION));
                    FormIdVersion cur = new FormIdVersion(tableId, formId, formVersion);

                    int appRelativeMediaPathIdx = c.getColumnIndex(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH);
                    String mediaPath = ODKDatabaseUtils.get().getIndexAsString(c, appRelativeMediaPathIdx);
                    if (mediaPath != null) {
                        mediaDirs.put(ODKFileUtils.asAppFile(appName, mediaPath),
                                (tableIdValue == null) ? DirType.FRAMEWORK : DirType.FORMS);
                    }

                    if (ref != null && !ref.equals(cur)) {
                        multiset = true;
                        break;
                    } else {
                        ref = cur;
                    }
                }
            }
        } catch (Exception e) {
            log.w(t, "FAILED Update of " + uri + " -- query for existing row failed: " + e.toString());

            if (e instanceof SQLException) {
                throw (SQLException) e;
            } else {
                throw new SQLException(
                        "FAILED Update of " + uri + " -- query for existing row failed: " + e.toString());
            }
        } finally {
            if (c != null) {
                c.close();
            }
        }

        if (multiset) {
            // don't let users manually update media path
            // we are referring to two or more (formId,formVersion) tuples.
            if (values.containsKey(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH)) {
                values.remove(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH);
            }
        } else if (values.containsKey(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH)) {
            // we are not a multiset and we are setting the media path
            // try to move all the existing non-matching media paths to
            // somewhere else...
            File mediaPath = ODKFileUtils.asAppFile(appName,
                    values.getAsString(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH));
            for (HashMap.Entry<File, DirType> entry : mediaDirs.entrySet()) {
                File altPath = entry.getKey();
                if (!altPath.equals(mediaPath)) {
                    try {
                        moveDirectory(appName, entry.getValue(), altPath);
                    } catch (IOException e) {
                        e.printStackTrace();
                        log.e(t, "Attempt to move " + altPath.getAbsolutePath() + " failed: " + e.toString());
                    }
                }
            }
            // OK. we have moved the existing form definitions elsewhere. We can
            // proceed with update...
        }

        // ensure that all values are correct and ignore some user-supplied
        // values...
        patchUpValues(appName, values);

        // Make sure that the necessary fields are all set
        if (values.containsKey(FormsColumns.DATE) == true) {
            Date today = new Date();
            String ts = new SimpleDateFormat(getContext().getString(R.string.added_on_date_at_time),
                    Locale.getDefault()).format(today);
            values.put(FormsColumns.DISPLAY_SUBTEXT, ts);
        }

        SQLiteDatabase db = null;
        int count;
        try {
            // OK Finally, now do the update...
            db = DatabaseFactory.get().getDatabase(getContext(), appName);
            db.beginTransaction();
            count = db.update(DatabaseConstants.FORMS_TABLE_NAME, values, whereId, whereIdArgs);
            db.setTransactionSuccessful();
        } catch (Exception e) {
            e.printStackTrace();
            log.w(t, "Unable to perform update " + uri);
            return 0;
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }

        if (count == 1) {
            Uri formUri = Uri.withAppendedPath(
                    Uri.withAppendedPath(Uri.parse("content://" + getFormsAuthority()), appName), formIdValue);
            getContext().getContentResolver().notifyChange(formUri, null);
            Uri idUri = Uri.withAppendedPath(
                    Uri.withAppendedPath(Uri.parse("content://" + getFormsAuthority()), appName),
                    Long.toString(idValue));
            getContext().getContentResolver().notifyChange(idUri, null);
        } else {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return count;
    }
}