/* * Copyright (C) 2009 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.odk.collect.android.tasks; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import org.odk.collect.android.R; import org.odk.collect.android.application.Collect; import org.odk.collect.android.listeners.DiskSyncListener; import org.odk.collect.android.provider.FormsProviderAPI.FormsColumns; import org.odk.collect.android.utilities.FileUtils; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.util.Log; /** * Background task for adding to the forms content provider, any forms that have been added to the * sdcard manually. Returns immediately if it detects an error. * * @author Carl Hartung (carlhartung@gmail.com) */ public class DiskSyncTask extends AsyncTask<Void, String, String> { private final static String t = "DiskSyncTask"; DiskSyncListener mListener; /* * (non-Javadoc) * @see android.os.AsyncTask#doInBackground(java.lang.Object[]) */ @Override protected String doInBackground(Void... params) { // Process everything then report what didn't work. StringBuffer errors = new StringBuffer(); File formDir = new File(Collect.FORMS_PATH); if (formDir.exists() && formDir.isDirectory()) { // Get all the files in the /odk/foms directory ArrayList<File> xFormsToAdd = new ArrayList<File>(); // Step 1: assemble the candidate form files // discard files beginning with "." // discard files not ending with ".xml" or ".xhtml" { File[] formDefs = formDir.listFiles(); for ( File addMe: formDefs ) { // Ignore invisible files that start with periods. if (!addMe.getName().startsWith(".") && (addMe.getName().endsWith(".xml") || addMe.getName().endsWith(".xhtml"))) { xFormsToAdd.add(addMe); } else { Log.i(t, "Ignoring: " + addMe.getAbsolutePath()); } } } // Step 2: quickly run through and figure out what files we need to // parse and update; this is quick, as we only calculate the md5 // and see if it has changed. Map<Uri, File> uriToUpdate = new HashMap<Uri, File>(); Cursor mCursor = null; // open the cursor within a try-catch block so it can always be closed. try { mCursor = Collect.getInstance().getContentResolver() .query(FormsColumns.CONTENT_URI, null, null, null, null); if (mCursor == null) { Log.e(t, "Forms Content Provider returned NULL"); errors.append("Internal Error: Unable to access Forms content provider").append("\r\n"); return errors.toString(); } mCursor.moveToPosition(-1); while (mCursor.moveToNext()) { // For each element in the provider, see if the file already exists String sqlFilename = mCursor.getString(mCursor.getColumnIndex(FormsColumns.FORM_FILE_PATH)); String md5 = mCursor.getString(mCursor.getColumnIndex(FormsColumns.MD5_HASH)); File sqlFile = new File(sqlFilename); if (sqlFile.exists()) { // remove it from the list of forms (we only want forms // we haven't added at the end) xFormsToAdd.remove(sqlFile); if (!FileUtils.getMd5Hash(sqlFile).contentEquals(md5)) { // Probably someone overwrite the file on the sdcard // So re-parse it and update it's information String id = mCursor.getString(mCursor.getColumnIndex(FormsColumns._ID)); Uri updateUri = Uri.withAppendedPath(FormsColumns.CONTENT_URI, id); uriToUpdate.put(updateUri, sqlFile); } } else { Log.w(t, "file referenced by content provider does not exist " + sqlFile); } } } finally { if ( mCursor != null ) { mCursor.close(); } } // Step3: go through uriToUpdate to parse and update each in turn. // This is slow because buildContentValues(...) is slow. for ( Map.Entry<Uri, File> entry : uriToUpdate.entrySet() ) { Uri updateUri = entry.getKey(); File formDefFile = entry.getValue(); // Probably someone overwrite the file on the sdcard // So re-parse it and update it's information ContentValues values; try { values = buildContentValues(formDefFile); } catch ( IllegalArgumentException e) { errors.append(e.getMessage()).append("\r\n"); continue; } // update in content provider int count = Collect.getInstance().getContentResolver() .update(updateUri, values, null, null); Log.i(t, count + " records successfully updated"); } uriToUpdate.clear(); // Step 4: go through the newly-discovered files in xFormsToAdd and add them. // This is slow because buildContentValues(...) is slow. for (File formDefFile : xFormsToAdd) { // Parse it for the first time... ContentValues values; try { values = buildContentValues(formDefFile); } catch ( IllegalArgumentException e) { errors.append(e.getMessage()).append("\r\n"); continue; } // insert into content provider Collect.getInstance().getContentResolver() .insert(FormsColumns.CONTENT_URI, values); } } if ( errors.length() != 0 ) { return errors.toString(); } else { return Collect.getInstance().getString(R.string.finished_disk_scan); } } /** * Attempts to parse the formDefFile as an XForm. * This is slow because FileUtils.parseXML is slow * * @param formDefFile * @return key-value list to update or insert into the content provider * @throws IllegalArgumentException if the file failed to parse or was missing fields */ public ContentValues buildContentValues(File formDefFile) throws IllegalArgumentException { // Probably someone overwrite the file on the sdcard // So re-parse it and update it's information ContentValues updateValues = new ContentValues(); HashMap<String, String> fields = null; try { fields = FileUtils.parseXML(formDefFile); } catch (RuntimeException e) { throw new IllegalArgumentException(formDefFile.getName() + " :: " + e.toString()); } String title = fields.get(FileUtils.TITLE); String ui = fields.get(FileUtils.UI); String model = fields.get(FileUtils.MODEL); String formid = fields.get(FileUtils.FORMID); String submission = fields.get(FileUtils.SUBMISSIONURI); String base64RsaPublicKey = fields.get(FileUtils.BASE64_RSA_PUBLIC_KEY); // update date Long now = Long.valueOf(System.currentTimeMillis()); updateValues.put(FormsColumns.DATE, now); if (title != null) { updateValues.put(FormsColumns.DISPLAY_NAME, title); } else { throw new IllegalArgumentException(Collect.getInstance().getString(R.string.xform_parse_error, formDefFile.getName(), "title")); } if (formid != null) { updateValues.put(FormsColumns.JR_FORM_ID, formid); } else { throw new IllegalArgumentException(Collect.getInstance().getString(R.string.xform_parse_error, formDefFile.getName(), "id")); } if (ui != null) { updateValues.put(FormsColumns.UI_VERSION, ui); } if (model != null) { updateValues.put(FormsColumns.MODEL_VERSION, model); } if (submission != null) { updateValues.put(FormsColumns.SUBMISSION_URI, submission); } if (base64RsaPublicKey != null) { updateValues.put(FormsColumns.BASE64_RSA_PUBLIC_KEY, base64RsaPublicKey); } // Note, the path doesn't change here, but it needs to be included so the // update will automatically update the .md5 and the cache path. updateValues.put(FormsColumns.FORM_FILE_PATH, formDefFile.getAbsolutePath()); return updateValues; } public void setDiskSyncListener(DiskSyncListener l) { mListener = l; } /* * (non-Javadoc) * @see android.os.AsyncTask#onPostExecute(java.lang.Object) */ @Override protected void onPostExecute(String result) { super.onPostExecute(result); if (mListener != null) { mListener.SyncComplete(result); } } }