// Copyright 2015 The Project Buendia Authors // // 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 distrib- // uted 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 // specific language governing permissions and limitations under the License. package org.projectbuendia.client.net; import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import android.database.SQLException; import android.os.AsyncTask; import android.support.annotation.Nullable; import com.android.volley.Response; import com.android.volley.VolleyError; import com.google.common.base.Preconditions; import org.odk.collect.android.application.Collect; import org.odk.collect.android.provider.FormsProviderAPI; import org.odk.collect.android.tasks.DiskSyncTask; import org.projectbuendia.client.App; import org.projectbuendia.client.events.FetchXformFailedEvent; import org.projectbuendia.client.events.FetchXformSucceededEvent; import org.projectbuendia.client.utils.Logger; import java.io.File; import java.io.FileWriter; import java.io.IOException; import de.greenrobot.event.EventBus; /** * Synchronizes 1 or more OpenMRS provided forms into the ODK database storage. Very like * {@link org.odk.collect.android.tasks.DiskSyncTask} or * {@link org.odk.collect.android.tasks.DownloadFormsTask} * <p/> * <p>Takes the UUID and, if the form doesn't exist in ODK storage, fetches it from OpenMRS, then * creates {$uuid}.xml in storage. Finally, the form is inserted into ODK's local metadata DB. */ public class OdkXformSyncTask extends AsyncTask<OpenMrsXformIndexEntry, Void, Void> { private static final Logger LOG = Logger.create(); @Nullable private final FormWrittenListener mFormWrittenListener; public static interface FormWrittenListener { public void formWritten(File path, String uuid); } public OdkXformSyncTask(@Nullable FormWrittenListener formWrittenListener) { this.mFormWrittenListener = formWrittenListener; } @Override protected Void doInBackground(OpenMrsXformIndexEntry... formInfos) { for (final OpenMrsXformIndexEntry formInfo : formInfos) { final File proposedPath = formInfo.makeFileForForm(); // Check if the uuid already exists in the database. Cursor cursor = null; boolean isNew; final boolean usersHaveChanged = App.getUserManager().isDirty(); try { cursor = getCursorForFormFile(proposedPath, new String[] { FormsProviderAPI.FormsColumns.DATE }); boolean isInDatabase = cursor.getCount() > 0; if (isInDatabase) { if (cursor.getCount() != 1) { LOG.e("Saw " + cursor.getCount() + " rows for " + proposedPath.getPath()); // In a fail-fast environment we would crash here, but we will keep going // to lead the code more robust to errors in the field. } Preconditions.checkArgument(cursor.getColumnCount() == 1); cursor.moveToNext(); long existingTimestamp = cursor.getLong(0); isNew = (existingTimestamp < formInfo.dateChanged); if (isNew || usersHaveChanged) { LOG.i("Form " + formInfo.uuid + " requires an update." + " (Local creation date: " + existingTimestamp + ", (Latest version: " + formInfo.dateChanged + ")" + ", (Invalidated by UserManager: " + usersHaveChanged + ")"); } } else { LOG.i("Form " + formInfo.uuid + " not found in database."); isNew = true; } } finally { if (cursor != null) { cursor.close(); } } if (!isNew && !usersHaveChanged) { LOG.i("Using form " + formInfo.uuid + " from local cache."); if (mFormWrittenListener != null) { mFormWrittenListener.formWritten(proposedPath, formInfo.uuid); } EventBus.getDefault().post(new FetchXformSucceededEvent()); continue; } // Doesn't exist, so insert it. Fetch the file from OpenMRS fetchAndAddXFormToDb(formInfo.uuid, proposedPath); } return null; } /** * Fetches the requested xform from the server and adds it into db. * @param uuid UUID of the form to be fetched * @param proposedPath a {@link File} containing the form fields that should be * added */ public void fetchAndAddXFormToDb(final String uuid, final File proposedPath) { LOG.i("fetching form " + uuid); OpenMrsXformsConnection openMrsXformsConnection = new OpenMrsXformsConnection(App.getConnectionDetails()); openMrsXformsConnection.getXform(uuid, new Response.Listener<String>() { @Override public void onResponse(String response) { LOG.i("adding form '%s' to db", uuid); new AddFormToDbAsyncTask(mFormWrittenListener, uuid) .execute(new FormToWrite(response, proposedPath)); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "failed to fetch file"); EventBus.getDefault().post(new FetchXformFailedEvent( FetchXformFailedEvent.Reason.SERVER_FAILED_TO_FETCH, error)); } }); } /** * Get a Cursor for the form from the filename. If there is more than one they are ordered * descending by id, so most recent is first. * @param proposedPath the path for the forms file * @param projection a projection of fields to get * @return the Cursor pointing to ideally one form. */ public static Cursor getCursorForFormFile(File proposedPath, String[] projection) { String[] selectionArgs = { proposedPath.getAbsolutePath() }; String selection = FormsProviderAPI.FormsColumns.FORM_FILE_PATH + "=?"; return Collect.getInstance() .getApplication() .getContentResolver() .query(FormsProviderAPI.FormsColumns.CONTENT_URI, projection, selection, selectionArgs, FormsProviderAPI.FormsColumns._ID + " DESC"); } private static class FormToWrite { public final String form; public final File path; private FormToWrite(String form, File path) { this.form = Preconditions.checkNotNull(form); this.path = Preconditions.checkNotNull(path); } } private static class AddFormToDbAsyncTask extends AsyncTask<FormToWrite, Void, File> { private final FormWrittenListener mFormWrittenListener; private final String mUuid; private AddFormToDbAsyncTask( @Nullable FormWrittenListener formWrittenListener, String uuid) { mFormWrittenListener = formWrittenListener; mUuid = uuid; } @Override protected File doInBackground(FormToWrite[] params) { Preconditions.checkArgument(params.length != 0); String form = params[0].form; File proposedPath = params[0].path; // Write file into OpenMRS forms directory. if (!writeStringToFile(form, proposedPath)) { // we failed to load it, just skip for now return null; } // do the equivalent of DownloadFormsTask.findExistingOrCreateNewUri() or // DiskSyncTask step 4 to insert the file into the database ContentValues cv; try { cv = DiskSyncTask.buildContentValues(proposedPath); } catch (IllegalArgumentException e) { // yuck, but this is what it throws on a bad parse LOG.e(e, "Failed to parse: " + proposedPath); return null; } // insert into content provider try { ContentResolver contentResolver = Collect.getInstance().getApplication().getContentResolver(); // Always replace existing forms. cv.put(FormsProviderAPI.SQL_INSERT_OR_REPLACE, true); contentResolver.insert(FormsProviderAPI.FormsColumns.CONTENT_URI, cv); } catch (SQLException e) { LOG.i(e, "failed to insert fetched file"); } return proposedPath; } @Override protected void onPostExecute(File path) { super.onPostExecute(path); if (mFormWrittenListener != null && path != null) { mFormWrittenListener.formWritten(path, mUuid); } EventBus.getDefault().post(new FetchXformSucceededEvent()); App.getUserManager().setDirty(false); } private static boolean writeStringToFile(String response, File proposedPath) { //Create OKD dirs if necessary Collect.getInstance().createODKDirs(); FileWriter writer = null; try { writer = new FileWriter(proposedPath); writer.write(response); return true; } catch (IOException e) { LOG.e(e, "failed to write downloaded xform to ODK forms directory"); return false; } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { LOG.e(e, "failed to close writer into ODK directory"); } } } } } }