From 259f267a97f882c23be3d299549fbbe483d40bcb Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Sun, 29 Nov 2015 19:27:56 -0200 Subject: [PATCH 01/69] Extracting submit form error to a handler method and relocating fetch error handler to stay next to it. No side effects --- .../client/ui/OdkActivityLauncher.java | 111 +++++++++--------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 2aeb37a8..cee16b83 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -91,7 +91,7 @@ public static void fetchAndCacheAllXforms() { } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { - handleSyncError(error); + handleFetchSyncError(error); } }); } @@ -145,7 +145,7 @@ public static void fetchAndShowXform( }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "Fetching xform list from server failed. "); - handleSyncError(error); + handleFetchSyncError(error); } }); } @@ -229,26 +229,6 @@ private static List getLocalFormEntries() { return entries; } - private static void handleSyncError(VolleyError error) { - FetchXformFailedEvent.Reason reason = - FetchXformFailedEvent.Reason.SERVER_UNKNOWN; - if (error.networkResponse != null) { - switch (error.networkResponse.statusCode) { - case HttpURLConnection.HTTP_FORBIDDEN: - case HttpURLConnection.HTTP_UNAUTHORIZED: - reason = FetchXformFailedEvent.Reason.SERVER_AUTH; - break; - case HttpURLConnection.HTTP_NOT_FOUND: - reason = FetchXformFailedEvent.Reason.SERVER_BAD_ENDPOINT; - break; - case HttpURLConnection.HTTP_INTERNAL_ERROR: - default: - reason = FetchXformFailedEvent.Reason.SERVER_UNKNOWN; - } - } - EventBus.getDefault().post(new FetchXformFailedEvent(reason, error)); - } - /** * Launches ODK using the requested form. * @param callingActivity the {@link Activity} requesting the xform; when ODK closes, the user @@ -404,43 +384,62 @@ private static void sendFormToServer(String patientUuid, String xml, successListener, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { - LOG.e(error, "Did not submit form to server successfully"); - - SubmitXformFailedEvent.Reason reason = - SubmitXformFailedEvent.Reason.UNKNOWN; - if (error.networkResponse != null) { - switch (error.networkResponse.statusCode) { - case 401: - case 403: - reason = SubmitXformFailedEvent.Reason.SERVER_AUTH; - break; - case 404: - reason = SubmitXformFailedEvent.Reason.SERVER_BAD_ENDPOINT; - break; - case 500: - if (error.networkResponse.data == null) { - LOG.e("Server error, but no internal error stack trace " - + "available."); - } else { - LOG.e(new String( - error.networkResponse.data, Charsets.UTF_8)); - LOG.e("Server error. Internal error stack trace:\n"); - } - reason = SubmitXformFailedEvent.Reason.SERVER_ERROR; - break; - default: - reason = SubmitXformFailedEvent.Reason.SERVER_ERROR; - break; - } - } + LOG.e(error, "Error submitting form to server"); + handleSubmitSyncError(error); + } + }); + } - if (error instanceof TimeoutError) { - reason = SubmitXformFailedEvent.Reason.SERVER_TIMEOUT; + private static void handleSubmitSyncError(VolleyError error) { + SubmitXformFailedEvent.Reason reason = SubmitXformFailedEvent.Reason.UNKNOWN; + + if (error instanceof TimeoutError) { + reason = SubmitXformFailedEvent.Reason.SERVER_TIMEOUT; + } else if (error.networkResponse != null) { + switch (error.networkResponse.statusCode) { + case HttpURLConnection.HTTP_UNAUTHORIZED: + case HttpURLConnection.HTTP_FORBIDDEN: + reason = SubmitXformFailedEvent.Reason.SERVER_AUTH; + break; + case HttpURLConnection.HTTP_NOT_FOUND: + reason = SubmitXformFailedEvent.Reason.SERVER_BAD_ENDPOINT; + break; + case HttpURLConnection.HTTP_INTERNAL_ERROR: + if (error.networkResponse.data == null) { + LOG.e("Server error, but no internal error stack trace available."); + } else { + LOG.e(new String(error.networkResponse.data, Charsets.UTF_8)); + LOG.e("Server error. Internal error stack trace:\n"); } + reason = SubmitXformFailedEvent.Reason.SERVER_ERROR; + break; + default: + reason = SubmitXformFailedEvent.Reason.SERVER_ERROR; + break; + } + } - EventBus.getDefault().post(new SubmitXformFailedEvent(reason, error)); - } - }); + EventBus.getDefault().post(new SubmitXformFailedEvent(reason, error)); + } + + private static void handleFetchSyncError(VolleyError error) { + FetchXformFailedEvent.Reason reason = + FetchXformFailedEvent.Reason.SERVER_UNKNOWN; + if (error.networkResponse != null) { + switch (error.networkResponse.statusCode) { + case HttpURLConnection.HTTP_FORBIDDEN: + case HttpURLConnection.HTTP_UNAUTHORIZED: + reason = FetchXformFailedEvent.Reason.SERVER_AUTH; + break; + case HttpURLConnection.HTTP_NOT_FOUND: + reason = FetchXformFailedEvent.Reason.SERVER_BAD_ENDPOINT; + break; + case HttpURLConnection.HTTP_INTERNAL_ERROR: + default: + reason = FetchXformFailedEvent.Reason.SERVER_UNKNOWN; + } + } + EventBus.getDefault().post(new FetchXformFailedEvent(reason, error)); } private static String readFromPath(String path) throws IOException { From 5d48348abb0b97b245171dcefe03ec9e1be9e1c9 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Sun, 29 Nov 2015 23:03:35 -0200 Subject: [PATCH 02/69] Refactoring sendOdkResultToServer method to more succint and readable. There is no intention of side effects --- .../client/ui/OdkActivityLauncher.java | 218 ++++++++++++------ 1 file changed, 144 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index cee16b83..dfd0c110 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -36,7 +36,6 @@ import org.odk.collect.android.application.Collect; import org.odk.collect.android.model.Preset; import org.odk.collect.android.provider.FormsProviderAPI; -import org.odk.collect.android.provider.InstanceProviderAPI; import org.odk.collect.android.tasks.DeleteInstancesTask; import org.odk.collect.android.utilities.FileUtils; import org.projectbuendia.client.App; @@ -70,6 +69,9 @@ import de.greenrobot.event.EventBus; import static android.provider.BaseColumns._ID; +import static java.lang.String.format; +import static org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns + .CONTENT_ITEM_TYPE; import static org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns.INSTANCE_FILE_PATH; /** Convenience class for launching ODK to display an Xform. */ @@ -203,7 +205,7 @@ private static boolean loadXformFromCache(final Activity callingActivity, OpenMrsXformIndexEntry formToShow = findUuid(entries, uuidToShow); if (!formToShow.makeFileForForm().exists()) return false; - LOG.i(String.format("Using form %s from local cache.", uuidToShow)); + LOG.i(format("Using form %s from local cache.", uuidToShow)); showForm(callingActivity, requestCode, patient, fields, formToShow); return true; @@ -287,88 +289,99 @@ public static void sendOdkResultToServer( int resultCode, Intent data) { - if (resultCode == Activity.RESULT_CANCELED) return; - - if (data == null || data.getData() == null) { - // Cancelled. - LOG.i("No data for form result, probably cancelled."); - return; - } + if(isActivityCanceled(resultCode, data)) return; Uri uri = data.getData(); + if(!assertThatContentUriHasValidType(context, uri, CONTENT_ITEM_TYPE)) return; - if (!context.getContentResolver().getType(uri).equals( - InstanceProviderAPI.InstanceColumns.CONTENT_ITEM_TYPE)) { - LOG.e("Tried to load a content URI of the wrong type: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return; - } + final String filePath = getFormFilePath(context, uri); + final Long idToDelete = getIdToDeleteAfterUpload(context, uri); + if(filePath == null || idToDelete == null) return; // SubmitXformFailedEvent was already triggered + + // Temporary code for messing about with xform instance, reading values. + byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); + + // get the root of the saved and template instances + final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); + + final String xml = readFromPath(filePath); + if(xml == null) return; // SubmitXformFailedEvent was already triggered + + sendFormToServer(patientUuid, xml , + new Response.Listener() { + @Override public void onResponse(JSONObject response) { + LOG.i("Created new encounter successfully on server" + response.toString()); + + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateClientCache(patientUuid, savedRoot, context.getContentResolver()); + } + + if (!settings.getKeepFormInstancesLocally()) { + //Code largely copied from InstanceUploaderTask to delete on upload + DeleteInstancesTask dit = new DeleteInstancesTask(); + dit.setContentResolver( + Collect.getInstance().getApplication() + .getContentResolver()); + dit.execute(idToDelete); + } + EventBus.getDefault().post(new SubmitXformSucceededEvent()); + } + }); + } + /** + * Returns the form file path queried from the given {@link Uri}. If no file path was found, + * it triggers a {@link SubmitXformFailedEvent} event and returns null. + * @param context the application context + * @param uri the URI containing the form file path + */ + private static String getFormFilePath(final Context context, final Uri uri) { Cursor instanceCursor = null; try { - instanceCursor = context.getContentResolver().query(uri, - null, null, null, null); - if (instanceCursor.getCount() != 1) { - LOG.e("The form that we tried to load did not exist: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return; - } - instanceCursor.moveToFirst(); - String instancePath = instanceCursor.getString( + instanceCursor = getCursorAtRightPosition(context, uri); + if(instanceCursor == null) return null; + + String filePath = instanceCursor.getString( instanceCursor.getColumnIndex(INSTANCE_FILE_PATH)); - if (instancePath == null) { + if (filePath == null) { LOG.e("No file path for form instance: " + uri); EventBus.getDefault().post( new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return; + return null; } - int columnIndex = instanceCursor - .getColumnIndex(_ID); + + return filePath; + } finally { + if (instanceCursor != null) { + instanceCursor.close(); + } + } + } + + /** + * Returns the id to be deleted after the form upload, which was queried from the given + * {@link Uri}. If no id was found, it triggers a {@link SubmitXformFailedEvent} event and + * returns null. + * @param context the application context + * @param uri the URI containing the id to be deleted + */ + private static Long getIdToDeleteAfterUpload(final Context context, final Uri uri) { + Cursor instanceCursor = null; + try { + instanceCursor = getCursorAtRightPosition(context, uri); + if(instanceCursor == null) return null; + + int columnIndex = instanceCursor.getColumnIndex(_ID); if (columnIndex == -1) { LOG.e("No id to delete for after upload: " + uri); EventBus.getDefault().post( new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return; + return null; } - final long idToDelete = instanceCursor.getLong(columnIndex); - - // Temporary code for messing about with xform instance, reading values. - // - byte[] fileBytes = FileUtils.getFileAsBytes(new File(instancePath)); - - // get the root of the saved and template instances - final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); - - sendFormToServer(patientUuid, readFromPath(instancePath), - new Response.Listener() { - @Override public void onResponse(JSONObject response) { - LOG.i("Created new encounter successfully on server" - + response.toString()); - - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateClientCache( - patientUuid, savedRoot, context.getContentResolver()); - } - - if (!settings.getKeepFormInstancesLocally()) { - //Code largely copied from InstanceUploaderTask to delete on upload - DeleteInstancesTask dit = new DeleteInstancesTask(); - dit.setContentResolver( - Collect.getInstance().getApplication() - .getContentResolver()); - dit.execute(idToDelete); - } - EventBus.getDefault().post(new SubmitXformSucceededEvent()); - } - }); - } catch (IOException e) { - LOG.e(e, "Failed to read xml form into a String " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + + return instanceCursor.getLong(columnIndex); } finally { if (instanceCursor != null) { instanceCursor.close(); @@ -376,6 +389,51 @@ public static void sendOdkResultToServer( } } + private static Cursor getCursorAtRightPosition(final Context context, final Uri uri) { + Cursor instanceCursor = context.getContentResolver().query(uri, null, null, null, null); + if (instanceCursor.getCount() != 1) { + LOG.e("The form that we tried to load did not exist: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return null; + } + instanceCursor.moveToFirst(); + + return instanceCursor; + } + + /** + * Checks if the URI has a valid type. If so, returns true. Otherwise, triggers a + * SubmitXformFailedEvent event and returns false + * @param context the application context + * @param uri the URI to be checked + * @param validType the accepted type for URI + */ + private static boolean assertThatContentUriHasValidType(final Context context, final Uri uri, + final String validType) { + if (!context.getContentResolver().getType(uri).equals(validType)) { + LOG.e("Tried to load a content URI of the wrong type: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } + return true; + } + + /** + * Returns true if the activity was canceled + * @param resultCode the result code sent from Android activity transition + * @param data the incoming intent + */ + private static boolean isActivityCanceled(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_CANCELED) return true; + if (data == null || data.getData() == null) { + LOG.i("No data for form result, probably cancelled."); + return true; + } + return false; + } + private static void sendFormToServer(String patientUuid, String xml, Response.Listener successListener) { OpenMrsXformsConnection connection = @@ -442,14 +500,26 @@ private static void handleFetchSyncError(VolleyError error) { EventBus.getDefault().post(new FetchXformFailedEvent(reason, error)); } - private static String readFromPath(String path) throws IOException { - StringBuilder sb = new StringBuilder(); - BufferedReader reader = new BufferedReader(new FileReader(path)); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); + /** + * Returns the xml form as a String from the path. If for any reason, the file couldn't be read, + * it triggers {@link SubmitXformFailedEvent} and returns null + * @param path the path to be read + */ + private static String readFromPath(String path) { + try { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = new BufferedReader(new FileReader(path)); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + return sb.toString(); + } catch (IOException e) { + LOG.e(e, format("Failed to read xml form into a String. FilePath= ", path)); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return null; } - return sb.toString(); } private static void updateClientCache(String patientUuid, TreeElement savedRoot, From e7ad26cd80b3a12464477dc9886e3a20b6526794 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Sun, 29 Nov 2015 23:10:22 -0200 Subject: [PATCH 03/69] Commenting getCursorAtRightPosition metahod --- .../org/projectbuendia/client/ui/OdkActivityLauncher.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index dfd0c110..9fb0c9b6 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -389,6 +389,12 @@ private static Long getIdToDeleteAfterUpload(final Context context, final Uri ur } } + /** + * Returns the form {@link Cursor} ready to be used. If no form was found, it triggers a + * {@link SubmitXformFailedEvent} event and returns null. + * @param context the application context + * @param uri the URI to be queried + */ private static Cursor getCursorAtRightPosition(final Context context, final Uri uri) { Cursor instanceCursor = context.getContentResolver().query(uri, null, null, null, null); if (instanceCursor.getCount() != 1) { From 28e84dfe2ab3228e26adc37bad95d7323b0b1007 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Mon, 30 Nov 2015 00:43:18 -0200 Subject: [PATCH 04/69] Refactoring sendFormToServer call to be more succint. No side effects --- .../client/ui/OdkActivityLauncher.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 9fb0c9b6..a96164da 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -318,18 +318,28 @@ public static void sendOdkResultToServer( } if (!settings.getKeepFormInstancesLocally()) { - //Code largely copied from InstanceUploaderTask to delete on upload - DeleteInstancesTask dit = new DeleteInstancesTask(); - dit.setContentResolver( - Collect.getInstance().getApplication() - .getContentResolver()); - dit.execute(idToDelete); + deleteFormInstances(idToDelete); } EventBus.getDefault().post(new SubmitXformSucceededEvent()); } + + }, new Response.ErrorListener() { + @Override public void onErrorResponse(VolleyError error) { + LOG.e(error, "Error submitting form to server"); + handleSubmitSyncError(error); + } }); } + private static void deleteFormInstances(Long formIdToDelete) { + //Code largely copied from InstanceUploaderTask to delete on upload + DeleteInstancesTask dit = new DeleteInstancesTask(); + dit.setContentResolver( + Collect.getInstance().getApplication() + .getContentResolver()); + dit.execute(formIdToDelete); + } + /** * Returns the form file path queried from the given {@link Uri}. If no file path was found, * it triggers a {@link SubmitXformFailedEvent} event and returns null. @@ -441,17 +451,11 @@ private static boolean isActivityCanceled(int resultCode, Intent data) { } private static void sendFormToServer(String patientUuid, String xml, - Response.Listener successListener) { + Response.Listener successListener, + Response.ErrorListener errorListener) { OpenMrsXformsConnection connection = new OpenMrsXformsConnection(App.getConnectionDetails()); - connection.postXformInstance(patientUuid, xml, - successListener, - new Response.ErrorListener() { - @Override public void onErrorResponse(VolleyError error) { - LOG.e(error, "Error submitting form to server"); - handleSubmitSyncError(error); - } - }); + connection.postXformInstance(patientUuid, xml, successListener, errorListener); } private static void handleSubmitSyncError(VolleyError error) { From a75e2681f62007aaeac07588b89b7ce3704f028c Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Mon, 30 Nov 2015 00:45:29 -0200 Subject: [PATCH 05/69] Improving variable name to be more readable --- .../projectbuendia/client/ui/OdkActivityLauncher.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index a96164da..1680235c 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -295,8 +295,8 @@ public static void sendOdkResultToServer( if(!assertThatContentUriHasValidType(context, uri, CONTENT_ITEM_TYPE)) return; final String filePath = getFormFilePath(context, uri); - final Long idToDelete = getIdToDeleteAfterUpload(context, uri); - if(filePath == null || idToDelete == null) return; // SubmitXformFailedEvent was already triggered + final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); + if(filePath == null || formIdToDelete == null) return; // SubmitXformFailedEvent was already triggered // Temporary code for messing about with xform instance, reading values. byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); @@ -318,11 +318,10 @@ public static void sendOdkResultToServer( } if (!settings.getKeepFormInstancesLocally()) { - deleteFormInstances(idToDelete); + deleteLocalFormInstances(formIdToDelete); } EventBus.getDefault().post(new SubmitXformSucceededEvent()); } - }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "Error submitting form to server"); @@ -331,7 +330,7 @@ public static void sendOdkResultToServer( }); } - private static void deleteFormInstances(Long formIdToDelete) { + private static void deleteLocalFormInstances(Long formIdToDelete) { //Code largely copied from InstanceUploaderTask to delete on upload DeleteInstancesTask dit = new DeleteInstancesTask(); dit.setContentResolver( From e7847aef3230dc939ceee25acf44b2971a993244 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Mon, 30 Nov 2015 02:30:23 -0200 Subject: [PATCH 06/69] Refactoring updateClientCache method. It was too long and to hard to comprehend. It was necessary to debug the entire method to know what it was doing. It is much more readable now. No side effects --- .../client/ui/OdkActivityLauncher.java | 176 ++++++++++-------- 1 file changed, 96 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 1680235c..9d0e0398 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -61,6 +61,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; @@ -307,16 +308,11 @@ public static void sendOdkResultToServer( final String xml = readFromPath(filePath); if(xml == null) return; // SubmitXformFailedEvent was already triggered - sendFormToServer(patientUuid, xml , + sendFormToServer(patientUuid, xml, new Response.Listener() { @Override public void onResponse(JSONObject response) { LOG.i("Created new encounter successfully on server" + response.toString()); - - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateClientCache(patientUuid, savedRoot, context.getContentResolver()); - } - + updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); if (!settings.getKeepFormInstancesLocally()) { deleteLocalFormInstances(formIdToDelete); } @@ -531,28 +527,14 @@ private static String readFromPath(String path) { } } - private static void updateClientCache(String patientUuid, TreeElement savedRoot, - ContentResolver resolver) { - // id, fill in auto - // patient uuid: context - // encounter uuid: make one up - // encounter time: - // - // 2014-12-15T13:33:00.000Z - // concept uuid: - // - // - // - // value: - // - // - // 36.0 - // temp_cache: true - - // or for coded - // - // - // 1066^NO^99DCT + /** + * Caches the observation changes locally for a given patient. + * For a new patient (patientUuid == null), no information is cached. + */ + private static void updateObservationCache(@Nullable String patientUuid, TreeElement savedRoot, + ContentResolver resolver) { + // Only locally cache new observations, not new patients. + if (patientUuid == null) return; ContentValues common = new ContentValues(); // It's critical that UUID is {@code null} for temporary observations, so we make it @@ -560,32 +542,63 @@ private static void updateClientCache(String patientUuid, TreeElement savedRoot, common.put(Contracts.Observations.UUID, (String) null); common.put(Contracts.Observations.PATIENT_UUID, patientUuid); - TreeElement encounter = savedRoot.getChild("encounter", 0); - if (encounter == null) { - LOG.e("No encounter found in instance"); - return; - } + final DateTime encounterTime = getEncounterAnswerDateTime(savedRoot); + if(encounterTime == null) return; + common.put(Contracts.Observations.ENCOUNTER_MILLIS, encounterTime.getMillis()); + common.put(Contracts.Observations.ENCOUNTER_UUID, UUID.randomUUID().toString()); - TreeElement encounterDatetime = - encounter.getChild("encounter.encounter_datetime", 0); - if (encounterDatetime == null) { - LOG.e("No encounter date time found in instance"); - return; + Set xformConceptIds = new HashSet<>(); + List toInsert = getAnsweredObservations(common, savedRoot, xformConceptIds); + Map xformIdToUuid = mapFormConceptIdToUuid(xformConceptIds, resolver); + + // Remap concept ids to uuids, skipping anything we can't remap. + for (Iterator i = toInsert.iterator(); i.hasNext(); ) { + ContentValues values = i.next(); + if (!mapIdToUuid(xformIdToUuid, values, Contracts.Observations.CONCEPT_UUID)) { + i.remove(); + } + mapIdToUuid(xformIdToUuid, values, Contracts.Observations.VALUE); } - IAnswerData dateTimeValue = encounterDatetime.getValue(); - try { - DateTime encounterTime = - ISODateTimeFormat.dateTime().parseDateTime((String) dateTimeValue.getValue()); - common.put(Contracts.Observations.ENCOUNTER_MILLIS, encounterTime.getMillis()); - common.put(Contracts.Observations.ENCOUNTER_UUID, UUID.randomUUID().toString()); - } catch (IllegalArgumentException e) { - LOG.e("Could not parse datetime" + dateTimeValue.getValue()); - return; + resolver.bulkInsert(Contracts.Observations.CONTENT_URI, + toInsert.toArray(new ContentValues[toInsert.size()])); + } + + /** Get a map from XForm ids to UUIDs from our local concept database. */ + private static Map mapFormConceptIdToUuid(Set xformConceptIds, + ContentResolver resolver) { + String inClause = Joiner.on(",").join(xformConceptIds); + + HashMap xformIdToUuid = new HashMap<>(); + Cursor cursor = resolver.query(Contracts.Concepts.CONTENT_URI, + new String[] {Contracts.Concepts.UUID, Contracts.Concepts.XFORM_ID}, + Contracts.Concepts.XFORM_ID + " IN (" + inClause + ")", + null, null); + + try { + while (cursor.moveToNext()) { + xformIdToUuid.put(Utils.getString(cursor, Contracts.Concepts.XFORM_ID), + Utils.getString(cursor, Contracts.Concepts.UUID)); + } + } finally { + cursor.close(); } - ArrayList toInsert = new ArrayList<>(); - HashSet xformConceptIds = new HashSet<>(); + return xformIdToUuid; + } + + /** + * Returns a {@link ContentValues} list containing the id concept and the answer valeu from + * all answered observations. Returns a empty {@link List} if no observation was answered. + * + * @param common the current content values. + * @param savedRoot the root tree form element + * @param xformConceptIdsAccumulator the set to store the form concept ids found + */ + private static List getAnsweredObservations(ContentValues common, + TreeElement savedRoot, + Set xformConceptIdsAccumulator) { + List answeredObservations = new ArrayList<>(); for (int i = 0; i < savedRoot.getNumChildren(); i++) { TreeElement group = savedRoot.getChildAt(i); if (group.getNumChildren() == 0) continue; @@ -594,20 +607,22 @@ private static void updateClientCache(String patientUuid, TreeElement savedRoot, TreeElement openmrsConcept = question.getAttribute(null, "openmrs_concept"); TreeElement openmrsDatatype = question.getAttribute(null, "openmrs_datatype"); if (openmrsConcept == null || openmrsDatatype == null) continue; + // Get the concept for the question. // eg "5088^Temperature (C)^99DCT" String encodedConcept = (String) openmrsConcept.getValue().getValue(); - Integer id = getConceptId(xformConceptIds, encodedConcept); + Integer id = getConceptId(xformConceptIdsAccumulator, encodedConcept); if (id == null) continue; + // Also get for the answer if a coded question - String value; TreeElement valueChild = question.getChild("value", 0); IAnswerData answer = valueChild.getValue(); - if (answer == null) continue; + if (answer == null || answer.getValue() == null) continue; + Object answerObject = answer.getValue(); - if (answerObject == null) continue; + String value; if ("CWE".equals(openmrsDatatype.getValue().getValue())) { - value = getConceptId(xformConceptIds, answerObject.toString()).toString(); + value = getConceptId(xformConceptIdsAccumulator, answerObject.toString()).toString(); } else { value = answerObject.toString(); } @@ -616,36 +631,37 @@ private static void updateClientCache(String patientUuid, TreeElement savedRoot, // Set to the id for now, we'll replace with uuid later observation.put(Contracts.Observations.CONCEPT_UUID, id.toString()); observation.put(Contracts.Observations.VALUE, value); - toInsert.add(observation); + + answeredObservations.add(observation); } } + return answeredObservations; + } - String inClause = Joiner.on(",").join(xformConceptIds); - // Get a map from XForm ids to UUIDs from our local concept database. - HashMap xformIdToUuid = new HashMap<>(); - Cursor cursor = resolver.query(Contracts.Concepts.CONTENT_URI, - new String[] {Contracts.Concepts.UUID, Contracts.Concepts.XFORM_ID}, - Contracts.Concepts.XFORM_ID + " IN (" + inClause + ")", - null, null); - try { - while (cursor.moveToNext()) { - xformIdToUuid.put(Utils.getString(cursor, Contracts.Concepts.XFORM_ID), - Utils.getString(cursor, Contracts.Concepts.UUID)); - } - } finally { - cursor.close(); + /** + * Returns the encounter's answer date time. Returns null if it cannot be retrieved. + */ + private static DateTime getEncounterAnswerDateTime(TreeElement root) { + TreeElement encounter = root.getChild("encounter", 0); + if (encounter == null) { + LOG.e("No encounter found in instance"); + return null; } - // Remap concept ids to uuids, skipping anything we can't remap. - for (Iterator i = toInsert.iterator(); i.hasNext(); ) { - ContentValues values = i.next(); - if (!mapIdToUuid(xformIdToUuid, values, Contracts.Observations.CONCEPT_UUID)) { - i.remove(); - } - mapIdToUuid(xformIdToUuid, values, Contracts.Observations.VALUE); + TreeElement encounterDatetime = + encounter.getChild("encounter.encounter_datetime", 0); + if (encounterDatetime == null) { + LOG.e("No encounter date time found in instance"); + return null; + } + + IAnswerData dateTimeValue = encounterDatetime.getValue(); + try { + return ISODateTimeFormat.dateTime().parseDateTime((String) dateTimeValue.getValue()); + } catch (IllegalArgumentException e) { + LOG.e("Could not parse datetime" + dateTimeValue.getValue()); + return null; } - resolver.bulkInsert(Contracts.Observations.CONTENT_URI, - toInsert.toArray(new ContentValues[toInsert.size()])); } private static Integer getConceptId(Set accumulator, String encodedConcept) { @@ -657,7 +673,7 @@ private static Integer getConceptId(Set accumulator, String encodedConc } private static boolean mapIdToUuid( - HashMap idToUuid, ContentValues values, String key) { + Map idToUuid, ContentValues values, String key) { String id = (String) values.get(key); String uuid = idToUuid.get(id); if (uuid == null) { From 35dd211193dab86520a28852a852d6992873ed3e Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Tue, 1 Dec 2015 08:51:12 -0200 Subject: [PATCH 07/69] OpenMrsJsonRequest was creating a response listener only to delegate call to an already received successListener. So instead of create one, it has just to use the received one --- .../client/net/OpenMrsXformsConnection.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/net/OpenMrsXformsConnection.java b/app/src/main/java/org/projectbuendia/client/net/OpenMrsXformsConnection.java index 27a74d26..716003d5 100644 --- a/app/src/main/java/org/projectbuendia/client/net/OpenMrsXformsConnection.java +++ b/app/src/main/java/org/projectbuendia/client/net/OpenMrsXformsConnection.java @@ -129,14 +129,14 @@ public void listXforms(final Response.Listener> lis * Send a single Xform to the OpenMRS server. * @param patientUuid null if this is to add a new patient, non-null for observation on existing * patient - * @param resultListener the listener to be informed of the form asynchronously + * @param successListener the listener to be informed of the form asynchronously * @param errorListener a listener to be informed of any errors */ public void postXformInstance( - @Nullable String patientUuid, - String xform, - final Response.Listener resultListener, - Response.ErrorListener errorListener) { + final @Nullable String patientUuid, + final String xform, + final Response.Listener successListener, + final Response.ErrorListener errorListener) { // The JsonObject members in the API as written at the moment. // int "patient_id" @@ -163,11 +163,8 @@ public void postXformInstance( OpenMrsJsonRequest request = new OpenMrsJsonRequest( mConnectionDetails, "/xforminstances", postBody, // non-null implies POST - new Response.Listener() { - @Override public void onResponse(JSONObject response) { - resultListener.onResponse(response); - } - }, errorListener + successListener, + errorListener ); // Set a permissive timeout. request.setRetryPolicy(new DefaultRetryPolicy(Common.REQUEST_TIMEOUT_MS_MEDIUM, 1, 1f)); From 2f46d8aa26df455ffdb4d0b8bc2f2011f175ecaf Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 2 Dec 2015 01:09:57 -0200 Subject: [PATCH 08/69] Adjusments based on Code Review --- .../client/ui/OdkActivityLauncher.java | 140 +++++++++++------- 1 file changed, 85 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 9d0e0398..420097d2 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -94,7 +94,7 @@ public static void fetchAndCacheAllXforms() { } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { - handleFetchSyncError(error); + handleFetchError(error); } }); } @@ -148,7 +148,7 @@ public static void fetchAndShowXform( }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "Fetching xform list from server failed. "); - handleFetchSyncError(error); + handleFetchError(error); } }); } @@ -293,11 +293,13 @@ public static void sendOdkResultToServer( if(isActivityCanceled(resultCode, data)) return; Uri uri = data.getData(); - if(!assertThatContentUriHasValidType(context, uri, CONTENT_ITEM_TYPE)) return; + if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) return; final String filePath = getFormFilePath(context, uri); + if(!validateFilePath(filePath, uri)) return; + final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); - if(filePath == null || formIdToDelete == null) return; // SubmitXformFailedEvent was already triggered + if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) return; // Temporary code for messing about with xform instance, reading values. byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); @@ -306,13 +308,16 @@ public static void sendOdkResultToServer( final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); final String xml = readFromPath(filePath); - if(xml == null) return; // SubmitXformFailedEvent was already triggered + if(!validateXml(xml)) return; sendFormToServer(patientUuid, xml, new Response.Listener() { @Override public void onResponse(JSONObject response) { LOG.i("Created new encounter successfully on server" + response.toString()); - updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); + } if (!settings.getKeepFormInstancesLocally()) { deleteLocalFormInstances(formIdToDelete); } @@ -321,11 +326,76 @@ public static void sendOdkResultToServer( }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "Error submitting form to server"); - handleSubmitSyncError(error); + handleSubmitError(error); } }); } + /** + * Checks if the file path is valid. If so, it returns true. Otherwise + * it triggers a {@link SubmitXformFailedEvent} event and returns false. + * @param filePath the file path to be validated + * @param uri the form uri + */ + private static boolean validateFilePath(String filePath, Uri uri) { + if (filePath == null) { + LOG.e("No file path for form instance: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } + return true; + } + + /** + * Checks if the URI has a valid type. If so, returns true. Otherwise, triggers a + * SubmitXformFailedEvent event and returns false + * @param context the application context + * @param uri the URI to be checked + * @param validType the accepted type for URI + */ + private static boolean validateContentUriType(final Context context, final Uri uri, + final String validType) { + if (!context.getContentResolver().getType(uri).equals(validType)) { + LOG.e("Tried to load a content URI of the wrong type: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } + return true; + } + + /** + * Validates the id to be deleted after the form upload. If id is valid, it returns + * true. Otherwise, it triggers * {@link SubmitXformFailedEvent} event and + * returns false. + * @param context the application context + * @param uri the URI containing the id to be deleted + */ + private static boolean validateIdToDeleteAfterUpload(final Long id, Uri uri) { + if (id == null) { + LOG.e("No id to delete for after upload: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } + return true; + } + + /** + * Validates the xml. Returns true if it is valid. Otherwise, it triggers + * {@link SubmitXformFailedEvent} and returns false + */ + private static boolean validateXml(String xml) { + if(xml == null) { + LOG.e("Xml form is not valid."); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } + return true; + } + private static void deleteLocalFormInstances(Long formIdToDelete) { //Code largely copied from InstanceUploaderTask to delete on upload DeleteInstancesTask dit = new DeleteInstancesTask(); @@ -337,7 +407,7 @@ private static void deleteLocalFormInstances(Long formIdToDelete) { /** * Returns the form file path queried from the given {@link Uri}. If no file path was found, - * it triggers a {@link SubmitXformFailedEvent} event and returns null. + * it returns null. * @param context the application context * @param uri the URI containing the form file path */ @@ -347,17 +417,7 @@ private static String getFormFilePath(final Context context, final Uri uri) { instanceCursor = getCursorAtRightPosition(context, uri); if(instanceCursor == null) return null; - String filePath = instanceCursor.getString( - instanceCursor.getColumnIndex(INSTANCE_FILE_PATH)); - if (filePath == null) { - LOG.e("No file path for form instance: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return null; - - } - - return filePath; + return instanceCursor.getString(instanceCursor.getColumnIndex(INSTANCE_FILE_PATH)); } finally { if (instanceCursor != null) { instanceCursor.close(); @@ -367,8 +427,7 @@ private static String getFormFilePath(final Context context, final Uri uri) { /** * Returns the id to be deleted after the form upload, which was queried from the given - * {@link Uri}. If no id was found, it triggers a {@link SubmitXformFailedEvent} event and - * returns null. + * {@link Uri}. If no id was found, it returns null. * @param context the application context * @param uri the URI containing the id to be deleted */ @@ -379,12 +438,7 @@ private static Long getIdToDeleteAfterUpload(final Context context, final Uri ur if(instanceCursor == null) return null; int columnIndex = instanceCursor.getColumnIndex(_ID); - if (columnIndex == -1) { - LOG.e("No id to delete for after upload: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return null; - } + if (columnIndex == -1) return null; return instanceCursor.getLong(columnIndex); } finally { @@ -413,24 +467,6 @@ private static Cursor getCursorAtRightPosition(final Context context, final Uri return instanceCursor; } - /** - * Checks if the URI has a valid type. If so, returns true. Otherwise, triggers a - * SubmitXformFailedEvent event and returns false - * @param context the application context - * @param uri the URI to be checked - * @param validType the accepted type for URI - */ - private static boolean assertThatContentUriHasValidType(final Context context, final Uri uri, - final String validType) { - if (!context.getContentResolver().getType(uri).equals(validType)) { - LOG.e("Tried to load a content URI of the wrong type: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } - return true; - } - /** * Returns true if the activity was canceled * @param resultCode the result code sent from Android activity transition @@ -453,7 +489,7 @@ private static void sendFormToServer(String patientUuid, String xml, connection.postXformInstance(patientUuid, xml, successListener, errorListener); } - private static void handleSubmitSyncError(VolleyError error) { + private static void handleSubmitError(VolleyError error) { SubmitXformFailedEvent.Reason reason = SubmitXformFailedEvent.Reason.UNKNOWN; if (error instanceof TimeoutError) { @@ -485,7 +521,7 @@ private static void handleSubmitSyncError(VolleyError error) { EventBus.getDefault().post(new SubmitXformFailedEvent(reason, error)); } - private static void handleFetchSyncError(VolleyError error) { + private static void handleFetchError(VolleyError error) { FetchXformFailedEvent.Reason reason = FetchXformFailedEvent.Reason.SERVER_UNKNOWN; if (error.networkResponse != null) { @@ -507,7 +543,7 @@ private static void handleFetchSyncError(VolleyError error) { /** * Returns the xml form as a String from the path. If for any reason, the file couldn't be read, - * it triggers {@link SubmitXformFailedEvent} and returns null + * it returns null * @param path the path to be read */ private static String readFromPath(String path) { @@ -521,21 +557,15 @@ private static String readFromPath(String path) { return sb.toString(); } catch (IOException e) { LOG.e(e, format("Failed to read xml form into a String. FilePath= ", path)); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); return null; } } /** * Caches the observation changes locally for a given patient. - * For a new patient (patientUuid == null), no information is cached. */ - private static void updateObservationCache(@Nullable String patientUuid, TreeElement savedRoot, + private static void updateObservationCache(String patientUuid, TreeElement savedRoot, ContentResolver resolver) { - // Only locally cache new observations, not new patients. - if (patientUuid == null) return; - ContentValues common = new ContentValues(); // It's critical that UUID is {@code null} for temporary observations, so we make it // explicit here. See {@link Contracts.Observations.UUID} for details. From 00a9c4fd192462dd78186c56b9cfd4eea326af01 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 2 Dec 2015 02:08:35 -0200 Subject: [PATCH 09/69] Fixing the javadoc description --- .../java/org/projectbuendia/client/ui/OdkActivityLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 420097d2..e0d691f5 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -369,7 +369,7 @@ private static boolean validateContentUriType(final Context context, final Uri u * Validates the id to be deleted after the form upload. If id is valid, it returns * true. Otherwise, it triggers * {@link SubmitXformFailedEvent} event and * returns false. - * @param context the application context + * @param id the id to be deleted * @param uri the URI containing the id to be deleted */ private static boolean validateIdToDeleteAfterUpload(final Long id, Uri uri) { From 4de0f186e8b44e15ba0dd4cae045c34318af4411 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 2 Dec 2015 22:29:50 -0200 Subject: [PATCH 10/69] If submission is canceled, there is no need to try submiting it --- .../client/ui/chart/PatientChartController.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java index 4a6b4267..1dbc3a13 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java @@ -276,14 +276,14 @@ public void onXFormResult(int requestCode, int resultCode, Intent data) { return; } - boolean shouldShowSubmissionDialog = (resultCode != Activity.RESULT_CANCELED); - String action = (resultCode == Activity.RESULT_CANCELED) - ? "form_discard_pressed" : "form_save_pressed"; - Utils.logUserAction(action, - "form", request.formUuid, - "patient_uuid", request.patientUuid); + boolean isSubmissionCanceled = (resultCode == Activity.RESULT_CANCELED); + Utils.logUserAction(isSubmissionCanceled ? "form_discard_pressed" : "form_save_pressed", + "form", request.formUuid, "patient_uuid", request.patientUuid); + + if(isSubmissionCanceled) return; + mOdkResultSender.sendOdkResultToServer(request.patientUuid, resultCode, data); - mUi.showFormSubmissionDialog(shouldShowSubmissionDialog); + mUi.showFormSubmissionDialog(true); } FormRequest popFormRequest(int requestIndex) { From 67861a07ffbfa19f30727e26fde3d1967f8aa7fe Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Thu, 3 Dec 2015 10:27:06 -0200 Subject: [PATCH 11/69] Refactoring form submssion to save it locally, whether or not it was successfully sent to the server --- .../client/providers/Contracts.java | 9 +++-- .../projectbuendia/client/sync/Database.java | 1 + .../ObservationsSyncPhaseRunnable.java | 4 +- .../client/ui/OdkActivityLauncher.java | 37 ++++++++++++------- .../client/ui/chart/PatientChartActivity.java | 7 ++-- .../ui/chart/PatientChartController.java | 16 ++++---- 6 files changed, 45 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/providers/Contracts.java b/app/src/main/java/org/projectbuendia/client/providers/Contracts.java index 53bbb305..123fa58c 100644 --- a/app/src/main/java/org/projectbuendia/client/providers/Contracts.java +++ b/app/src/main/java/org/projectbuendia/client/providers/Contracts.java @@ -165,9 +165,11 @@ public interface Observations { /** * UUID is populated if the record was retrieved from the server. If this observation was - * written locally as a cached value from a submitted XForm, UUID is null. As part of every - * successful sync, all observations with null UUIDs are deleted, on the basis that an - * authoritative version for each has been obtained from the server. + * written locally as a cached value, UUID is null. But the cached record may not be + * submitted to the server yet. So SUBMITTED flags if it was submitted indeed. As part of + * every successful sync, all observations with **null UUIDs and SUBMITTED == true** are + * deleted, on the basis that an authoritative version for each has been obtained from the + * server. */ String UUID = "uuid"; String PATIENT_UUID = "patient_uuid"; @@ -175,6 +177,7 @@ public interface Observations { String ENCOUNTER_MILLIS = "encounter_millis"; // milliseconds since epoch String CONCEPT_UUID = "concept_uuid"; String VALUE = "value"; // concept value or order UUID + String SUBMITTED = "submitted"; //indicates if the record was already submitted to the server } public interface Orders { diff --git a/app/src/main/java/org/projectbuendia/client/sync/Database.java b/app/src/main/java/org/projectbuendia/client/sync/Database.java index b3c646ae..2e39ae98 100644 --- a/app/src/main/java/org/projectbuendia/client/sync/Database.java +++ b/app/src/main/java/org/projectbuendia/client/sync/Database.java @@ -122,6 +122,7 @@ public class Database extends SQLiteOpenHelper { + "encounter_millis INTEGER," + "concept_uuid INTEGER," + "value STRING," + + "submitted INTEGER," + "UNIQUE (patient_uuid, encounter_uuid, concept_uuid)"); SCHEMAS.put(Table.ORDERS, "" diff --git a/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java b/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java index eaf9faa5..0712f380 100644 --- a/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java +++ b/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java @@ -24,6 +24,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static java.lang.String.format; + /** * Handles syncing observations. Uses an incremental sync mechanism. */ @@ -53,7 +55,7 @@ public void sync(ContentResolver contentResolver, // Remove all temporary observations now we have the real ones providerClient.delete(Observations.CONTENT_URI, - Observations.UUID + " IS NULL", + format("%s IS NULL AND %s == 1", Observations.UUID, Observations.SUBMITTED), new String[0]); timingLogger.addSplit("delete temp observations"); timingLogger.dumpToLog(); diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index e0d691f5..3596d5c2 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -276,30 +276,36 @@ private static OpenMrsXformIndexEntry findUuid( } /** - * Convenient shared code for handling an ODK activity result. + * Convenient shared code for handling an ODK activity result. This method submits the ODK form + * to the server and saves it locally, whether or not the form was successfully submitted. + * If an error occurs over the submission, the form is kept to be resubmitted later. + * See link(TODO:which?). This method returns {@code true} if it tries to send a request + * to the server, successfully or not. If any error occurs before submission, it returns + * {@code false}. + * * @param context the application context * @param settings the application settings * @param patientUuid the patient to add an observation to, or null to create a new patient * @param resultCode the result code sent from Android activity transition * @param data the incoming intent */ - public static void sendOdkResultToServer( + public static boolean sendOdkResultToServer( final Context context, final AppSettings settings, @Nullable final String patientUuid, int resultCode, Intent data) { - if(isActivityCanceled(resultCode, data)) return; + if(isActivityCanceled(resultCode, data)) return false; Uri uri = data.getData(); - if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) return; + if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) return false; final String filePath = getFormFilePath(context, uri); - if(!validateFilePath(filePath, uri)) return; + if(!validateFilePath(filePath, uri)) return false; final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); - if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) return; + if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) return false; // Temporary code for messing about with xform instance, reading values. byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); @@ -308,16 +314,18 @@ public static void sendOdkResultToServer( final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); final String xml = readFromPath(filePath); - if(!validateXml(xml)) return; + if(!validateXml(xml)) return false; + + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); + return false; + } sendFormToServer(patientUuid, xml, new Response.Listener() { @Override public void onResponse(JSONObject response) { LOG.i("Created new encounter successfully on server" + response.toString()); - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); - } if (!settings.getKeepFormInstancesLocally()) { deleteLocalFormInstances(formIdToDelete); } @@ -329,6 +337,7 @@ public static void sendOdkResultToServer( handleSubmitError(error); } }); + return true; } /** @@ -567,10 +576,12 @@ private static String readFromPath(String path) { private static void updateObservationCache(String patientUuid, TreeElement savedRoot, ContentResolver resolver) { ContentValues common = new ContentValues(); - // It's critical that UUID is {@code null} for temporary observations, so we make it - // explicit here. See {@link Contracts.Observations.UUID} for details. + // It's critical that UUID is {@code null} and SUBMITTED is {@code false} for temporary + // observations, so we make it explicit here. See {@link Contracts.Observations.UUID} + // and {@link Contracts.Observations.SUBMITTED} for details. common.put(Contracts.Observations.UUID, (String) null); common.put(Contracts.Observations.PATIENT_UUID, patientUuid); + common.put(Contracts.Observations.SUBMITTED, false); final DateTime encounterTime = getEncounterAnswerDateTime(savedRoot); if(encounterTime == null) return; diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java index 4d19642c..c1ccae75 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java @@ -218,10 +218,9 @@ public static void start(Context caller, String uuid) { mChartRenderer = new ChartRenderer(mGridWebView, getResources()); final OdkResultSender odkResultSender = new OdkResultSender() { - @Override public void sendOdkResultToServer(String patientUuid, int resultCode, Intent data) { - OdkActivityLauncher.sendOdkResultToServer( - PatientChartActivity.this, mSettings, - patientUuid, resultCode, data); + @Override public boolean sendOdkResultToServer(String patientUuid, int resultCode, Intent data) { + return OdkActivityLauncher.sendOdkResultToServer(PatientChartActivity.this, + mSettings, patientUuid, resultCode, data); } }; final MinimalHandler minimalHandler = new MinimalHandler() { diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java index 1dbc3a13..e4152f38 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java @@ -174,7 +174,7 @@ void showOrderExecutionDialog(org.projectbuendia.client.sync.Order order, Interv /** Sends ODK form data. */ public interface OdkResultSender { - void sendOdkResultToServer( + boolean sendOdkResultToServer( @Nullable String patientUuid, int resultCode, Intent data); @@ -269,21 +269,21 @@ public void suspend() { } } - public void onXFormResult(int requestCode, int resultCode, Intent data) { - FormRequest request = popFormRequest(requestCode); - if (request == null) { + public void onXFormResult(final int requestCode, final int resultCode, final Intent data) { + final FormRequest request = popFormRequest(requestCode); + if (request == null) { LOG.e("Unknown form request code: " + requestCode); return; } - boolean isSubmissionCanceled = (resultCode == Activity.RESULT_CANCELED); + final boolean isSubmissionCanceled = (resultCode == Activity.RESULT_CANCELED); Utils.logUserAction(isSubmissionCanceled ? "form_discard_pressed" : "form_save_pressed", "form", request.formUuid, "patient_uuid", request.patientUuid); - if(isSubmissionCanceled) return; - mOdkResultSender.sendOdkResultToServer(request.patientUuid, resultCode, data); - mUi.showFormSubmissionDialog(true); + final boolean isSubmittingForm = mOdkResultSender.sendOdkResultToServer(request.patientUuid, + resultCode, data); + mUi.showFormSubmissionDialog(isSubmittingForm); } FormRequest popFormRequest(int requestIndex) { From 3062cb9612e47e8b7b0ad37faa79bb4ed5d48721 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Thu, 3 Dec 2015 11:42:21 -0200 Subject: [PATCH 12/69] Refactoring sendOdkResultToServer to trigger failed events rather then inside validation methods --- .../client/ui/OdkActivityLauncher.java | 75 +++++++++---------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 3596d5c2..3ce6b958 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -298,14 +298,29 @@ public static boolean sendOdkResultToServer( if(isActivityCanceled(resultCode, data)) return false; - Uri uri = data.getData(); - if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) return false; + final Uri uri = data.getData(); + if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) { + LOG.e("Tried to load a content URI of the wrong type: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } final String filePath = getFormFilePath(context, uri); - if(!validateFilePath(filePath, uri)) return false; + if(!validateFilePath(filePath, uri)) { + LOG.e("No file path for form instance: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); - if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) return false; + if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) { + LOG.e("No id to delete for after upload: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } // Temporary code for messing about with xform instance, reading values. byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); @@ -314,7 +329,12 @@ public static boolean sendOdkResultToServer( final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); final String xml = readFromPath(filePath); - if(!validateXml(xml)) return false; + if(!validateXml(xml)) { + LOG.e("Xml form is not valid."); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } // Only locally cache new observations, not new patients. if (patientUuid != null) { @@ -341,68 +361,41 @@ public static boolean sendOdkResultToServer( } /** - * Checks if the file path is valid. If so, it returns true. Otherwise - * it triggers a {@link SubmitXformFailedEvent} event and returns false. + * Checks if the file path is valid. If so, it returns {@code true}. Otherwise returns + * false. * @param filePath the file path to be validated * @param uri the form uri */ private static boolean validateFilePath(String filePath, Uri uri) { - if (filePath == null) { - LOG.e("No file path for form instance: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } - return true; + return filePath != null; } /** - * Checks if the URI has a valid type. If so, returns true. Otherwise, triggers a - * SubmitXformFailedEvent event and returns false + * Checks if the URI has a valid type. If so, returns {@code true}. Otherwise, returns {@code false} * @param context the application context * @param uri the URI to be checked * @param validType the accepted type for URI */ private static boolean validateContentUriType(final Context context, final Uri uri, final String validType) { - if (!context.getContentResolver().getType(uri).equals(validType)) { - LOG.e("Tried to load a content URI of the wrong type: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } - return true; + return context.getContentResolver().getType(uri).equals(validType); } /** * Validates the id to be deleted after the form upload. If id is valid, it returns - * true. Otherwise, it triggers * {@link SubmitXformFailedEvent} event and - * returns false. + * {@code true}. Otherwise, returns {@code false}. * @param id the id to be deleted * @param uri the URI containing the id to be deleted */ private static boolean validateIdToDeleteAfterUpload(final Long id, Uri uri) { - if (id == null) { - LOG.e("No id to delete for after upload: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } - return true; + return id != null; } /** - * Validates the xml. Returns true if it is valid. Otherwise, it triggers - * {@link SubmitXformFailedEvent} and returns false + * Validates the xml. Returns {@code true} if it is valid. Otherwise, returns {@code false} */ private static boolean validateXml(String xml) { - if(xml == null) { - LOG.e("Xml form is not valid."); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } - return true; + return xml != null; } private static void deleteLocalFormInstances(Long formIdToDelete) { From e98ca30e367841f8b52a2146a93e8e07b663362d Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Thu, 3 Dec 2015 12:07:29 -0200 Subject: [PATCH 13/69] Refactoring event error trigger to a try-catch approach --- .../client/ui/OdkActivityLauncher.java | 135 ++++++------------ 1 file changed, 46 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 3ce6b958..d728c811 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -298,104 +298,61 @@ public static boolean sendOdkResultToServer( if(isActivityCanceled(resultCode, data)) return false; - final Uri uri = data.getData(); - if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) { - LOG.e("Tried to load a content URI of the wrong type: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } + try { + final Uri uri = data.getData(); + if(!context.getContentResolver().getType(uri).equals(CONTENT_ITEM_TYPE)) { + throw new IllegalStateException("Tried to load a content URI of the wrong type: " + + uri); + } - final String filePath = getFormFilePath(context, uri); - if(!validateFilePath(filePath, uri)) { - LOG.e("No file path for form instance: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } + final String filePath = getFormFilePath(context, uri); + if(filePath == null) { + throw new IllegalStateException("No file path for form instance: " + uri); + } - final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); - if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) { - LOG.e("No id to delete for after upload: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } + final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); + if(formIdToDelete == null) { + throw new IllegalStateException("No id to delete for after upload: " + uri); + } + + // Temporary code for messing about with xform instance, reading values. + byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); - // Temporary code for messing about with xform instance, reading values. - byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); + // get the root of the saved and template instances + final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); - // get the root of the saved and template instances - final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); + final String xml = readFromPath(filePath); + if(xml == null) { + throw new IllegalStateException("Xml form is not valid for uri: " + uri); + } + + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); + } - final String xml = readFromPath(filePath); - if(!validateXml(xml)) { - LOG.e("Xml form is not valid."); + sendFormToServer(patientUuid, xml, + new Response.Listener() { + @Override public void onResponse(JSONObject response) { + LOG.i("Created new encounter successfully on server" + response.toString()); + if (!settings.getKeepFormInstancesLocally()) { + deleteLocalFormInstances(formIdToDelete); + } + EventBus.getDefault().post(new SubmitXformSucceededEvent()); + } + }, new Response.ErrorListener() { + @Override public void onErrorResponse(VolleyError error) { + LOG.e(error, "Error submitting form to server"); + handleSubmitError(error); + } + }); + return true; + } catch(IllegalStateException ise) { + LOG.e(ise.getMessage()); EventBus.getDefault().post( new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); return false; } - - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); - return false; - } - - sendFormToServer(patientUuid, xml, - new Response.Listener() { - @Override public void onResponse(JSONObject response) { - LOG.i("Created new encounter successfully on server" + response.toString()); - if (!settings.getKeepFormInstancesLocally()) { - deleteLocalFormInstances(formIdToDelete); - } - EventBus.getDefault().post(new SubmitXformSucceededEvent()); - } - }, new Response.ErrorListener() { - @Override public void onErrorResponse(VolleyError error) { - LOG.e(error, "Error submitting form to server"); - handleSubmitError(error); - } - }); - return true; - } - - /** - * Checks if the file path is valid. If so, it returns {@code true}. Otherwise returns - * false. - * @param filePath the file path to be validated - * @param uri the form uri - */ - private static boolean validateFilePath(String filePath, Uri uri) { - return filePath != null; - } - - /** - * Checks if the URI has a valid type. If so, returns {@code true}. Otherwise, returns {@code false} - * @param context the application context - * @param uri the URI to be checked - * @param validType the accepted type for URI - */ - private static boolean validateContentUriType(final Context context, final Uri uri, - final String validType) { - return context.getContentResolver().getType(uri).equals(validType); - } - - /** - * Validates the id to be deleted after the form upload. If id is valid, it returns - * {@code true}. Otherwise, returns {@code false}. - * @param id the id to be deleted - * @param uri the URI containing the id to be deleted - */ - private static boolean validateIdToDeleteAfterUpload(final Long id, Uri uri) { - return id != null; - } - - /** - * Validates the xml. Returns {@code true} if it is valid. Otherwise, returns {@code false} - */ - private static boolean validateXml(String xml) { - return xml != null; } private static void deleteLocalFormInstances(Long formIdToDelete) { From 3d3c96109e7b7d4b03a8e90ff9e7dd7bdc83ad23 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Fri, 4 Dec 2015 08:23:22 -0200 Subject: [PATCH 14/69] Saving local form cache, submitted or not --- .../client/ui/OdkActivityLauncher.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index d728c811..87c8aaf2 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -326,15 +326,15 @@ public static boolean sendOdkResultToServer( throw new IllegalStateException("Xml form is not valid for uri: " + uri); } - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); - } - sendFormToServer(patientUuid, xml, new Response.Listener() { @Override public void onResponse(JSONObject response) { LOG.i("Created new encounter successfully on server" + response.toString()); + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateObservationCache(patientUuid, savedRoot, + context.getContentResolver(), true /*submitted*/); + } if (!settings.getKeepFormInstancesLocally()) { deleteLocalFormInstances(formIdToDelete); } @@ -343,6 +343,11 @@ public static boolean sendOdkResultToServer( }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "Error submitting form to server"); + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateObservationCache(patientUuid, savedRoot, + context.getContentResolver(), false /*submitted*/); + } handleSubmitError(error); } }); @@ -524,14 +529,14 @@ private static String readFromPath(String path) { * Caches the observation changes locally for a given patient. */ private static void updateObservationCache(String patientUuid, TreeElement savedRoot, - ContentResolver resolver) { + ContentResolver resolver, boolean wasSubmitted) { ContentValues common = new ContentValues(); // It's critical that UUID is {@code null} and SUBMITTED is {@code false} for temporary // observations, so we make it explicit here. See {@link Contracts.Observations.UUID} // and {@link Contracts.Observations.SUBMITTED} for details. common.put(Contracts.Observations.UUID, (String) null); common.put(Contracts.Observations.PATIENT_UUID, patientUuid); - common.put(Contracts.Observations.SUBMITTED, false); + common.put(Contracts.Observations.SUBMITTED, wasSubmitted); final DateTime encounterTime = getEncounterAnswerDateTime(savedRoot); if(encounterTime == null) return; From c807abe3ff9b209e041274197c3b55f130372377 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Fri, 4 Dec 2015 09:21:14 -0200 Subject: [PATCH 15/69] SQLite does not support boolean. Changing submitted type from boolean to integer --- .../org/projectbuendia/client/ui/OdkActivityLauncher.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 87c8aaf2..4cf8b6d6 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -536,7 +536,7 @@ private static void updateObservationCache(String patientUuid, TreeElement saved // and {@link Contracts.Observations.SUBMITTED} for details. common.put(Contracts.Observations.UUID, (String) null); common.put(Contracts.Observations.PATIENT_UUID, patientUuid); - common.put(Contracts.Observations.SUBMITTED, wasSubmitted); + common.put(Contracts.Observations.SUBMITTED, wasSubmitted? 1 : 0); final DateTime encounterTime = getEncounterAnswerDateTime(savedRoot); if(encounterTime == null) return; @@ -584,7 +584,7 @@ private static Map mapFormConceptIdToUuid(Set xformConc } /** - * Returns a {@link ContentValues} list containing the id concept and the answer valeu from + * Returns a {@link ContentValues} list containing the id concept and the answer value from * all answered observations. Returns a empty {@link List} if no observation was answered. * * @param common the current content values. From 015ead88b3cfd7e3b7c37bb76064aa24339e08ba Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Sun, 29 Nov 2015 19:27:56 -0200 Subject: [PATCH 16/69] Extracting submit form error to a handler method and relocating fetch error handler to stay next to it. No side effects --- .../client/ui/OdkActivityLauncher.java | 111 +++++++++--------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 2aeb37a8..cee16b83 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -91,7 +91,7 @@ public static void fetchAndCacheAllXforms() { } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { - handleSyncError(error); + handleFetchSyncError(error); } }); } @@ -145,7 +145,7 @@ public static void fetchAndShowXform( }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "Fetching xform list from server failed. "); - handleSyncError(error); + handleFetchSyncError(error); } }); } @@ -229,26 +229,6 @@ private static List getLocalFormEntries() { return entries; } - private static void handleSyncError(VolleyError error) { - FetchXformFailedEvent.Reason reason = - FetchXformFailedEvent.Reason.SERVER_UNKNOWN; - if (error.networkResponse != null) { - switch (error.networkResponse.statusCode) { - case HttpURLConnection.HTTP_FORBIDDEN: - case HttpURLConnection.HTTP_UNAUTHORIZED: - reason = FetchXformFailedEvent.Reason.SERVER_AUTH; - break; - case HttpURLConnection.HTTP_NOT_FOUND: - reason = FetchXformFailedEvent.Reason.SERVER_BAD_ENDPOINT; - break; - case HttpURLConnection.HTTP_INTERNAL_ERROR: - default: - reason = FetchXformFailedEvent.Reason.SERVER_UNKNOWN; - } - } - EventBus.getDefault().post(new FetchXformFailedEvent(reason, error)); - } - /** * Launches ODK using the requested form. * @param callingActivity the {@link Activity} requesting the xform; when ODK closes, the user @@ -404,43 +384,62 @@ private static void sendFormToServer(String patientUuid, String xml, successListener, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { - LOG.e(error, "Did not submit form to server successfully"); - - SubmitXformFailedEvent.Reason reason = - SubmitXformFailedEvent.Reason.UNKNOWN; - if (error.networkResponse != null) { - switch (error.networkResponse.statusCode) { - case 401: - case 403: - reason = SubmitXformFailedEvent.Reason.SERVER_AUTH; - break; - case 404: - reason = SubmitXformFailedEvent.Reason.SERVER_BAD_ENDPOINT; - break; - case 500: - if (error.networkResponse.data == null) { - LOG.e("Server error, but no internal error stack trace " - + "available."); - } else { - LOG.e(new String( - error.networkResponse.data, Charsets.UTF_8)); - LOG.e("Server error. Internal error stack trace:\n"); - } - reason = SubmitXformFailedEvent.Reason.SERVER_ERROR; - break; - default: - reason = SubmitXformFailedEvent.Reason.SERVER_ERROR; - break; - } - } + LOG.e(error, "Error submitting form to server"); + handleSubmitSyncError(error); + } + }); + } - if (error instanceof TimeoutError) { - reason = SubmitXformFailedEvent.Reason.SERVER_TIMEOUT; + private static void handleSubmitSyncError(VolleyError error) { + SubmitXformFailedEvent.Reason reason = SubmitXformFailedEvent.Reason.UNKNOWN; + + if (error instanceof TimeoutError) { + reason = SubmitXformFailedEvent.Reason.SERVER_TIMEOUT; + } else if (error.networkResponse != null) { + switch (error.networkResponse.statusCode) { + case HttpURLConnection.HTTP_UNAUTHORIZED: + case HttpURLConnection.HTTP_FORBIDDEN: + reason = SubmitXformFailedEvent.Reason.SERVER_AUTH; + break; + case HttpURLConnection.HTTP_NOT_FOUND: + reason = SubmitXformFailedEvent.Reason.SERVER_BAD_ENDPOINT; + break; + case HttpURLConnection.HTTP_INTERNAL_ERROR: + if (error.networkResponse.data == null) { + LOG.e("Server error, but no internal error stack trace available."); + } else { + LOG.e(new String(error.networkResponse.data, Charsets.UTF_8)); + LOG.e("Server error. Internal error stack trace:\n"); } + reason = SubmitXformFailedEvent.Reason.SERVER_ERROR; + break; + default: + reason = SubmitXformFailedEvent.Reason.SERVER_ERROR; + break; + } + } - EventBus.getDefault().post(new SubmitXformFailedEvent(reason, error)); - } - }); + EventBus.getDefault().post(new SubmitXformFailedEvent(reason, error)); + } + + private static void handleFetchSyncError(VolleyError error) { + FetchXformFailedEvent.Reason reason = + FetchXformFailedEvent.Reason.SERVER_UNKNOWN; + if (error.networkResponse != null) { + switch (error.networkResponse.statusCode) { + case HttpURLConnection.HTTP_FORBIDDEN: + case HttpURLConnection.HTTP_UNAUTHORIZED: + reason = FetchXformFailedEvent.Reason.SERVER_AUTH; + break; + case HttpURLConnection.HTTP_NOT_FOUND: + reason = FetchXformFailedEvent.Reason.SERVER_BAD_ENDPOINT; + break; + case HttpURLConnection.HTTP_INTERNAL_ERROR: + default: + reason = FetchXformFailedEvent.Reason.SERVER_UNKNOWN; + } + } + EventBus.getDefault().post(new FetchXformFailedEvent(reason, error)); } private static String readFromPath(String path) throws IOException { From 76f7ff9929d47456634eb97648760cbdf46d5ec6 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Sun, 29 Nov 2015 23:03:35 -0200 Subject: [PATCH 17/69] Refactoring sendOdkResultToServer method to more succint and readable. There is no intention of side effects --- .../client/ui/OdkActivityLauncher.java | 218 ++++++++++++------ 1 file changed, 144 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index cee16b83..dfd0c110 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -36,7 +36,6 @@ import org.odk.collect.android.application.Collect; import org.odk.collect.android.model.Preset; import org.odk.collect.android.provider.FormsProviderAPI; -import org.odk.collect.android.provider.InstanceProviderAPI; import org.odk.collect.android.tasks.DeleteInstancesTask; import org.odk.collect.android.utilities.FileUtils; import org.projectbuendia.client.App; @@ -70,6 +69,9 @@ import de.greenrobot.event.EventBus; import static android.provider.BaseColumns._ID; +import static java.lang.String.format; +import static org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns + .CONTENT_ITEM_TYPE; import static org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns.INSTANCE_FILE_PATH; /** Convenience class for launching ODK to display an Xform. */ @@ -203,7 +205,7 @@ private static boolean loadXformFromCache(final Activity callingActivity, OpenMrsXformIndexEntry formToShow = findUuid(entries, uuidToShow); if (!formToShow.makeFileForForm().exists()) return false; - LOG.i(String.format("Using form %s from local cache.", uuidToShow)); + LOG.i(format("Using form %s from local cache.", uuidToShow)); showForm(callingActivity, requestCode, patient, fields, formToShow); return true; @@ -287,88 +289,99 @@ public static void sendOdkResultToServer( int resultCode, Intent data) { - if (resultCode == Activity.RESULT_CANCELED) return; - - if (data == null || data.getData() == null) { - // Cancelled. - LOG.i("No data for form result, probably cancelled."); - return; - } + if(isActivityCanceled(resultCode, data)) return; Uri uri = data.getData(); + if(!assertThatContentUriHasValidType(context, uri, CONTENT_ITEM_TYPE)) return; - if (!context.getContentResolver().getType(uri).equals( - InstanceProviderAPI.InstanceColumns.CONTENT_ITEM_TYPE)) { - LOG.e("Tried to load a content URI of the wrong type: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return; - } + final String filePath = getFormFilePath(context, uri); + final Long idToDelete = getIdToDeleteAfterUpload(context, uri); + if(filePath == null || idToDelete == null) return; // SubmitXformFailedEvent was already triggered + + // Temporary code for messing about with xform instance, reading values. + byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); + + // get the root of the saved and template instances + final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); + + final String xml = readFromPath(filePath); + if(xml == null) return; // SubmitXformFailedEvent was already triggered + + sendFormToServer(patientUuid, xml , + new Response.Listener() { + @Override public void onResponse(JSONObject response) { + LOG.i("Created new encounter successfully on server" + response.toString()); + + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateClientCache(patientUuid, savedRoot, context.getContentResolver()); + } + + if (!settings.getKeepFormInstancesLocally()) { + //Code largely copied from InstanceUploaderTask to delete on upload + DeleteInstancesTask dit = new DeleteInstancesTask(); + dit.setContentResolver( + Collect.getInstance().getApplication() + .getContentResolver()); + dit.execute(idToDelete); + } + EventBus.getDefault().post(new SubmitXformSucceededEvent()); + } + }); + } + /** + * Returns the form file path queried from the given {@link Uri}. If no file path was found, + * it triggers a {@link SubmitXformFailedEvent} event and returns null. + * @param context the application context + * @param uri the URI containing the form file path + */ + private static String getFormFilePath(final Context context, final Uri uri) { Cursor instanceCursor = null; try { - instanceCursor = context.getContentResolver().query(uri, - null, null, null, null); - if (instanceCursor.getCount() != 1) { - LOG.e("The form that we tried to load did not exist: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return; - } - instanceCursor.moveToFirst(); - String instancePath = instanceCursor.getString( + instanceCursor = getCursorAtRightPosition(context, uri); + if(instanceCursor == null) return null; + + String filePath = instanceCursor.getString( instanceCursor.getColumnIndex(INSTANCE_FILE_PATH)); - if (instancePath == null) { + if (filePath == null) { LOG.e("No file path for form instance: " + uri); EventBus.getDefault().post( new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return; + return null; } - int columnIndex = instanceCursor - .getColumnIndex(_ID); + + return filePath; + } finally { + if (instanceCursor != null) { + instanceCursor.close(); + } + } + } + + /** + * Returns the id to be deleted after the form upload, which was queried from the given + * {@link Uri}. If no id was found, it triggers a {@link SubmitXformFailedEvent} event and + * returns null. + * @param context the application context + * @param uri the URI containing the id to be deleted + */ + private static Long getIdToDeleteAfterUpload(final Context context, final Uri uri) { + Cursor instanceCursor = null; + try { + instanceCursor = getCursorAtRightPosition(context, uri); + if(instanceCursor == null) return null; + + int columnIndex = instanceCursor.getColumnIndex(_ID); if (columnIndex == -1) { LOG.e("No id to delete for after upload: " + uri); EventBus.getDefault().post( new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return; + return null; } - final long idToDelete = instanceCursor.getLong(columnIndex); - - // Temporary code for messing about with xform instance, reading values. - // - byte[] fileBytes = FileUtils.getFileAsBytes(new File(instancePath)); - - // get the root of the saved and template instances - final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); - - sendFormToServer(patientUuid, readFromPath(instancePath), - new Response.Listener() { - @Override public void onResponse(JSONObject response) { - LOG.i("Created new encounter successfully on server" - + response.toString()); - - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateClientCache( - patientUuid, savedRoot, context.getContentResolver()); - } - - if (!settings.getKeepFormInstancesLocally()) { - //Code largely copied from InstanceUploaderTask to delete on upload - DeleteInstancesTask dit = new DeleteInstancesTask(); - dit.setContentResolver( - Collect.getInstance().getApplication() - .getContentResolver()); - dit.execute(idToDelete); - } - EventBus.getDefault().post(new SubmitXformSucceededEvent()); - } - }); - } catch (IOException e) { - LOG.e(e, "Failed to read xml form into a String " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + + return instanceCursor.getLong(columnIndex); } finally { if (instanceCursor != null) { instanceCursor.close(); @@ -376,6 +389,51 @@ public static void sendOdkResultToServer( } } + private static Cursor getCursorAtRightPosition(final Context context, final Uri uri) { + Cursor instanceCursor = context.getContentResolver().query(uri, null, null, null, null); + if (instanceCursor.getCount() != 1) { + LOG.e("The form that we tried to load did not exist: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return null; + } + instanceCursor.moveToFirst(); + + return instanceCursor; + } + + /** + * Checks if the URI has a valid type. If so, returns true. Otherwise, triggers a + * SubmitXformFailedEvent event and returns false + * @param context the application context + * @param uri the URI to be checked + * @param validType the accepted type for URI + */ + private static boolean assertThatContentUriHasValidType(final Context context, final Uri uri, + final String validType) { + if (!context.getContentResolver().getType(uri).equals(validType)) { + LOG.e("Tried to load a content URI of the wrong type: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } + return true; + } + + /** + * Returns true if the activity was canceled + * @param resultCode the result code sent from Android activity transition + * @param data the incoming intent + */ + private static boolean isActivityCanceled(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_CANCELED) return true; + if (data == null || data.getData() == null) { + LOG.i("No data for form result, probably cancelled."); + return true; + } + return false; + } + private static void sendFormToServer(String patientUuid, String xml, Response.Listener successListener) { OpenMrsXformsConnection connection = @@ -442,14 +500,26 @@ private static void handleFetchSyncError(VolleyError error) { EventBus.getDefault().post(new FetchXformFailedEvent(reason, error)); } - private static String readFromPath(String path) throws IOException { - StringBuilder sb = new StringBuilder(); - BufferedReader reader = new BufferedReader(new FileReader(path)); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); + /** + * Returns the xml form as a String from the path. If for any reason, the file couldn't be read, + * it triggers {@link SubmitXformFailedEvent} and returns null + * @param path the path to be read + */ + private static String readFromPath(String path) { + try { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = new BufferedReader(new FileReader(path)); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + return sb.toString(); + } catch (IOException e) { + LOG.e(e, format("Failed to read xml form into a String. FilePath= ", path)); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return null; } - return sb.toString(); } private static void updateClientCache(String patientUuid, TreeElement savedRoot, From ddc2970c0ed2c7a594d65dd24797b9b388e2d527 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Sun, 29 Nov 2015 23:10:22 -0200 Subject: [PATCH 18/69] Commenting getCursorAtRightPosition metahod --- .../org/projectbuendia/client/ui/OdkActivityLauncher.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index dfd0c110..9fb0c9b6 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -389,6 +389,12 @@ private static Long getIdToDeleteAfterUpload(final Context context, final Uri ur } } + /** + * Returns the form {@link Cursor} ready to be used. If no form was found, it triggers a + * {@link SubmitXformFailedEvent} event and returns null. + * @param context the application context + * @param uri the URI to be queried + */ private static Cursor getCursorAtRightPosition(final Context context, final Uri uri) { Cursor instanceCursor = context.getContentResolver().query(uri, null, null, null, null); if (instanceCursor.getCount() != 1) { From c1048459fb29082fb55bc986754ddeb0b7b8c645 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Mon, 30 Nov 2015 00:43:18 -0200 Subject: [PATCH 19/69] Refactoring sendFormToServer call to be more succint. No side effects --- .../client/ui/OdkActivityLauncher.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 9fb0c9b6..a96164da 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -318,18 +318,28 @@ public static void sendOdkResultToServer( } if (!settings.getKeepFormInstancesLocally()) { - //Code largely copied from InstanceUploaderTask to delete on upload - DeleteInstancesTask dit = new DeleteInstancesTask(); - dit.setContentResolver( - Collect.getInstance().getApplication() - .getContentResolver()); - dit.execute(idToDelete); + deleteFormInstances(idToDelete); } EventBus.getDefault().post(new SubmitXformSucceededEvent()); } + + }, new Response.ErrorListener() { + @Override public void onErrorResponse(VolleyError error) { + LOG.e(error, "Error submitting form to server"); + handleSubmitSyncError(error); + } }); } + private static void deleteFormInstances(Long formIdToDelete) { + //Code largely copied from InstanceUploaderTask to delete on upload + DeleteInstancesTask dit = new DeleteInstancesTask(); + dit.setContentResolver( + Collect.getInstance().getApplication() + .getContentResolver()); + dit.execute(formIdToDelete); + } + /** * Returns the form file path queried from the given {@link Uri}. If no file path was found, * it triggers a {@link SubmitXformFailedEvent} event and returns null. @@ -441,17 +451,11 @@ private static boolean isActivityCanceled(int resultCode, Intent data) { } private static void sendFormToServer(String patientUuid, String xml, - Response.Listener successListener) { + Response.Listener successListener, + Response.ErrorListener errorListener) { OpenMrsXformsConnection connection = new OpenMrsXformsConnection(App.getConnectionDetails()); - connection.postXformInstance(patientUuid, xml, - successListener, - new Response.ErrorListener() { - @Override public void onErrorResponse(VolleyError error) { - LOG.e(error, "Error submitting form to server"); - handleSubmitSyncError(error); - } - }); + connection.postXformInstance(patientUuid, xml, successListener, errorListener); } private static void handleSubmitSyncError(VolleyError error) { From 29a8e4bee7c0c561df29b8b7a2e556a2e37da43d Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Mon, 30 Nov 2015 00:45:29 -0200 Subject: [PATCH 20/69] Improving variable name to be more readable --- .../projectbuendia/client/ui/OdkActivityLauncher.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index a96164da..1680235c 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -295,8 +295,8 @@ public static void sendOdkResultToServer( if(!assertThatContentUriHasValidType(context, uri, CONTENT_ITEM_TYPE)) return; final String filePath = getFormFilePath(context, uri); - final Long idToDelete = getIdToDeleteAfterUpload(context, uri); - if(filePath == null || idToDelete == null) return; // SubmitXformFailedEvent was already triggered + final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); + if(filePath == null || formIdToDelete == null) return; // SubmitXformFailedEvent was already triggered // Temporary code for messing about with xform instance, reading values. byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); @@ -318,11 +318,10 @@ public static void sendOdkResultToServer( } if (!settings.getKeepFormInstancesLocally()) { - deleteFormInstances(idToDelete); + deleteLocalFormInstances(formIdToDelete); } EventBus.getDefault().post(new SubmitXformSucceededEvent()); } - }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "Error submitting form to server"); @@ -331,7 +330,7 @@ public static void sendOdkResultToServer( }); } - private static void deleteFormInstances(Long formIdToDelete) { + private static void deleteLocalFormInstances(Long formIdToDelete) { //Code largely copied from InstanceUploaderTask to delete on upload DeleteInstancesTask dit = new DeleteInstancesTask(); dit.setContentResolver( From 97a6ed29859f8bc4ca77555441fc7770b5ba8793 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Mon, 30 Nov 2015 02:30:23 -0200 Subject: [PATCH 21/69] Refactoring updateClientCache method. It was too long and to hard to comprehend. It was necessary to debug the entire method to know what it was doing. It is much more readable now. No side effects --- .../client/ui/OdkActivityLauncher.java | 176 ++++++++++-------- 1 file changed, 96 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 1680235c..9d0e0398 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -61,6 +61,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; @@ -307,16 +308,11 @@ public static void sendOdkResultToServer( final String xml = readFromPath(filePath); if(xml == null) return; // SubmitXformFailedEvent was already triggered - sendFormToServer(patientUuid, xml , + sendFormToServer(patientUuid, xml, new Response.Listener() { @Override public void onResponse(JSONObject response) { LOG.i("Created new encounter successfully on server" + response.toString()); - - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateClientCache(patientUuid, savedRoot, context.getContentResolver()); - } - + updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); if (!settings.getKeepFormInstancesLocally()) { deleteLocalFormInstances(formIdToDelete); } @@ -531,28 +527,14 @@ private static String readFromPath(String path) { } } - private static void updateClientCache(String patientUuid, TreeElement savedRoot, - ContentResolver resolver) { - // id, fill in auto - // patient uuid: context - // encounter uuid: make one up - // encounter time: - // - // 2014-12-15T13:33:00.000Z - // concept uuid: - // - // - // - // value: - // - // - // 36.0 - // temp_cache: true - - // or for coded - // - // - // 1066^NO^99DCT + /** + * Caches the observation changes locally for a given patient. + * For a new patient (patientUuid == null), no information is cached. + */ + private static void updateObservationCache(@Nullable String patientUuid, TreeElement savedRoot, + ContentResolver resolver) { + // Only locally cache new observations, not new patients. + if (patientUuid == null) return; ContentValues common = new ContentValues(); // It's critical that UUID is {@code null} for temporary observations, so we make it @@ -560,32 +542,63 @@ private static void updateClientCache(String patientUuid, TreeElement savedRoot, common.put(Contracts.Observations.UUID, (String) null); common.put(Contracts.Observations.PATIENT_UUID, patientUuid); - TreeElement encounter = savedRoot.getChild("encounter", 0); - if (encounter == null) { - LOG.e("No encounter found in instance"); - return; - } + final DateTime encounterTime = getEncounterAnswerDateTime(savedRoot); + if(encounterTime == null) return; + common.put(Contracts.Observations.ENCOUNTER_MILLIS, encounterTime.getMillis()); + common.put(Contracts.Observations.ENCOUNTER_UUID, UUID.randomUUID().toString()); - TreeElement encounterDatetime = - encounter.getChild("encounter.encounter_datetime", 0); - if (encounterDatetime == null) { - LOG.e("No encounter date time found in instance"); - return; + Set xformConceptIds = new HashSet<>(); + List toInsert = getAnsweredObservations(common, savedRoot, xformConceptIds); + Map xformIdToUuid = mapFormConceptIdToUuid(xformConceptIds, resolver); + + // Remap concept ids to uuids, skipping anything we can't remap. + for (Iterator i = toInsert.iterator(); i.hasNext(); ) { + ContentValues values = i.next(); + if (!mapIdToUuid(xformIdToUuid, values, Contracts.Observations.CONCEPT_UUID)) { + i.remove(); + } + mapIdToUuid(xformIdToUuid, values, Contracts.Observations.VALUE); } - IAnswerData dateTimeValue = encounterDatetime.getValue(); - try { - DateTime encounterTime = - ISODateTimeFormat.dateTime().parseDateTime((String) dateTimeValue.getValue()); - common.put(Contracts.Observations.ENCOUNTER_MILLIS, encounterTime.getMillis()); - common.put(Contracts.Observations.ENCOUNTER_UUID, UUID.randomUUID().toString()); - } catch (IllegalArgumentException e) { - LOG.e("Could not parse datetime" + dateTimeValue.getValue()); - return; + resolver.bulkInsert(Contracts.Observations.CONTENT_URI, + toInsert.toArray(new ContentValues[toInsert.size()])); + } + + /** Get a map from XForm ids to UUIDs from our local concept database. */ + private static Map mapFormConceptIdToUuid(Set xformConceptIds, + ContentResolver resolver) { + String inClause = Joiner.on(",").join(xformConceptIds); + + HashMap xformIdToUuid = new HashMap<>(); + Cursor cursor = resolver.query(Contracts.Concepts.CONTENT_URI, + new String[] {Contracts.Concepts.UUID, Contracts.Concepts.XFORM_ID}, + Contracts.Concepts.XFORM_ID + " IN (" + inClause + ")", + null, null); + + try { + while (cursor.moveToNext()) { + xformIdToUuid.put(Utils.getString(cursor, Contracts.Concepts.XFORM_ID), + Utils.getString(cursor, Contracts.Concepts.UUID)); + } + } finally { + cursor.close(); } - ArrayList toInsert = new ArrayList<>(); - HashSet xformConceptIds = new HashSet<>(); + return xformIdToUuid; + } + + /** + * Returns a {@link ContentValues} list containing the id concept and the answer valeu from + * all answered observations. Returns a empty {@link List} if no observation was answered. + * + * @param common the current content values. + * @param savedRoot the root tree form element + * @param xformConceptIdsAccumulator the set to store the form concept ids found + */ + private static List getAnsweredObservations(ContentValues common, + TreeElement savedRoot, + Set xformConceptIdsAccumulator) { + List answeredObservations = new ArrayList<>(); for (int i = 0; i < savedRoot.getNumChildren(); i++) { TreeElement group = savedRoot.getChildAt(i); if (group.getNumChildren() == 0) continue; @@ -594,20 +607,22 @@ private static void updateClientCache(String patientUuid, TreeElement savedRoot, TreeElement openmrsConcept = question.getAttribute(null, "openmrs_concept"); TreeElement openmrsDatatype = question.getAttribute(null, "openmrs_datatype"); if (openmrsConcept == null || openmrsDatatype == null) continue; + // Get the concept for the question. // eg "5088^Temperature (C)^99DCT" String encodedConcept = (String) openmrsConcept.getValue().getValue(); - Integer id = getConceptId(xformConceptIds, encodedConcept); + Integer id = getConceptId(xformConceptIdsAccumulator, encodedConcept); if (id == null) continue; + // Also get for the answer if a coded question - String value; TreeElement valueChild = question.getChild("value", 0); IAnswerData answer = valueChild.getValue(); - if (answer == null) continue; + if (answer == null || answer.getValue() == null) continue; + Object answerObject = answer.getValue(); - if (answerObject == null) continue; + String value; if ("CWE".equals(openmrsDatatype.getValue().getValue())) { - value = getConceptId(xformConceptIds, answerObject.toString()).toString(); + value = getConceptId(xformConceptIdsAccumulator, answerObject.toString()).toString(); } else { value = answerObject.toString(); } @@ -616,36 +631,37 @@ private static void updateClientCache(String patientUuid, TreeElement savedRoot, // Set to the id for now, we'll replace with uuid later observation.put(Contracts.Observations.CONCEPT_UUID, id.toString()); observation.put(Contracts.Observations.VALUE, value); - toInsert.add(observation); + + answeredObservations.add(observation); } } + return answeredObservations; + } - String inClause = Joiner.on(",").join(xformConceptIds); - // Get a map from XForm ids to UUIDs from our local concept database. - HashMap xformIdToUuid = new HashMap<>(); - Cursor cursor = resolver.query(Contracts.Concepts.CONTENT_URI, - new String[] {Contracts.Concepts.UUID, Contracts.Concepts.XFORM_ID}, - Contracts.Concepts.XFORM_ID + " IN (" + inClause + ")", - null, null); - try { - while (cursor.moveToNext()) { - xformIdToUuid.put(Utils.getString(cursor, Contracts.Concepts.XFORM_ID), - Utils.getString(cursor, Contracts.Concepts.UUID)); - } - } finally { - cursor.close(); + /** + * Returns the encounter's answer date time. Returns null if it cannot be retrieved. + */ + private static DateTime getEncounterAnswerDateTime(TreeElement root) { + TreeElement encounter = root.getChild("encounter", 0); + if (encounter == null) { + LOG.e("No encounter found in instance"); + return null; } - // Remap concept ids to uuids, skipping anything we can't remap. - for (Iterator i = toInsert.iterator(); i.hasNext(); ) { - ContentValues values = i.next(); - if (!mapIdToUuid(xformIdToUuid, values, Contracts.Observations.CONCEPT_UUID)) { - i.remove(); - } - mapIdToUuid(xformIdToUuid, values, Contracts.Observations.VALUE); + TreeElement encounterDatetime = + encounter.getChild("encounter.encounter_datetime", 0); + if (encounterDatetime == null) { + LOG.e("No encounter date time found in instance"); + return null; + } + + IAnswerData dateTimeValue = encounterDatetime.getValue(); + try { + return ISODateTimeFormat.dateTime().parseDateTime((String) dateTimeValue.getValue()); + } catch (IllegalArgumentException e) { + LOG.e("Could not parse datetime" + dateTimeValue.getValue()); + return null; } - resolver.bulkInsert(Contracts.Observations.CONTENT_URI, - toInsert.toArray(new ContentValues[toInsert.size()])); } private static Integer getConceptId(Set accumulator, String encodedConcept) { @@ -657,7 +673,7 @@ private static Integer getConceptId(Set accumulator, String encodedConc } private static boolean mapIdToUuid( - HashMap idToUuid, ContentValues values, String key) { + Map idToUuid, ContentValues values, String key) { String id = (String) values.get(key); String uuid = idToUuid.get(id); if (uuid == null) { From 0ede562287e8bdfbf0b4836da50c47135c003217 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Tue, 1 Dec 2015 08:51:12 -0200 Subject: [PATCH 22/69] OpenMrsJsonRequest was creating a response listener only to delegate call to an already received successListener. So instead of create one, it has just to use the received one --- .../client/net/OpenMrsXformsConnection.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/net/OpenMrsXformsConnection.java b/app/src/main/java/org/projectbuendia/client/net/OpenMrsXformsConnection.java index 27a74d26..716003d5 100644 --- a/app/src/main/java/org/projectbuendia/client/net/OpenMrsXformsConnection.java +++ b/app/src/main/java/org/projectbuendia/client/net/OpenMrsXformsConnection.java @@ -129,14 +129,14 @@ public void listXforms(final Response.Listener> lis * Send a single Xform to the OpenMRS server. * @param patientUuid null if this is to add a new patient, non-null for observation on existing * patient - * @param resultListener the listener to be informed of the form asynchronously + * @param successListener the listener to be informed of the form asynchronously * @param errorListener a listener to be informed of any errors */ public void postXformInstance( - @Nullable String patientUuid, - String xform, - final Response.Listener resultListener, - Response.ErrorListener errorListener) { + final @Nullable String patientUuid, + final String xform, + final Response.Listener successListener, + final Response.ErrorListener errorListener) { // The JsonObject members in the API as written at the moment. // int "patient_id" @@ -163,11 +163,8 @@ public void postXformInstance( OpenMrsJsonRequest request = new OpenMrsJsonRequest( mConnectionDetails, "/xforminstances", postBody, // non-null implies POST - new Response.Listener() { - @Override public void onResponse(JSONObject response) { - resultListener.onResponse(response); - } - }, errorListener + successListener, + errorListener ); // Set a permissive timeout. request.setRetryPolicy(new DefaultRetryPolicy(Common.REQUEST_TIMEOUT_MS_MEDIUM, 1, 1f)); From 25052775f57b76298e4bf1ab1f8607db6520db21 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 2 Dec 2015 01:09:57 -0200 Subject: [PATCH 23/69] Adjusments based on Code Review --- .../client/ui/OdkActivityLauncher.java | 140 +++++++++++------- 1 file changed, 85 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 9d0e0398..420097d2 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -94,7 +94,7 @@ public static void fetchAndCacheAllXforms() { } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { - handleFetchSyncError(error); + handleFetchError(error); } }); } @@ -148,7 +148,7 @@ public static void fetchAndShowXform( }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "Fetching xform list from server failed. "); - handleFetchSyncError(error); + handleFetchError(error); } }); } @@ -293,11 +293,13 @@ public static void sendOdkResultToServer( if(isActivityCanceled(resultCode, data)) return; Uri uri = data.getData(); - if(!assertThatContentUriHasValidType(context, uri, CONTENT_ITEM_TYPE)) return; + if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) return; final String filePath = getFormFilePath(context, uri); + if(!validateFilePath(filePath, uri)) return; + final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); - if(filePath == null || formIdToDelete == null) return; // SubmitXformFailedEvent was already triggered + if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) return; // Temporary code for messing about with xform instance, reading values. byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); @@ -306,13 +308,16 @@ public static void sendOdkResultToServer( final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); final String xml = readFromPath(filePath); - if(xml == null) return; // SubmitXformFailedEvent was already triggered + if(!validateXml(xml)) return; sendFormToServer(patientUuid, xml, new Response.Listener() { @Override public void onResponse(JSONObject response) { LOG.i("Created new encounter successfully on server" + response.toString()); - updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); + } if (!settings.getKeepFormInstancesLocally()) { deleteLocalFormInstances(formIdToDelete); } @@ -321,11 +326,76 @@ public static void sendOdkResultToServer( }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "Error submitting form to server"); - handleSubmitSyncError(error); + handleSubmitError(error); } }); } + /** + * Checks if the file path is valid. If so, it returns true. Otherwise + * it triggers a {@link SubmitXformFailedEvent} event and returns false. + * @param filePath the file path to be validated + * @param uri the form uri + */ + private static boolean validateFilePath(String filePath, Uri uri) { + if (filePath == null) { + LOG.e("No file path for form instance: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } + return true; + } + + /** + * Checks if the URI has a valid type. If so, returns true. Otherwise, triggers a + * SubmitXformFailedEvent event and returns false + * @param context the application context + * @param uri the URI to be checked + * @param validType the accepted type for URI + */ + private static boolean validateContentUriType(final Context context, final Uri uri, + final String validType) { + if (!context.getContentResolver().getType(uri).equals(validType)) { + LOG.e("Tried to load a content URI of the wrong type: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } + return true; + } + + /** + * Validates the id to be deleted after the form upload. If id is valid, it returns + * true. Otherwise, it triggers * {@link SubmitXformFailedEvent} event and + * returns false. + * @param context the application context + * @param uri the URI containing the id to be deleted + */ + private static boolean validateIdToDeleteAfterUpload(final Long id, Uri uri) { + if (id == null) { + LOG.e("No id to delete for after upload: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } + return true; + } + + /** + * Validates the xml. Returns true if it is valid. Otherwise, it triggers + * {@link SubmitXformFailedEvent} and returns false + */ + private static boolean validateXml(String xml) { + if(xml == null) { + LOG.e("Xml form is not valid."); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } + return true; + } + private static void deleteLocalFormInstances(Long formIdToDelete) { //Code largely copied from InstanceUploaderTask to delete on upload DeleteInstancesTask dit = new DeleteInstancesTask(); @@ -337,7 +407,7 @@ private static void deleteLocalFormInstances(Long formIdToDelete) { /** * Returns the form file path queried from the given {@link Uri}. If no file path was found, - * it triggers a {@link SubmitXformFailedEvent} event and returns null. + * it returns null. * @param context the application context * @param uri the URI containing the form file path */ @@ -347,17 +417,7 @@ private static String getFormFilePath(final Context context, final Uri uri) { instanceCursor = getCursorAtRightPosition(context, uri); if(instanceCursor == null) return null; - String filePath = instanceCursor.getString( - instanceCursor.getColumnIndex(INSTANCE_FILE_PATH)); - if (filePath == null) { - LOG.e("No file path for form instance: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return null; - - } - - return filePath; + return instanceCursor.getString(instanceCursor.getColumnIndex(INSTANCE_FILE_PATH)); } finally { if (instanceCursor != null) { instanceCursor.close(); @@ -367,8 +427,7 @@ private static String getFormFilePath(final Context context, final Uri uri) { /** * Returns the id to be deleted after the form upload, which was queried from the given - * {@link Uri}. If no id was found, it triggers a {@link SubmitXformFailedEvent} event and - * returns null. + * {@link Uri}. If no id was found, it returns null. * @param context the application context * @param uri the URI containing the id to be deleted */ @@ -379,12 +438,7 @@ private static Long getIdToDeleteAfterUpload(final Context context, final Uri ur if(instanceCursor == null) return null; int columnIndex = instanceCursor.getColumnIndex(_ID); - if (columnIndex == -1) { - LOG.e("No id to delete for after upload: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return null; - } + if (columnIndex == -1) return null; return instanceCursor.getLong(columnIndex); } finally { @@ -413,24 +467,6 @@ private static Cursor getCursorAtRightPosition(final Context context, final Uri return instanceCursor; } - /** - * Checks if the URI has a valid type. If so, returns true. Otherwise, triggers a - * SubmitXformFailedEvent event and returns false - * @param context the application context - * @param uri the URI to be checked - * @param validType the accepted type for URI - */ - private static boolean assertThatContentUriHasValidType(final Context context, final Uri uri, - final String validType) { - if (!context.getContentResolver().getType(uri).equals(validType)) { - LOG.e("Tried to load a content URI of the wrong type: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } - return true; - } - /** * Returns true if the activity was canceled * @param resultCode the result code sent from Android activity transition @@ -453,7 +489,7 @@ private static void sendFormToServer(String patientUuid, String xml, connection.postXformInstance(patientUuid, xml, successListener, errorListener); } - private static void handleSubmitSyncError(VolleyError error) { + private static void handleSubmitError(VolleyError error) { SubmitXformFailedEvent.Reason reason = SubmitXformFailedEvent.Reason.UNKNOWN; if (error instanceof TimeoutError) { @@ -485,7 +521,7 @@ private static void handleSubmitSyncError(VolleyError error) { EventBus.getDefault().post(new SubmitXformFailedEvent(reason, error)); } - private static void handleFetchSyncError(VolleyError error) { + private static void handleFetchError(VolleyError error) { FetchXformFailedEvent.Reason reason = FetchXformFailedEvent.Reason.SERVER_UNKNOWN; if (error.networkResponse != null) { @@ -507,7 +543,7 @@ private static void handleFetchSyncError(VolleyError error) { /** * Returns the xml form as a String from the path. If for any reason, the file couldn't be read, - * it triggers {@link SubmitXformFailedEvent} and returns null + * it returns null * @param path the path to be read */ private static String readFromPath(String path) { @@ -521,21 +557,15 @@ private static String readFromPath(String path) { return sb.toString(); } catch (IOException e) { LOG.e(e, format("Failed to read xml form into a String. FilePath= ", path)); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); return null; } } /** * Caches the observation changes locally for a given patient. - * For a new patient (patientUuid == null), no information is cached. */ - private static void updateObservationCache(@Nullable String patientUuid, TreeElement savedRoot, + private static void updateObservationCache(String patientUuid, TreeElement savedRoot, ContentResolver resolver) { - // Only locally cache new observations, not new patients. - if (patientUuid == null) return; - ContentValues common = new ContentValues(); // It's critical that UUID is {@code null} for temporary observations, so we make it // explicit here. See {@link Contracts.Observations.UUID} for details. From 5f2b64a0b162ec156ff8309ae30026cf7b0c3c2f Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 2 Dec 2015 02:08:35 -0200 Subject: [PATCH 24/69] Fixing the javadoc description --- .../java/org/projectbuendia/client/ui/OdkActivityLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 420097d2..e0d691f5 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -369,7 +369,7 @@ private static boolean validateContentUriType(final Context context, final Uri u * Validates the id to be deleted after the form upload. If id is valid, it returns * true. Otherwise, it triggers * {@link SubmitXformFailedEvent} event and * returns false. - * @param context the application context + * @param id the id to be deleted * @param uri the URI containing the id to be deleted */ private static boolean validateIdToDeleteAfterUpload(final Long id, Uri uri) { From 8ad2b7895ec26ae269afe183a8e066012a67aed3 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 2 Dec 2015 22:29:50 -0200 Subject: [PATCH 25/69] If submission is canceled, there is no need to try submiting it --- .../client/ui/chart/PatientChartController.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java index 4a6b4267..1dbc3a13 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java @@ -276,14 +276,14 @@ public void onXFormResult(int requestCode, int resultCode, Intent data) { return; } - boolean shouldShowSubmissionDialog = (resultCode != Activity.RESULT_CANCELED); - String action = (resultCode == Activity.RESULT_CANCELED) - ? "form_discard_pressed" : "form_save_pressed"; - Utils.logUserAction(action, - "form", request.formUuid, - "patient_uuid", request.patientUuid); + boolean isSubmissionCanceled = (resultCode == Activity.RESULT_CANCELED); + Utils.logUserAction(isSubmissionCanceled ? "form_discard_pressed" : "form_save_pressed", + "form", request.formUuid, "patient_uuid", request.patientUuid); + + if(isSubmissionCanceled) return; + mOdkResultSender.sendOdkResultToServer(request.patientUuid, resultCode, data); - mUi.showFormSubmissionDialog(shouldShowSubmissionDialog); + mUi.showFormSubmissionDialog(true); } FormRequest popFormRequest(int requestIndex) { From 1a8ff8dc168bd7569f80a75e0bf959c23e651172 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 16 Dec 2015 23:14:05 -0800 Subject: [PATCH 26/69] Merging branch with upstream/dev --- .../client/providers/Contracts.java | 9 +++-- .../projectbuendia/client/sync/Database.java | 1 + .../ObservationsSyncPhaseRunnable.java | 26 +++++++------ .../client/ui/OdkActivityLauncher.java | 37 ++++++++++++------- .../client/ui/chart/PatientChartActivity.java | 7 ++-- .../ui/chart/PatientChartController.java | 16 ++++---- 6 files changed, 56 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/providers/Contracts.java b/app/src/main/java/org/projectbuendia/client/providers/Contracts.java index 53bbb305..123fa58c 100644 --- a/app/src/main/java/org/projectbuendia/client/providers/Contracts.java +++ b/app/src/main/java/org/projectbuendia/client/providers/Contracts.java @@ -165,9 +165,11 @@ public interface Observations { /** * UUID is populated if the record was retrieved from the server. If this observation was - * written locally as a cached value from a submitted XForm, UUID is null. As part of every - * successful sync, all observations with null UUIDs are deleted, on the basis that an - * authoritative version for each has been obtained from the server. + * written locally as a cached value, UUID is null. But the cached record may not be + * submitted to the server yet. So SUBMITTED flags if it was submitted indeed. As part of + * every successful sync, all observations with **null UUIDs and SUBMITTED == true** are + * deleted, on the basis that an authoritative version for each has been obtained from the + * server. */ String UUID = "uuid"; String PATIENT_UUID = "patient_uuid"; @@ -175,6 +177,7 @@ public interface Observations { String ENCOUNTER_MILLIS = "encounter_millis"; // milliseconds since epoch String CONCEPT_UUID = "concept_uuid"; String VALUE = "value"; // concept value or order UUID + String SUBMITTED = "submitted"; //indicates if the record was already submitted to the server } public interface Orders { diff --git a/app/src/main/java/org/projectbuendia/client/sync/Database.java b/app/src/main/java/org/projectbuendia/client/sync/Database.java index b3c646ae..2e39ae98 100644 --- a/app/src/main/java/org/projectbuendia/client/sync/Database.java +++ b/app/src/main/java/org/projectbuendia/client/sync/Database.java @@ -122,6 +122,7 @@ public class Database extends SQLiteOpenHelper { + "encounter_millis INTEGER," + "concept_uuid INTEGER," + "value STRING," + + "submitted INTEGER," + "UNIQUE (patient_uuid, encounter_uuid, concept_uuid)"); SCHEMAS.put(Table.ORDERS, "" diff --git a/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java b/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java index 4f94cb49..d15f7577 100644 --- a/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java +++ b/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java @@ -28,6 +28,8 @@ import java.util.ArrayList; +import static java.lang.String.format; + /** * Handles syncing observations. Uses an incremental sync mechanism - see * {@link IncrementalSyncPhaseRunnable} for details. @@ -37,14 +39,14 @@ public class ObservationsSyncPhaseRunnable extends IncrementalSyncPhaseRunnable< public ObservationsSyncPhaseRunnable() { super( - "observations", - Contracts.Table.OBSERVATIONS, - JsonObservation.class); + "observations", + Contracts.Table.OBSERVATIONS, + JsonObservation.class); } @Override protected ArrayList getUpdateOps( - JsonObservation[] list, SyncResult syncResult) { + JsonObservation[] list, SyncResult syncResult) { int deletes = 0; int inserts = 0; ArrayList ops = new ArrayList<>(); @@ -55,7 +57,7 @@ protected ArrayList getUpdateOps( deletes++; } else { ops.add(ContentProviderOperation.newInsert(Observations.CONTENT_URI) - .withValues(getObsValuesToInsert(observation)).build()); + .withValues(getObsValuesToInsert(observation)).build()); inserts++; } } @@ -67,7 +69,7 @@ protected ArrayList getUpdateOps( /** Converts an encounter data response into appropriate inserts in the encounters table. */ public static ContentValues getObsValuesToInsert( - JsonObservation observation) { + JsonObservation observation) { ContentValues cvs = new ContentValues(); cvs.put(Observations.UUID, observation.uuid); cvs.put(Observations.PATIENT_UUID, observation.patient_uuid); @@ -81,12 +83,12 @@ public static ContentValues getObsValuesToInsert( @Override protected void afterSyncFinished( - ContentResolver contentResolver, - SyncResult syncResult, - ContentProviderClient providerClient) throws RemoteException { + ContentResolver contentResolver, + SyncResult syncResult, + ContentProviderClient providerClient) throws RemoteException { // Remove all temporary observations now we have the real ones providerClient.delete(Observations.CONTENT_URI, - Observations.UUID + " IS NULL", - new String[0]); + format("%s IS NULL AND %s == 1", Observations.UUID, Observations.SUBMITTED), + new String[0]); } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index e0d691f5..3596d5c2 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -276,30 +276,36 @@ private static OpenMrsXformIndexEntry findUuid( } /** - * Convenient shared code for handling an ODK activity result. + * Convenient shared code for handling an ODK activity result. This method submits the ODK form + * to the server and saves it locally, whether or not the form was successfully submitted. + * If an error occurs over the submission, the form is kept to be resubmitted later. + * See link(TODO:which?). This method returns {@code true} if it tries to send a request + * to the server, successfully or not. If any error occurs before submission, it returns + * {@code false}. + * * @param context the application context * @param settings the application settings * @param patientUuid the patient to add an observation to, or null to create a new patient * @param resultCode the result code sent from Android activity transition * @param data the incoming intent */ - public static void sendOdkResultToServer( + public static boolean sendOdkResultToServer( final Context context, final AppSettings settings, @Nullable final String patientUuid, int resultCode, Intent data) { - if(isActivityCanceled(resultCode, data)) return; + if(isActivityCanceled(resultCode, data)) return false; Uri uri = data.getData(); - if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) return; + if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) return false; final String filePath = getFormFilePath(context, uri); - if(!validateFilePath(filePath, uri)) return; + if(!validateFilePath(filePath, uri)) return false; final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); - if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) return; + if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) return false; // Temporary code for messing about with xform instance, reading values. byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); @@ -308,16 +314,18 @@ public static void sendOdkResultToServer( final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); final String xml = readFromPath(filePath); - if(!validateXml(xml)) return; + if(!validateXml(xml)) return false; + + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); + return false; + } sendFormToServer(patientUuid, xml, new Response.Listener() { @Override public void onResponse(JSONObject response) { LOG.i("Created new encounter successfully on server" + response.toString()); - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); - } if (!settings.getKeepFormInstancesLocally()) { deleteLocalFormInstances(formIdToDelete); } @@ -329,6 +337,7 @@ public static void sendOdkResultToServer( handleSubmitError(error); } }); + return true; } /** @@ -567,10 +576,12 @@ private static String readFromPath(String path) { private static void updateObservationCache(String patientUuid, TreeElement savedRoot, ContentResolver resolver) { ContentValues common = new ContentValues(); - // It's critical that UUID is {@code null} for temporary observations, so we make it - // explicit here. See {@link Contracts.Observations.UUID} for details. + // It's critical that UUID is {@code null} and SUBMITTED is {@code false} for temporary + // observations, so we make it explicit here. See {@link Contracts.Observations.UUID} + // and {@link Contracts.Observations.SUBMITTED} for details. common.put(Contracts.Observations.UUID, (String) null); common.put(Contracts.Observations.PATIENT_UUID, patientUuid); + common.put(Contracts.Observations.SUBMITTED, false); final DateTime encounterTime = getEncounterAnswerDateTime(savedRoot); if(encounterTime == null) return; diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java index 4d19642c..c1ccae75 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java @@ -218,10 +218,9 @@ public static void start(Context caller, String uuid) { mChartRenderer = new ChartRenderer(mGridWebView, getResources()); final OdkResultSender odkResultSender = new OdkResultSender() { - @Override public void sendOdkResultToServer(String patientUuid, int resultCode, Intent data) { - OdkActivityLauncher.sendOdkResultToServer( - PatientChartActivity.this, mSettings, - patientUuid, resultCode, data); + @Override public boolean sendOdkResultToServer(String patientUuid, int resultCode, Intent data) { + return OdkActivityLauncher.sendOdkResultToServer(PatientChartActivity.this, + mSettings, patientUuid, resultCode, data); } }; final MinimalHandler minimalHandler = new MinimalHandler() { diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java index 1dbc3a13..e4152f38 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java @@ -174,7 +174,7 @@ void showOrderExecutionDialog(org.projectbuendia.client.sync.Order order, Interv /** Sends ODK form data. */ public interface OdkResultSender { - void sendOdkResultToServer( + boolean sendOdkResultToServer( @Nullable String patientUuid, int resultCode, Intent data); @@ -269,21 +269,21 @@ public void suspend() { } } - public void onXFormResult(int requestCode, int resultCode, Intent data) { - FormRequest request = popFormRequest(requestCode); - if (request == null) { + public void onXFormResult(final int requestCode, final int resultCode, final Intent data) { + final FormRequest request = popFormRequest(requestCode); + if (request == null) { LOG.e("Unknown form request code: " + requestCode); return; } - boolean isSubmissionCanceled = (resultCode == Activity.RESULT_CANCELED); + final boolean isSubmissionCanceled = (resultCode == Activity.RESULT_CANCELED); Utils.logUserAction(isSubmissionCanceled ? "form_discard_pressed" : "form_save_pressed", "form", request.formUuid, "patient_uuid", request.patientUuid); - if(isSubmissionCanceled) return; - mOdkResultSender.sendOdkResultToServer(request.patientUuid, resultCode, data); - mUi.showFormSubmissionDialog(true); + final boolean isSubmittingForm = mOdkResultSender.sendOdkResultToServer(request.patientUuid, + resultCode, data); + mUi.showFormSubmissionDialog(isSubmittingForm); } FormRequest popFormRequest(int requestIndex) { From 9779c96ba7fa10cbcc5a765e73311f8580ae5f03 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Thu, 3 Dec 2015 11:42:21 -0200 Subject: [PATCH 27/69] Refactoring sendOdkResultToServer to trigger failed events rather then inside validation methods --- .../client/ui/OdkActivityLauncher.java | 75 +++++++++---------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 3596d5c2..3ce6b958 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -298,14 +298,29 @@ public static boolean sendOdkResultToServer( if(isActivityCanceled(resultCode, data)) return false; - Uri uri = data.getData(); - if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) return false; + final Uri uri = data.getData(); + if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) { + LOG.e("Tried to load a content URI of the wrong type: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } final String filePath = getFormFilePath(context, uri); - if(!validateFilePath(filePath, uri)) return false; + if(!validateFilePath(filePath, uri)) { + LOG.e("No file path for form instance: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); - if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) return false; + if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) { + LOG.e("No id to delete for after upload: " + uri); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } // Temporary code for messing about with xform instance, reading values. byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); @@ -314,7 +329,12 @@ public static boolean sendOdkResultToServer( final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); final String xml = readFromPath(filePath); - if(!validateXml(xml)) return false; + if(!validateXml(xml)) { + LOG.e("Xml form is not valid."); + EventBus.getDefault().post( + new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); + return false; + } // Only locally cache new observations, not new patients. if (patientUuid != null) { @@ -341,68 +361,41 @@ public static boolean sendOdkResultToServer( } /** - * Checks if the file path is valid. If so, it returns true. Otherwise - * it triggers a {@link SubmitXformFailedEvent} event and returns false. + * Checks if the file path is valid. If so, it returns {@code true}. Otherwise returns + * false. * @param filePath the file path to be validated * @param uri the form uri */ private static boolean validateFilePath(String filePath, Uri uri) { - if (filePath == null) { - LOG.e("No file path for form instance: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } - return true; + return filePath != null; } /** - * Checks if the URI has a valid type. If so, returns true. Otherwise, triggers a - * SubmitXformFailedEvent event and returns false + * Checks if the URI has a valid type. If so, returns {@code true}. Otherwise, returns {@code false} * @param context the application context * @param uri the URI to be checked * @param validType the accepted type for URI */ private static boolean validateContentUriType(final Context context, final Uri uri, final String validType) { - if (!context.getContentResolver().getType(uri).equals(validType)) { - LOG.e("Tried to load a content URI of the wrong type: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } - return true; + return context.getContentResolver().getType(uri).equals(validType); } /** * Validates the id to be deleted after the form upload. If id is valid, it returns - * true. Otherwise, it triggers * {@link SubmitXformFailedEvent} event and - * returns false. + * {@code true}. Otherwise, returns {@code false}. * @param id the id to be deleted * @param uri the URI containing the id to be deleted */ private static boolean validateIdToDeleteAfterUpload(final Long id, Uri uri) { - if (id == null) { - LOG.e("No id to delete for after upload: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } - return true; + return id != null; } /** - * Validates the xml. Returns true if it is valid. Otherwise, it triggers - * {@link SubmitXformFailedEvent} and returns false + * Validates the xml. Returns {@code true} if it is valid. Otherwise, returns {@code false} */ private static boolean validateXml(String xml) { - if(xml == null) { - LOG.e("Xml form is not valid."); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } - return true; + return xml != null; } private static void deleteLocalFormInstances(Long formIdToDelete) { From 792facd23215eefb1cb8d4eea3d16be7534ba124 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Thu, 3 Dec 2015 12:07:29 -0200 Subject: [PATCH 28/69] Refactoring event error trigger to a try-catch approach --- .../client/ui/OdkActivityLauncher.java | 135 ++++++------------ 1 file changed, 46 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 3ce6b958..d728c811 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -298,104 +298,61 @@ public static boolean sendOdkResultToServer( if(isActivityCanceled(resultCode, data)) return false; - final Uri uri = data.getData(); - if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) { - LOG.e("Tried to load a content URI of the wrong type: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } + try { + final Uri uri = data.getData(); + if(!context.getContentResolver().getType(uri).equals(CONTENT_ITEM_TYPE)) { + throw new IllegalStateException("Tried to load a content URI of the wrong type: " + + uri); + } - final String filePath = getFormFilePath(context, uri); - if(!validateFilePath(filePath, uri)) { - LOG.e("No file path for form instance: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } + final String filePath = getFormFilePath(context, uri); + if(filePath == null) { + throw new IllegalStateException("No file path for form instance: " + uri); + } - final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); - if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) { - LOG.e("No id to delete for after upload: " + uri); - EventBus.getDefault().post( - new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); - return false; - } + final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); + if(formIdToDelete == null) { + throw new IllegalStateException("No id to delete for after upload: " + uri); + } + + // Temporary code for messing about with xform instance, reading values. + byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); - // Temporary code for messing about with xform instance, reading values. - byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); + // get the root of the saved and template instances + final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); - // get the root of the saved and template instances - final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); + final String xml = readFromPath(filePath); + if(xml == null) { + throw new IllegalStateException("Xml form is not valid for uri: " + uri); + } + + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); + } - final String xml = readFromPath(filePath); - if(!validateXml(xml)) { - LOG.e("Xml form is not valid."); + sendFormToServer(patientUuid, xml, + new Response.Listener() { + @Override public void onResponse(JSONObject response) { + LOG.i("Created new encounter successfully on server" + response.toString()); + if (!settings.getKeepFormInstancesLocally()) { + deleteLocalFormInstances(formIdToDelete); + } + EventBus.getDefault().post(new SubmitXformSucceededEvent()); + } + }, new Response.ErrorListener() { + @Override public void onErrorResponse(VolleyError error) { + LOG.e(error, "Error submitting form to server"); + handleSubmitError(error); + } + }); + return true; + } catch(IllegalStateException ise) { + LOG.e(ise.getMessage()); EventBus.getDefault().post( new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); return false; } - - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); - return false; - } - - sendFormToServer(patientUuid, xml, - new Response.Listener() { - @Override public void onResponse(JSONObject response) { - LOG.i("Created new encounter successfully on server" + response.toString()); - if (!settings.getKeepFormInstancesLocally()) { - deleteLocalFormInstances(formIdToDelete); - } - EventBus.getDefault().post(new SubmitXformSucceededEvent()); - } - }, new Response.ErrorListener() { - @Override public void onErrorResponse(VolleyError error) { - LOG.e(error, "Error submitting form to server"); - handleSubmitError(error); - } - }); - return true; - } - - /** - * Checks if the file path is valid. If so, it returns {@code true}. Otherwise returns - * false. - * @param filePath the file path to be validated - * @param uri the form uri - */ - private static boolean validateFilePath(String filePath, Uri uri) { - return filePath != null; - } - - /** - * Checks if the URI has a valid type. If so, returns {@code true}. Otherwise, returns {@code false} - * @param context the application context - * @param uri the URI to be checked - * @param validType the accepted type for URI - */ - private static boolean validateContentUriType(final Context context, final Uri uri, - final String validType) { - return context.getContentResolver().getType(uri).equals(validType); - } - - /** - * Validates the id to be deleted after the form upload. If id is valid, it returns - * {@code true}. Otherwise, returns {@code false}. - * @param id the id to be deleted - * @param uri the URI containing the id to be deleted - */ - private static boolean validateIdToDeleteAfterUpload(final Long id, Uri uri) { - return id != null; - } - - /** - * Validates the xml. Returns {@code true} if it is valid. Otherwise, returns {@code false} - */ - private static boolean validateXml(String xml) { - return xml != null; } private static void deleteLocalFormInstances(Long formIdToDelete) { From 6a8432dd2b2d279856541d0fef08bec90593d312 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Fri, 4 Dec 2015 08:23:22 -0200 Subject: [PATCH 29/69] Saving local form cache, submitted or not --- .../client/ui/OdkActivityLauncher.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index d728c811..87c8aaf2 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -326,15 +326,15 @@ public static boolean sendOdkResultToServer( throw new IllegalStateException("Xml form is not valid for uri: " + uri); } - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateObservationCache(patientUuid, savedRoot, context.getContentResolver()); - } - sendFormToServer(patientUuid, xml, new Response.Listener() { @Override public void onResponse(JSONObject response) { LOG.i("Created new encounter successfully on server" + response.toString()); + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateObservationCache(patientUuid, savedRoot, + context.getContentResolver(), true /*submitted*/); + } if (!settings.getKeepFormInstancesLocally()) { deleteLocalFormInstances(formIdToDelete); } @@ -343,6 +343,11 @@ public static boolean sendOdkResultToServer( }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "Error submitting form to server"); + // Only locally cache new observations, not new patients. + if (patientUuid != null) { + updateObservationCache(patientUuid, savedRoot, + context.getContentResolver(), false /*submitted*/); + } handleSubmitError(error); } }); @@ -524,14 +529,14 @@ private static String readFromPath(String path) { * Caches the observation changes locally for a given patient. */ private static void updateObservationCache(String patientUuid, TreeElement savedRoot, - ContentResolver resolver) { + ContentResolver resolver, boolean wasSubmitted) { ContentValues common = new ContentValues(); // It's critical that UUID is {@code null} and SUBMITTED is {@code false} for temporary // observations, so we make it explicit here. See {@link Contracts.Observations.UUID} // and {@link Contracts.Observations.SUBMITTED} for details. common.put(Contracts.Observations.UUID, (String) null); common.put(Contracts.Observations.PATIENT_UUID, patientUuid); - common.put(Contracts.Observations.SUBMITTED, false); + common.put(Contracts.Observations.SUBMITTED, wasSubmitted); final DateTime encounterTime = getEncounterAnswerDateTime(savedRoot); if(encounterTime == null) return; From e0afa566c4740b09038d37620d47efb35f30efcb Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Fri, 4 Dec 2015 09:21:14 -0200 Subject: [PATCH 30/69] SQLite does not support boolean. Changing submitted type from boolean to integer --- .../org/projectbuendia/client/ui/OdkActivityLauncher.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 87c8aaf2..4cf8b6d6 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -536,7 +536,7 @@ private static void updateObservationCache(String patientUuid, TreeElement saved // and {@link Contracts.Observations.SUBMITTED} for details. common.put(Contracts.Observations.UUID, (String) null); common.put(Contracts.Observations.PATIENT_UUID, patientUuid); - common.put(Contracts.Observations.SUBMITTED, wasSubmitted); + common.put(Contracts.Observations.SUBMITTED, wasSubmitted? 1 : 0); final DateTime encounterTime = getEncounterAnswerDateTime(savedRoot); if(encounterTime == null) return; @@ -584,7 +584,7 @@ private static Map mapFormConceptIdToUuid(Set xformConc } /** - * Returns a {@link ContentValues} list containing the id concept and the answer valeu from + * Returns a {@link ContentValues} list containing the id concept and the answer value from * all answered observations. Returns a empty {@link List} if no observation was answered. * * @param common the current content values. From d6582a52796ee98627cd24672ee933d224df9e9a Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 16 Dec 2015 23:22:02 -0800 Subject: [PATCH 31/69] Resolving merging conflit with upstream/dev --- .../java/org/projectbuendia/client/ui/OdkActivityLauncher.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 4cf8b6d6..08167b85 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -453,6 +453,7 @@ private static void sendFormToServer(String patientUuid, String xml, connection.postXformInstance(patientUuid, xml, successListener, errorListener); } + private static void handleSubmitError(VolleyError error) { SubmitXformFailedEvent.Reason reason = SubmitXformFailedEvent.Reason.UNKNOWN; From fa32e3b5be58eb316ad64314a150eed56879cc89 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 16 Dec 2015 23:36:22 -0800 Subject: [PATCH 32/69] Resolving merging conflit with upstream/dev --- .../client/ui/OdkActivityLauncher.java | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 08167b85..9b00a82b 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -43,6 +43,7 @@ import org.projectbuendia.client.events.FetchXformFailedEvent; import org.projectbuendia.client.events.SubmitXformFailedEvent; import org.projectbuendia.client.events.SubmitXformSucceededEvent; +import org.projectbuendia.client.exception.ValidationException; import org.projectbuendia.client.net.OdkDatabase; import org.projectbuendia.client.net.OdkXformSyncTask; import org.projectbuendia.client.net.OpenMrsXformIndexEntry; @@ -300,19 +301,19 @@ public static boolean sendOdkResultToServer( try { final Uri uri = data.getData(); - if(!context.getContentResolver().getType(uri).equals(CONTENT_ITEM_TYPE)) { - throw new IllegalStateException("Tried to load a content URI of the wrong type: " + if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) { + throw new ValidationException("Tried to load a content URI of the wrong type: " + uri); } final String filePath = getFormFilePath(context, uri); - if(filePath == null) { - throw new IllegalStateException("No file path for form instance: " + uri); + if(!validateFilePath(filePath, uri)) { + throw new ValidationException("No file path for form instance: " + uri); } final Long formIdToDelete = getIdToDeleteAfterUpload(context, uri); - if(formIdToDelete == null) { - throw new IllegalStateException("No id to delete for after upload: " + uri); + if(!validateIdToDeleteAfterUpload(formIdToDelete, uri)) { + throw new ValidationException("No id to delete for after upload: " + uri); } // Temporary code for messing about with xform instance, reading values. @@ -322,8 +323,8 @@ public static boolean sendOdkResultToServer( final TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); final String xml = readFromPath(filePath); - if(xml == null) { - throw new IllegalStateException("Xml form is not valid for uri: " + uri); + if(!validateXml(xml)) { + throw new ValidationException("Xml form is not valid for uri: " + uri); } sendFormToServer(patientUuid, xml, @@ -352,14 +353,51 @@ public static boolean sendOdkResultToServer( } }); return true; - } catch(IllegalStateException ise) { - LOG.e(ise.getMessage()); + } catch(ValidationException ve) { + LOG.e(ve.getMessage()); EventBus.getDefault().post( new SubmitXformFailedEvent(SubmitXformFailedEvent.Reason.CLIENT_ERROR)); return false; } } + /** + * Checks if the file path is valid. If so, it returns {@code true}. Otherwise returns + * {@code false} + * @param filePath the file path to be validated + * @param uri the form uri + */ + private static boolean validateFilePath(String filePath, Uri uri) { + return filePath != null; + } + + /** Checks if the URI has a valid type. If so, returns {@code true}. Otherwise, returns {@code false} + * @param context the application context + * @param uri the URI to be checked + * @param validType the accepted type for URI + */ + private static boolean validateContentUriType(final Context context, final Uri uri, + final String validType) { + return context.getContentResolver().getType(uri).equals(validType); + } + + /** + * Validates the id to be deleted after the form upload. If id is valid, it returns + * {@code true}. Otherwise, returns {@code false}. + * @param id the id to be deleted + * @param uri the URI containing the id to be deleted + */ + private static boolean validateIdToDeleteAfterUpload(final Long id, Uri uri) { + return id != null; + } + + /** + * Validates the xml. Returns {@code true} if it is valid. Otherwise, returns {@code false} + */ + private static boolean validateXml(String xml) { + return xml != null; + } + private static void deleteLocalFormInstances(Long formIdToDelete) { //Code largely copied from InstanceUploaderTask to delete on upload DeleteInstancesTask dit = new DeleteInstancesTask(); @@ -404,7 +442,7 @@ private static Long getIdToDeleteAfterUpload(final Context context, final Uri ur int columnIndex = instanceCursor.getColumnIndex(_ID); if (columnIndex == -1) return null; - return instanceCursor.getLong(columnIndex); + return instanceCursor.getLong(columnIndex); } finally { if (instanceCursor != null) { instanceCursor.close(); @@ -453,7 +491,6 @@ private static void sendFormToServer(String patientUuid, String xml, connection.postXformInstance(patientUuid, xml, successListener, errorListener); } - private static void handleSubmitError(VolleyError error) { SubmitXformFailedEvent.Reason reason = SubmitXformFailedEvent.Reason.UNKNOWN; @@ -585,7 +622,11 @@ private static Map mapFormConceptIdToUuid(Set xformConc } /** + <<<<<<< HEAD * Returns a {@link ContentValues} list containing the id concept and the answer value from + ======= + * Returns a {@link ContentValues} list containing the id concept and the answer valeu from + >>>>>>> e299aa54862b5a954f23fb252adc8ce9c9092ab0 * all answered observations. Returns a empty {@link List} if no observation was answered. * * @param common the current content values. @@ -593,8 +634,8 @@ private static Map mapFormConceptIdToUuid(Set xformConc * @param xformConceptIdsAccumulator the set to store the form concept ids found */ private static List getAnsweredObservations(ContentValues common, - TreeElement savedRoot, - Set xformConceptIdsAccumulator) { + TreeElement savedRoot, + Set xformConceptIdsAccumulator) { List answeredObservations = new ArrayList<>(); for (int i = 0; i < savedRoot.getNumChildren(); i++) { TreeElement group = savedRoot.getChildAt(i); @@ -654,7 +695,7 @@ private static DateTime getEncounterAnswerDateTime(TreeElement root) { IAnswerData dateTimeValue = encounterDatetime.getValue(); try { - return ISODateTimeFormat.dateTime().parseDateTime((String) dateTimeValue.getValue()); + return ISODateTimeFormat.dateTime().parseDateTime((String) dateTimeValue.getValue()); } catch (IllegalArgumentException e) { LOG.e("Could not parse datetime" + dateTimeValue.getValue()); return null; From 9c457ce5f97f7fc089c2aff8cbd0fdeab596e002 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 16 Dec 2015 23:40:04 -0800 Subject: [PATCH 33/69] Resolving merging conflit with upstream/dev --- .../java/org/projectbuendia/client/ui/OdkActivityLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 9b00a82b..8052434d 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -734,4 +734,4 @@ private static Integer getConceptId(String encodedConcept) { return null; } } -} +} \ No newline at end of file From 5808855dfe3473dbd8dfa36480282e1768e0e0dc Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 16 Dec 2015 23:42:04 -0800 Subject: [PATCH 34/69] Resolving merging conflit with upstream/dev --- .../java/org/projectbuendia/client/ui/OdkActivityLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 8052434d..9b00a82b 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -734,4 +734,4 @@ private static Integer getConceptId(String encodedConcept) { return null; } } -} \ No newline at end of file +} From 5b26307949acfafe337c556fd7fbc6804263a356 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Mon, 30 Nov 2015 02:30:23 -0200 Subject: [PATCH 35/69] Refactoring updateClientCache method. It was too long and to hard to comprehend. It was necessary to debug the entire method to know what it was doing. It is much more readable now. No side effects --- .../java/org/projectbuendia/client/ui/OdkActivityLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 9b00a82b..8052434d 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -734,4 +734,4 @@ private static Integer getConceptId(String encodedConcept) { return null; } } -} +} \ No newline at end of file From 13b0a65dc97c38a973ec2b772e6f535895dd15f0 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 2 Dec 2015 01:09:57 -0200 Subject: [PATCH 36/69] Adjusments based on Code Review --- .../java/org/projectbuendia/client/ui/OdkActivityLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 8052434d..9b00a82b 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -734,4 +734,4 @@ private static Integer getConceptId(String encodedConcept) { return null; } } -} \ No newline at end of file +} From 1a810c24eb7410bedf8e094c71ab050e8dcaba7d Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 16 Dec 2015 22:13:02 -0800 Subject: [PATCH 37/69] Refactoring validations to be in different methods and to throw a checked exception --- .../client/exception/ValidationException.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 app/src/main/java/org/projectbuendia/client/exception/ValidationException.java diff --git a/app/src/main/java/org/projectbuendia/client/exception/ValidationException.java b/app/src/main/java/org/projectbuendia/client/exception/ValidationException.java new file mode 100644 index 00000000..d144de55 --- /dev/null +++ b/app/src/main/java/org/projectbuendia/client/exception/ValidationException.java @@ -0,0 +1,35 @@ +// 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.exception; + +/** + * The base class for all validation exceptions. + * + * @author Vinicius Boson + */ +public class ValidationException extends Exception { + public ValidationException(String message) { + super( message ); + } + + public ValidationException() { + super(); + } + + public ValidationException(String message, Throwable cause) { + super( message, cause ); + } + + public ValidationException(Throwable cause) { + super( cause ); + } +} From d54b6edd7bfd7416a4f489e2a6e2dbb27c5ca8a1 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Sat, 19 Dec 2015 22:31:37 -0800 Subject: [PATCH 38/69] Refactiong resubmit form approach to save the unsent xml in local db and to disable Observation synchonization until all unsent forms are submitted --- .../client/models/UnsyncForm.java | 86 ++++++++++++++++ .../client/providers/BuendiaProvider.java | 12 +++ .../client/providers/Contracts.java | 26 +++-- .../projectbuendia/client/sync/Database.java | 7 +- .../IncrementalSyncPhaseRunnable.java | 16 ++- .../ObservationsSyncPhaseRunnable.java | 20 ++-- .../client/ui/OdkActivityLauncher.java | 98 +++++++++++++++---- 7 files changed, 227 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/org/projectbuendia/client/models/UnsyncForm.java diff --git a/app/src/main/java/org/projectbuendia/client/models/UnsyncForm.java b/app/src/main/java/org/projectbuendia/client/models/UnsyncForm.java new file mode 100644 index 00000000..1943a12a --- /dev/null +++ b/app/src/main/java/org/projectbuendia/client/models/UnsyncForm.java @@ -0,0 +1,86 @@ +// 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.models; + +import android.content.ContentValues; +import android.database.Cursor; + +import org.projectbuendia.client.providers.Contracts; +import org.projectbuendia.client.utils.Utils; + +import javax.annotation.concurrent.Immutable; + +@Immutable +public final class UnsyncForm { + public final String uuid; + public final String patientUuid; + public final String xml; + + public static Builder builder() { + return new Builder(); + } + + /** Puts this object's fields in a {@link ContentValues} object for insertion into a database. */ + public ContentValues toContentValues() { + ContentValues cv = new ContentValues(); + cv.put(Contracts.UnsyncForms.UUID, uuid); + cv.put(Contracts.UnsyncForms.PATIENT_UUID, patientUuid); + cv.put(Contracts.UnsyncForms.XML, xml); + return cv; + } + + public static final class Builder { + private String mUuid; + private String mPatientUuid; + private String mXml; + + public Builder setUuid(String uuid) { + this.mUuid = uuid; + return this; + } + + public Builder setPatientUuid(String patientUUid) { + this.mPatientUuid = patientUUid; + return this; + } + + public Builder setXml(String xml) { + this.mXml = xml; + return this; + } + + public UnsyncForm build() { + return new UnsyncForm(this); + } + + private Builder() { + } + } + + private UnsyncForm(Builder builder) { + this.uuid = builder.mUuid; + this.patientUuid = builder.mPatientUuid; + this.xml = builder.mXml; + } + + /** An {@link CursorLoader} that loads {@link UnsyncForm}s. */ + @Immutable + public static class Loader implements CursorLoader { + @Override public UnsyncForm fromCursor(Cursor cursor) { + return builder() + .setUuid(Utils.getString(cursor, Contracts.UnsyncForms.UUID)) + .setPatientUuid(Utils.getString(cursor, Contracts.UnsyncForms.PATIENT_UUID)) + .setXml(Utils.getString(cursor, Contracts.UnsyncForms.XML)) + .build(); + } + } +} diff --git a/app/src/main/java/org/projectbuendia/client/providers/BuendiaProvider.java b/app/src/main/java/org/projectbuendia/client/providers/BuendiaProvider.java index afd5817d..bd1709b1 100644 --- a/app/src/main/java/org/projectbuendia/client/providers/BuendiaProvider.java +++ b/app/src/main/java/org/projectbuendia/client/providers/BuendiaProvider.java @@ -162,6 +162,18 @@ public SQLiteDatabaseTransactionHelper getDbTransactionHelper() { Table.SYNC_TOKENS, Contracts.SyncTokens.TABLE_NAME)); + registry.registerDelegate( + Contracts.UnsyncForms.CONTENT_URI.getPath(), + new GroupProviderDelegate( + Contracts.UnsyncForms.ITEM_CONTENT_TYPE, + Table.UNSYNC_TOKENS)); + registry.registerDelegate( + Contracts.UnsyncForms.CONTENT_URI.getPath() + "/*", + new ItemProviderDelegate( + Contracts.UnsyncForms.GROUP_CONTENT_TYPE, + Table.UNSYNC_TOKENS, + Contracts.UnsyncForms.UUID)); + return registry; } } diff --git a/app/src/main/java/org/projectbuendia/client/providers/Contracts.java b/app/src/main/java/org/projectbuendia/client/providers/Contracts.java index 123fa58c..e378526a 100644 --- a/app/src/main/java/org/projectbuendia/client/providers/Contracts.java +++ b/app/src/main/java/org/projectbuendia/client/providers/Contracts.java @@ -36,7 +36,8 @@ public enum Table { ORDERS("orders"), PATIENTS("patients"), USERS("users"), - SYNC_TOKENS("sync_tokens"); + SYNC_TOKENS("sync_tokens"), + UNSYNC_TOKENS("unsync_forms"); public String name; @@ -165,11 +166,13 @@ public interface Observations { /** * UUID is populated if the record was retrieved from the server. If this observation was - * written locally as a cached value, UUID is null. But the cached record may not be - * submitted to the server yet. So SUBMITTED flags if it was submitted indeed. As part of - * every successful sync, all observations with **null UUIDs and SUBMITTED == true** are - * deleted, on the basis that an authoritative version for each has been obtained from the - * server. + * written locally as a cached value from a submitted XForm, UUID is null. For further + * details please check + * {@link org.projectbuendia.client.ui.OdkActivityLauncher#updateObservationCache}. + * As part of every successful sync, all observations with null UUIDs are deleted, + * on the basis that an authoritative version for each has been obtained from the server. + * For further details, please check + * {@link org.projectbuendia.client.sync.controllers.ObservationsSyncPhaseRunnable} */ String UUID = "uuid"; String PATIENT_UUID = "patient_uuid"; @@ -177,7 +180,6 @@ public interface Observations { String ENCOUNTER_MILLIS = "encounter_millis"; // milliseconds since epoch String CONCEPT_UUID = "concept_uuid"; String VALUE = "value"; // concept value or order UUID - String SUBMITTED = "submitted"; //indicates if the record was already submitted to the server } public interface Orders { @@ -240,6 +242,16 @@ public interface PatientCounts { String PATIENT_COUNT = "patient_count"; } + public interface UnsyncForms { + Uri CONTENT_URI = buildContentUri("unsync-forms"); + String GROUP_CONTENT_TYPE = buildGroupType("unsync-form"); + String ITEM_CONTENT_TYPE = buildItemType("unsync-form"); + + String UUID = "uuid"; + String PATIENT_UUID = "patient_uuid"; + String XML = "xml"; + } + public static Uri buildContentUri(String path) { return BASE_CONTENT_URI.buildUpon().appendPath(path).build(); } diff --git a/app/src/main/java/org/projectbuendia/client/sync/Database.java b/app/src/main/java/org/projectbuendia/client/sync/Database.java index 2e39ae98..15fd8e3d 100644 --- a/app/src/main/java/org/projectbuendia/client/sync/Database.java +++ b/app/src/main/java/org/projectbuendia/client/sync/Database.java @@ -122,7 +122,6 @@ public class Database extends SQLiteOpenHelper { + "encounter_millis INTEGER," + "concept_uuid INTEGER," + "value STRING," - + "submitted INTEGER," + "UNIQUE (patient_uuid, encounter_uuid, concept_uuid)"); SCHEMAS.put(Table.ORDERS, "" @@ -161,6 +160,12 @@ public class Database extends SQLiteOpenHelper { SCHEMAS.put(Table.SYNC_TOKENS, "" + "table_name TEXT PRIMARY KEY NOT NULL," + "sync_token TEXT NOT NULL"); + + SCHEMAS.put(Table.UNSYNC_TOKENS, "" + + "uuid TEXT PRIMARY KEY NOT NULL," + + "patient_uuid TEXT," + + "xml"); + } public Database(Context context) { diff --git a/app/src/main/java/org/projectbuendia/client/sync/controllers/IncrementalSyncPhaseRunnable.java b/app/src/main/java/org/projectbuendia/client/sync/controllers/IncrementalSyncPhaseRunnable.java index 4ed79d1b..315ecefc 100644 --- a/app/src/main/java/org/projectbuendia/client/sync/controllers/IncrementalSyncPhaseRunnable.java +++ b/app/src/main/java/org/projectbuendia/client/sync/controllers/IncrementalSyncPhaseRunnable.java @@ -85,7 +85,12 @@ protected IncrementalSyncPhaseRunnable( public final void sync(ContentResolver contentResolver, SyncResult syncResult, ContentProviderClient providerClient) throws Throwable { - beforeSyncStarted(contentResolver, syncResult, providerClient); + boolean okToProceedSync = beforeSyncStarted(contentResolver, syncResult, providerClient); + + if(!okToProceedSync) { + LOG.w("Skipping synchronization for %s", this.getClass().getName()); + return; + } String syncToken = SyncAdapter.getLastSyncToken(providerClient, dbTable); @@ -119,11 +124,14 @@ protected abstract ArrayList getUpdateOps( // Optional callbacks - /** Called before any records have been synced from the server. */ - protected void beforeSyncStarted( + /** + * Called before any records have been synced from the server. Returns {@code true} + * if the sync phase is good to proceed. Otherwise, returns {@code false} to skip the sync + * phase */ + protected boolean beforeSyncStarted( ContentResolver contentResolver, SyncResult syncResult, - ContentProviderClient providerClient) throws Throwable {} + ContentProviderClient providerClient) throws Throwable {return true;} /** * Called after all records have been synced from the server, even if the number of synced diff --git a/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java b/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java index edb405c7..222cb9e9 100644 --- a/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java +++ b/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java @@ -24,14 +24,11 @@ import org.projectbuendia.client.json.JsonObservation; import org.projectbuendia.client.providers.Contracts; import org.projectbuendia.client.providers.Contracts.Observations; +import org.projectbuendia.client.ui.OdkActivityLauncher; import org.projectbuendia.client.utils.Logger; import java.util.ArrayList; -import static java.lang.String.format; - -import static java.lang.String.format; - /** * Handles syncing observations. Uses an incremental sync mechanism - see * {@link IncrementalSyncPhaseRunnable} for details. @@ -47,8 +44,8 @@ public ObservationsSyncPhaseRunnable() { } @Override - protected ArrayList getUpdateOps( - JsonObservation[] list, SyncResult syncResult) { + protected ArrayList getUpdateOps(JsonObservation[] list, + SyncResult syncResult) { int deletes = 0; int inserts = 0; ArrayList ops = new ArrayList<>(); @@ -70,8 +67,7 @@ protected ArrayList getUpdateOps( } /** Converts an encounter data response into appropriate inserts in the encounters table. */ - public static ContentValues getObsValuesToInsert( - JsonObservation observation) { + public static ContentValues getObsValuesToInsert(JsonObservation observation) { ContentValues cvs = new ContentValues(); cvs.put(Observations.UUID, observation.uuid); cvs.put(Observations.PATIENT_UUID, observation.patient_uuid); @@ -83,6 +79,12 @@ public static ContentValues getObsValuesToInsert( return cvs; } + @Override + protected boolean beforeSyncStarted(ContentResolver contentResolver, SyncResult syncResult, + ContentProviderClient providerClient) throws Throwable { + return OdkActivityLauncher.resendFormsToServer(contentResolver); + } + @Override protected void afterSyncFinished( ContentResolver contentResolver, @@ -90,7 +92,7 @@ protected void afterSyncFinished( ContentProviderClient providerClient) throws RemoteException { // Remove all temporary observations now we have the real ones providerClient.delete(Observations.CONTENT_URI, - format("%s IS NULL AND %s == 1", Observations.UUID, Observations.SUBMITTED), + Observations.UUID + " IS NULL", new String[0]); } } \ No newline at end of file diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index c5b529a4..b5914e4d 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -44,11 +44,13 @@ import org.projectbuendia.client.events.SubmitXformFailedEvent; import org.projectbuendia.client.events.SubmitXformSucceededEvent; import org.projectbuendia.client.exception.ValidationException; +import org.projectbuendia.client.models.UnsyncForm; import org.projectbuendia.client.net.OdkDatabase; import org.projectbuendia.client.net.OdkXformSyncTask; import org.projectbuendia.client.net.OpenMrsXformIndexEntry; import org.projectbuendia.client.net.OpenMrsXformsConnection; import org.projectbuendia.client.providers.Contracts; +import org.projectbuendia.client.providers.Contracts.UnsyncForms; import org.projectbuendia.client.utils.Logger; import org.projectbuendia.client.utils.Utils; @@ -280,9 +282,9 @@ private static OpenMrsXformIndexEntry findUuid( * Convenient shared code for handling an ODK activity result. This method submits the ODK form * to the server and saves it locally, whether or not the form was successfully submitted. * If an error occurs over the submission, the form is kept to be resubmitted later. - * See link(TODO:which?). This method returns {@code true} if it tries to send a request - * to the server, successfully or not. If any error occurs before submission, it returns - * {@code false}. + * (See {@link #updateObservationCache(String, TreeElement, ContentResolver)}) + * This method returns {@code true} if it tries to send a request to the server, successfully + * or not. If any error occurs before submission, it returns {@code false}. * * @param context the application context * @param settings the application settings @@ -327,15 +329,16 @@ public static boolean sendOdkResultToServer( throw new ValidationException("Xml form is not valid for uri: " + uri); } + // Always cache new observations, whether or not it is successfully sent to the server. + if (patientUuid != null) { + updateObservationCache(patientUuid, savedRoot, + context.getContentResolver()); + } + sendFormToServer(patientUuid, xml, new Response.Listener() { @Override public void onResponse(JSONObject response) { LOG.i("Created new encounter successfully on server" + response.toString()); - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateObservationCache(patientUuid, savedRoot, - context.getContentResolver(), true /*submitted*/); - } if (!settings.getKeepFormInstancesLocally()) { deleteLocalFormInstances(formIdToDelete); } @@ -344,11 +347,7 @@ public static boolean sendOdkResultToServer( }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { LOG.e(error, "Error submitting form to server"); - // Only locally cache new observations, not new patients. - if (patientUuid != null) { - updateObservationCache(patientUuid, savedRoot, - context.getContentResolver(), false /*submitted*/); - } + saveUnsentForm(patientUuid, xml, context.getContentResolver()); handleSubmitError(error); } }); @@ -491,6 +490,52 @@ private static void sendFormToServer(String patientUuid, String xml, connection.postXformInstance(patientUuid, xml, successListener, errorListener); } + /** Tries to submit to the server all unsent forms. Returns {@code true} if there are no more + * unsent forms. Otherwise returns {@code false}. + */ + public static final boolean resendFormsToServer(final ContentResolver contentResolver) { + final boolean hasUnsubmittedForms[] = new boolean[]{false}; + for(final UnsyncForm unsyncForm : getUnsyncForms(contentResolver)) { + sendFormToServer(unsyncForm.patientUuid, unsyncForm.xml, + new Response.Listener() { + @Override public void onResponse(JSONObject response) { + LOG.i("Created new encounter successfully on server. " + response.toString()); + deleteUnsyncForm(unsyncForm.uuid, contentResolver); + + } + }, new Response.ErrorListener() { + @Override public void onErrorResponse(VolleyError error) { + //Just log it and flag as not synchronized, this form is already persisted. + LOG.e(error, format("Error resubmitting %s form to server ", unsyncForm.uuid)); + hasUnsubmittedForms[0] = true; + } + }); + } + return !hasUnsubmittedForms[0]; + } + + public static void deleteUnsyncForm(final String uuid, final ContentResolver contentResolver) { + LOG.i("Removing the unsynchronized form from the db"); + contentResolver.delete(UnsyncForms.CONTENT_URI, format("%s='%s'", UnsyncForms.UUID, uuid), + null); + } + + /** Returns all local forms which were NOT submitted to the server yet*/ + public static List getUnsyncForms(final ContentResolver contentResolver) { + try (Cursor cursor = contentResolver.query(UnsyncForms.CONTENT_URI, + new String[]{UnsyncForms.UUID, UnsyncForms.PATIENT_UUID, UnsyncForms.XML}, null, null, + null)) { + List unsyncForms = new ArrayList<>(); + while (cursor.moveToNext()) { + unsyncForms.add(UnsyncForm.builder() + .setUuid(Utils.getString(cursor, UnsyncForms.UUID, null)) + .setPatientUuid(Utils.getString(cursor, UnsyncForms.PATIENT_UUID, "")) + .setXml( Utils.getString(cursor, UnsyncForms.XML, "")).build()); + } + return unsyncForms; + } + } + private static void handleSubmitError(VolleyError error) { SubmitXformFailedEvent.Reason reason = SubmitXformFailedEvent.Reason.UNKNOWN; @@ -545,7 +590,7 @@ private static void handleFetchError(VolleyError error) { /** * Returns the xml form as a String from the path. If for any reason, the file couldn't be read, - * it returns null + * it returns {@code null} * @param path the path to be read */ private static String readFromPath(String path) { @@ -564,17 +609,36 @@ private static String readFromPath(String path) { } /** - * Caches the observation changes locally for a given patient. + * Saves the forms which couldn't be submitted to server into the local db. So that, when the + * application connects to server again, it can try to resend the form again. Note that this + * method just save the data as it is required to be resend to the server. Please, check + * {@link #updateObservationCache} to see how the observation itself is saved into db. + * The {@link org.projectbuendia.client.sync.controllers.ObservationsSyncPhaseRunnable} + * will check if there are unsent observations, and if is the case, it will try to resend it + * prior to pull new ones (See {@link #resendFormsToServer}). + */ + private static void saveUnsentForm(String patientUuid, String xml, ContentResolver resolver) { + resolver.insert(Contracts.UnsyncForms.CONTENT_URI, + UnsyncForm.builder().setUuid(UUID.randomUUID().toString()).setPatientUuid(patientUuid) + .setXml(xml).build().toContentValues()); + } + + /** + * Caches the observation changes locally for a given patient. Saving the observations locally + * allows them to be used by users even it the application is connected to the server at that + * time. In this case, when the app becomes online and synchronizes with the server, this + * temporary observations are deleted. Please, see {@link #saveUnsentForm} and + * {@link org.projectbuendia.client.sync.controllers.ObservationsSyncPhaseRunnable} for more + * details. */ private static void updateObservationCache(String patientUuid, TreeElement savedRoot, - ContentResolver resolver, boolean wasSubmitted) { + ContentResolver resolver) { ContentValues common = new ContentValues(); // It's critical that UUID is {@code null} and SUBMITTED is {@code false} for temporary // observations, so we make it explicit here. See {@link Contracts.Observations.UUID} // and {@link Contracts.Observations.SUBMITTED} for details. common.put(Contracts.Observations.UUID, (String) null); common.put(Contracts.Observations.PATIENT_UUID, patientUuid); - common.put(Contracts.Observations.SUBMITTED, wasSubmitted? 1 : 0); final DateTime encounterTime = getEncounterAnswerDateTime(savedRoot); if(encounterTime == null) return; From b3c704cead5b04b1e6a0652486a1260885170dd4 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Sat, 16 Jan 2016 19:42:20 -0800 Subject: [PATCH 39/69] Renaming UnsyncForm model and its field to more meaningful names. Refactoring UnsyncForm model initialization from builder to class constructor. Fixing unsync_form table and columns. --- .../client/models/UnsentForm.java | 52 +++++++++++ .../client/models/UnsyncForm.java | 86 ------------------ .../client/providers/BuendiaProvider.java | 14 +-- .../client/providers/Contracts.java | 12 +-- .../projectbuendia/client/sync/Database.java | 6 +- .../IncrementalSyncPhaseRunnable.java | 2 +- .../client/ui/OdkActivityLauncher.java | 89 +++++++++---------- .../client/ui/chart/PatientChartActivity.java | 4 +- .../ui/chart/PatientChartController.java | 3 +- .../android/tasks/DeleteInstancesTask.java | 6 ++ 10 files changed, 119 insertions(+), 155 deletions(-) create mode 100644 app/src/main/java/org/projectbuendia/client/models/UnsentForm.java delete mode 100644 app/src/main/java/org/projectbuendia/client/models/UnsyncForm.java diff --git a/app/src/main/java/org/projectbuendia/client/models/UnsentForm.java b/app/src/main/java/org/projectbuendia/client/models/UnsentForm.java new file mode 100644 index 00000000..9073c111 --- /dev/null +++ b/app/src/main/java/org/projectbuendia/client/models/UnsentForm.java @@ -0,0 +1,52 @@ +// 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.models; + +import android.content.ContentValues; +import android.database.Cursor; + +import org.projectbuendia.client.providers.Contracts; +import org.projectbuendia.client.utils.Utils; + +import javax.annotation.concurrent.Immutable; + +@Immutable +public final class UnsentForm { + public final String uuid; + public final String patientUuid; + public final String formContents; + + public UnsentForm(String uuid, String patientUuid, String formContents ) { + this.uuid = uuid; + this.patientUuid = patientUuid; + this.formContents = formContents; + } + + /** Puts this object's fields in a {@link ContentValues} object for insertion into a database. */ + public ContentValues toContentValues() { + ContentValues cv = new ContentValues(); + cv.put(Contracts.UnsentForms.UUID, uuid); + cv.put(Contracts.UnsentForms.PATIENT_UUID, patientUuid); + cv.put(Contracts.UnsentForms.FORM_CONTENTS, formContents); + return cv; + } + + /** An {@link CursorLoader} that loads {@link UnsentForm}s. */ + @Immutable + public static class Loader implements CursorLoader { + @Override public UnsentForm fromCursor(Cursor cursor) { + return new UnsentForm(Utils.getString(cursor, Contracts.UnsentForms.UUID), + Utils.getString(cursor, Contracts.UnsentForms.PATIENT_UUID), + Utils.getString(cursor, Contracts.UnsentForms.FORM_CONTENTS)); + } + } +} diff --git a/app/src/main/java/org/projectbuendia/client/models/UnsyncForm.java b/app/src/main/java/org/projectbuendia/client/models/UnsyncForm.java deleted file mode 100644 index 1943a12a..00000000 --- a/app/src/main/java/org/projectbuendia/client/models/UnsyncForm.java +++ /dev/null @@ -1,86 +0,0 @@ -// 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.models; - -import android.content.ContentValues; -import android.database.Cursor; - -import org.projectbuendia.client.providers.Contracts; -import org.projectbuendia.client.utils.Utils; - -import javax.annotation.concurrent.Immutable; - -@Immutable -public final class UnsyncForm { - public final String uuid; - public final String patientUuid; - public final String xml; - - public static Builder builder() { - return new Builder(); - } - - /** Puts this object's fields in a {@link ContentValues} object for insertion into a database. */ - public ContentValues toContentValues() { - ContentValues cv = new ContentValues(); - cv.put(Contracts.UnsyncForms.UUID, uuid); - cv.put(Contracts.UnsyncForms.PATIENT_UUID, patientUuid); - cv.put(Contracts.UnsyncForms.XML, xml); - return cv; - } - - public static final class Builder { - private String mUuid; - private String mPatientUuid; - private String mXml; - - public Builder setUuid(String uuid) { - this.mUuid = uuid; - return this; - } - - public Builder setPatientUuid(String patientUUid) { - this.mPatientUuid = patientUUid; - return this; - } - - public Builder setXml(String xml) { - this.mXml = xml; - return this; - } - - public UnsyncForm build() { - return new UnsyncForm(this); - } - - private Builder() { - } - } - - private UnsyncForm(Builder builder) { - this.uuid = builder.mUuid; - this.patientUuid = builder.mPatientUuid; - this.xml = builder.mXml; - } - - /** An {@link CursorLoader} that loads {@link UnsyncForm}s. */ - @Immutable - public static class Loader implements CursorLoader { - @Override public UnsyncForm fromCursor(Cursor cursor) { - return builder() - .setUuid(Utils.getString(cursor, Contracts.UnsyncForms.UUID)) - .setPatientUuid(Utils.getString(cursor, Contracts.UnsyncForms.PATIENT_UUID)) - .setXml(Utils.getString(cursor, Contracts.UnsyncForms.XML)) - .build(); - } - } -} diff --git a/app/src/main/java/org/projectbuendia/client/providers/BuendiaProvider.java b/app/src/main/java/org/projectbuendia/client/providers/BuendiaProvider.java index bd1709b1..e80beab5 100644 --- a/app/src/main/java/org/projectbuendia/client/providers/BuendiaProvider.java +++ b/app/src/main/java/org/projectbuendia/client/providers/BuendiaProvider.java @@ -163,16 +163,16 @@ public SQLiteDatabaseTransactionHelper getDbTransactionHelper() { Contracts.SyncTokens.TABLE_NAME)); registry.registerDelegate( - Contracts.UnsyncForms.CONTENT_URI.getPath(), + Contracts.UnsentForms.CONTENT_URI.getPath(), new GroupProviderDelegate( - Contracts.UnsyncForms.ITEM_CONTENT_TYPE, - Table.UNSYNC_TOKENS)); + Contracts.UnsentForms.ITEM_CONTENT_TYPE, + Table.UNSENT_FORMS)); registry.registerDelegate( - Contracts.UnsyncForms.CONTENT_URI.getPath() + "/*", + Contracts.UnsentForms.CONTENT_URI.getPath() + "/*", new ItemProviderDelegate( - Contracts.UnsyncForms.GROUP_CONTENT_TYPE, - Table.UNSYNC_TOKENS, - Contracts.UnsyncForms.UUID)); + Contracts.UnsentForms.GROUP_CONTENT_TYPE, + Table.UNSENT_FORMS, + Contracts.UnsentForms.UUID)); return registry; } diff --git a/app/src/main/java/org/projectbuendia/client/providers/Contracts.java b/app/src/main/java/org/projectbuendia/client/providers/Contracts.java index 5a7ca664..4514634b 100644 --- a/app/src/main/java/org/projectbuendia/client/providers/Contracts.java +++ b/app/src/main/java/org/projectbuendia/client/providers/Contracts.java @@ -37,7 +37,7 @@ public enum Table { PATIENTS("patients"), USERS("users"), SYNC_TOKENS("sync_tokens"), - UNSYNC_TOKENS("unsync_forms"); + UNSENT_FORMS("unset_forms"); public String name; @@ -243,14 +243,14 @@ public interface PatientCounts { String PATIENT_COUNT = "patient_count"; } - public interface UnsyncForms { - Uri CONTENT_URI = buildContentUri("unsync-forms"); - String GROUP_CONTENT_TYPE = buildGroupType("unsync-form"); - String ITEM_CONTENT_TYPE = buildItemType("unsync-form"); + public interface UnsentForms { + Uri CONTENT_URI = buildContentUri("unsent-forms"); + String GROUP_CONTENT_TYPE = buildGroupType("unsent-form"); + String ITEM_CONTENT_TYPE = buildItemType("unsent-form"); String UUID = "uuid"; String PATIENT_UUID = "patient_uuid"; - String XML = "xml"; + String FORM_CONTENTS = "form_contents"; } public static Uri buildContentUri(String path) { diff --git a/app/src/main/java/org/projectbuendia/client/sync/Database.java b/app/src/main/java/org/projectbuendia/client/sync/Database.java index 6116bb13..2dc46cf3 100644 --- a/app/src/main/java/org/projectbuendia/client/sync/Database.java +++ b/app/src/main/java/org/projectbuendia/client/sync/Database.java @@ -165,10 +165,10 @@ public class Database extends SQLiteOpenHelper { + "table_name TEXT PRIMARY KEY NOT NULL," + "sync_token TEXT NOT NULL"); - SCHEMAS.put(Table.UNSYNC_TOKENS, "" + SCHEMAS.put(Table.UNSENT_FORMS, "" + "uuid TEXT PRIMARY KEY NOT NULL," - + "patient_uuid TEXT," - + "xml"); + + "patient_uuid TEXT NOT NULL," + + "form_contents TEXT NOT NULL"); } diff --git a/app/src/main/java/org/projectbuendia/client/sync/controllers/IncrementalSyncPhaseRunnable.java b/app/src/main/java/org/projectbuendia/client/sync/controllers/IncrementalSyncPhaseRunnable.java index 8bf677f1..35b9d81d 100644 --- a/app/src/main/java/org/projectbuendia/client/sync/controllers/IncrementalSyncPhaseRunnable.java +++ b/app/src/main/java/org/projectbuendia/client/sync/controllers/IncrementalSyncPhaseRunnable.java @@ -88,7 +88,7 @@ public final void sync(ContentResolver contentResolver, SyncResult syncResult, boolean okToProceedSync = beforeSyncStarted(contentResolver, syncResult, providerClient); if(!okToProceedSync) { - LOG.w("Skipping synchronization for %s", this.getClass().getName()); + LOG.w("Skipping synchronization for %s", this.getClass().getSimpleName()); return; } diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index b5914e4d..8eac17cc 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -44,13 +44,12 @@ import org.projectbuendia.client.events.SubmitXformFailedEvent; import org.projectbuendia.client.events.SubmitXformSucceededEvent; import org.projectbuendia.client.exception.ValidationException; -import org.projectbuendia.client.models.UnsyncForm; +import org.projectbuendia.client.models.UnsentForm; import org.projectbuendia.client.net.OdkDatabase; import org.projectbuendia.client.net.OdkXformSyncTask; import org.projectbuendia.client.net.OpenMrsXformIndexEntry; import org.projectbuendia.client.net.OpenMrsXformsConnection; import org.projectbuendia.client.providers.Contracts; -import org.projectbuendia.client.providers.Contracts.UnsyncForms; import org.projectbuendia.client.utils.Logger; import org.projectbuendia.client.utils.Utils; @@ -67,6 +66,7 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CountDownLatch; import javax.annotation.Nullable; @@ -78,6 +78,8 @@ .CONTENT_ITEM_TYPE; import static org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns.INSTANCE_FILE_PATH; +import static org.projectbuendia.client.providers.Contracts.UnsentForms; + /** Convenience class for launching ODK to display an Xform. */ public class OdkActivityLauncher { @@ -289,18 +291,14 @@ private static OpenMrsXformIndexEntry findUuid( * @param context the application context * @param settings the application settings * @param patientUuid the patient to add an observation to, or null to create a new patient - * @param resultCode the result code sent from Android activity transition * @param data the incoming intent */ public static boolean sendOdkResultToServer( final Context context, final AppSettings settings, @Nullable final String patientUuid, - int resultCode, Intent data) { - if(isActivityCanceled(resultCode, data)) return false; - try { final Uri uri = data.getData(); if(!validateContentUriType(context, uri, CONTENT_ITEM_TYPE)) { @@ -318,7 +316,6 @@ public static boolean sendOdkResultToServer( throw new ValidationException("No id to delete for after upload: " + uri); } - // Temporary code for messing about with xform instance, reading values. byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); // get the root of the saved and template instances @@ -399,10 +396,8 @@ private static boolean validateXml(String xml) { private static void deleteLocalFormInstances(Long formIdToDelete) { //Code largely copied from InstanceUploaderTask to delete on upload - DeleteInstancesTask dit = new DeleteInstancesTask(); - dit.setContentResolver( - Collect.getInstance().getApplication() - .getContentResolver()); + DeleteInstancesTask dit = new DeleteInstancesTask(Collect.getInstance().getApplication() + .getContentResolver()); dit.execute(formIdToDelete); } @@ -468,20 +463,6 @@ private static Cursor getCursorAtRightPosition(final Context context, final Uri return instanceCursor; } - /** - * Returns true if the activity was canceled - * @param resultCode the result code sent from Android activity transition - * @param data the incoming intent - */ - private static boolean isActivityCanceled(int resultCode, Intent data) { - if (resultCode == Activity.RESULT_CANCELED) return true; - if (data == null || data.getData() == null) { - LOG.i("No data for form result, probably cancelled."); - return true; - } - return false; - } - private static void sendFormToServer(String patientUuid, String xml, Response.Listener successListener, Response.ErrorListener errorListener) { @@ -490,49 +471,62 @@ private static void sendFormToServer(String patientUuid, String xml, connection.postXformInstance(patientUuid, xml, successListener, errorListener); } - /** Tries to submit to the server all unsent forms. Returns {@code true} if there are no more + /** Tries to submit all unsent forms to the server . Returns {@code true} if there are no more * unsent forms. Otherwise returns {@code false}. */ public static final boolean resendFormsToServer(final ContentResolver contentResolver) { final boolean hasUnsubmittedForms[] = new boolean[]{false}; - for(final UnsyncForm unsyncForm : getUnsyncForms(contentResolver)) { - sendFormToServer(unsyncForm.patientUuid, unsyncForm.xml, + final List forms = getUnsetForms(contentResolver); + + //A sync barrier to wait for all returning async submissions + final CountDownLatch countDownLatch = new CountDownLatch(forms.size()); + for(final UnsentForm unsentForm : forms) { + sendFormToServer(unsentForm.patientUuid, unsentForm.formContents, new Response.Listener() { @Override public void onResponse(JSONObject response) { LOG.i("Created new encounter successfully on server. " + response.toString()); - deleteUnsyncForm(unsyncForm.uuid, contentResolver); + deleteUnsentForm(unsentForm.uuid, contentResolver); + countDownLatch.countDown(); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { - //Just log it and flag as not synchronized, this form is already persisted. - LOG.e(error, format("Error resubmitting %s form to server ", unsyncForm.uuid)); + //Just log it and flag returning valeue as pendent. It is not necessary to + // keep its content, since this form is already persisted. + LOG.e(error, format("Error resubmitting %s form to server ", unsentForm.uuid)); hasUnsubmittedForms[0] = true; + countDownLatch.countDown(); } }); } + try { + countDownLatch.await(); + } catch (InterruptedException e) { + LOG.e("Interrupted whilst waiting for unsubmitted forms to be uploaded", e); + return false; + } + return !hasUnsubmittedForms[0]; } - public static void deleteUnsyncForm(final String uuid, final ContentResolver contentResolver) { - LOG.i("Removing the unsynchronized form from the db"); - contentResolver.delete(UnsyncForms.CONTENT_URI, format("%s='%s'", UnsyncForms.UUID, uuid), + public static void deleteUnsentForm(final String uuid, final ContentResolver contentResolver) { + LOG.i("Removing the unsent form from the db"); + contentResolver.delete(UnsentForms.CONTENT_URI, format("%s='%s'", UnsentForms.UUID, uuid), null); } /** Returns all local forms which were NOT submitted to the server yet*/ - public static List getUnsyncForms(final ContentResolver contentResolver) { - try (Cursor cursor = contentResolver.query(UnsyncForms.CONTENT_URI, - new String[]{UnsyncForms.UUID, UnsyncForms.PATIENT_UUID, UnsyncForms.XML}, null, null, - null)) { - List unsyncForms = new ArrayList<>(); + public static List getUnsetForms(final ContentResolver contentResolver) { + try (Cursor cursor = contentResolver.query(UnsentForms.CONTENT_URI, + new String[]{UnsentForms.UUID, UnsentForms.PATIENT_UUID, UnsentForms.FORM_CONTENTS}, + null, null, null)) { + List unsentForms = new ArrayList<>(); while (cursor.moveToNext()) { - unsyncForms.add(UnsyncForm.builder() - .setUuid(Utils.getString(cursor, UnsyncForms.UUID, null)) - .setPatientUuid(Utils.getString(cursor, UnsyncForms.PATIENT_UUID, "")) - .setXml( Utils.getString(cursor, UnsyncForms.XML, "")).build()); + unsentForms.add(new UnsentForm(Utils.getString(cursor, UnsentForms.UUID, null), + Utils.getString(cursor, UnsentForms.PATIENT_UUID, ""), + Utils.getString(cursor, UnsentForms.FORM_CONTENTS, ""))); } - return unsyncForms; + return unsentForms; } } @@ -611,16 +605,15 @@ private static String readFromPath(String path) { /** * Saves the forms which couldn't be submitted to server into the local db. So that, when the * application connects to server again, it can try to resend the form again. Note that this - * method just save the data as it is required to be resend to the server. Please, check + * method just save the data as is required to be resend to the server. Please, check * {@link #updateObservationCache} to see how the observation itself is saved into db. * The {@link org.projectbuendia.client.sync.controllers.ObservationsSyncPhaseRunnable} * will check if there are unsent observations, and if is the case, it will try to resend it * prior to pull new ones (See {@link #resendFormsToServer}). */ private static void saveUnsentForm(String patientUuid, String xml, ContentResolver resolver) { - resolver.insert(Contracts.UnsyncForms.CONTENT_URI, - UnsyncForm.builder().setUuid(UUID.randomUUID().toString()).setPatientUuid(patientUuid) - .setXml(xml).build().toContentValues()); + resolver.insert(UnsentForms.CONTENT_URI, new UnsentForm(UUID.randomUUID().toString(), + patientUuid, xml).toContentValues()); } /** diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java index c6218908..42c4943f 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java @@ -232,9 +232,9 @@ public void onPageFinished(WebView view, String url) { mChartRenderer = new ChartRenderer(mGridWebView, getResources()); final OdkResultSender odkResultSender = new OdkResultSender() { - @Override public boolean sendOdkResultToServer(String patientUuid, int resultCode, Intent data) { + @Override public boolean sendOdkResultToServer(String patientUuid, Intent data) { return OdkActivityLauncher.sendOdkResultToServer(PatientChartActivity.this, - mSettings, patientUuid, resultCode, data); + mSettings, patientUuid, data); } }; final MinimalHandler minimalHandler = new MinimalHandler() { diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java index a292ba98..54d6b85d 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java @@ -187,7 +187,6 @@ void showOrderExecutionDialog(org.projectbuendia.client.sync.Order order, Interv public interface OdkResultSender { boolean sendOdkResultToServer( @Nullable String patientUuid, - int resultCode, Intent data); } @@ -294,7 +293,7 @@ public void onXFormResult(final int requestCode, final int resultCode, final Int if(isSubmissionCanceled) return; final boolean isSubmittingForm = mOdkResultSender.sendOdkResultToServer(request.patientUuid, - resultCode, data); + data); mUi.showFormSubmissionDialog(isSubmittingForm); } diff --git a/third_party/odkcollect/src/main/java/org/odk/collect/android/tasks/DeleteInstancesTask.java b/third_party/odkcollect/src/main/java/org/odk/collect/android/tasks/DeleteInstancesTask.java index c51a6500..baf1886b 100644 --- a/third_party/odkcollect/src/main/java/org/odk/collect/android/tasks/DeleteInstancesTask.java +++ b/third_party/odkcollect/src/main/java/org/odk/collect/android/tasks/DeleteInstancesTask.java @@ -36,6 +36,12 @@ public class DeleteInstancesTask extends AsyncTask { private DeleteInstancesListener dl; private int successCount = 0; + + public DeleteInstancesTask() {} + + public DeleteInstancesTask(final ContentResolver cr) { + setContentResolver(cr); + } @Override protected Integer doInBackground(Long... params) { From b7d323a29111b69765ecfbf787df7b062f8f117b Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Mon, 18 Jan 2016 00:07:11 -0800 Subject: [PATCH 40/69] Adding check to not allow users to submit new forms prior to resent all unset ones. Refactoring method name and its position too --- .../ObservationsSyncPhaseRunnable.java | 2 +- .../client/ui/OdkActivityLauncher.java | 147 ++++++++++-------- 2 files changed, 83 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java b/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java index 222cb9e9..dd15a432 100644 --- a/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java +++ b/app/src/main/java/org/projectbuendia/client/sync/controllers/ObservationsSyncPhaseRunnable.java @@ -82,7 +82,7 @@ public static ContentValues getObsValuesToInsert(JsonObservation observation) { @Override protected boolean beforeSyncStarted(ContentResolver contentResolver, SyncResult syncResult, ContentProviderClient providerClient) throws Throwable { - return OdkActivityLauncher.resendFormsToServer(contentResolver); + return OdkActivityLauncher.submitUnsetFormsToServer(contentResolver); } @Override diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 8eac17cc..f2115b30 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -332,7 +332,20 @@ public static boolean sendOdkResultToServer( context.getContentResolver()); } - sendFormToServer(patientUuid, xml, + /* We should prevent application to submit new forms if there are still unsent forms. + * In a scenario where an user tries to submit an form 'A' unsuccessfully, this form is + * saved to be sent later. Then, if the user tries to submit another form 'B', + * the latter can only be submitted if the former was submitted first. + * In the case of the former still can't be resent, the latter form will be saved to be + * sent all together in a future moment. + * + */ + if(!submitUnsetFormsToServer(App.getInstance().getContentResolver())) { + saveUnsentForm(patientUuid, xml, context.getContentResolver()); + return false; + } + + submitFormToServer(patientUuid, xml, new Response.Listener() { @Override public void onResponse(JSONObject response) { LOG.i("Created new encounter successfully on server" + response.toString()); @@ -357,6 +370,68 @@ public static boolean sendOdkResultToServer( } } + /** Tries to submit all unsent forms to the server . Returns {@code true} if there are no more + * unsent forms. Otherwise returns {@code false}. + */ + public static final boolean submitUnsetFormsToServer(final ContentResolver contentResolver) { + final boolean hasUnsubmittedForms[] = new boolean[]{false}; + final List forms = getUnsetForms(contentResolver); + + //Creating a sync barrier to wait for all returning async submissions + final CountDownLatch countDownLatch = new CountDownLatch(forms.size()); + for(final UnsentForm unsentForm : forms) { + submitFormToServer(unsentForm.patientUuid, unsentForm.formContents, + new Response.Listener() { + @Override public void onResponse(JSONObject response) { + LOG.i("Created new encounter successfully on server. " + response + .toString()); + deleteUnsentForm(unsentForm.uuid, contentResolver); + countDownLatch.countDown(); + + } + }, new Response.ErrorListener() { + @Override public void onErrorResponse(VolleyError error) { + //Just log it and flag returning value as pendent. It is not necessary to + // keep its content, since this form is already persisted. + LOG.e(error, format("Error resubmitting %s form to server ", unsentForm + .uuid)); + hasUnsubmittedForms[0] = true; + countDownLatch.countDown(); + } + }); + } + try { + //Waiting until all forms submissions return from server + countDownLatch.await(); + } catch (InterruptedException e) { + LOG.e("Interrupted whilst waiting for unsubmitted forms to be uploaded", e); + return false; + } + + return !hasUnsubmittedForms[0]; + } + + public static void deleteUnsentForm(final String uuid, final ContentResolver contentResolver) { + LOG.i("Removing the unsent form from the db"); + contentResolver.delete(UnsentForms.CONTENT_URI, format("%s='%s'", UnsentForms.UUID, uuid), + null); + } + + /** Returns all local forms which were NOT submitted to the server yet*/ + public static List getUnsetForms(final ContentResolver contentResolver) { + try (Cursor cursor = contentResolver.query(UnsentForms.CONTENT_URI, + new String[]{UnsentForms.UUID, UnsentForms.PATIENT_UUID, UnsentForms.FORM_CONTENTS}, + null, null, null)) { + List unsentForms = new ArrayList<>(); + while (cursor.moveToNext()) { + unsentForms.add(new UnsentForm(Utils.getString(cursor, UnsentForms.UUID, null), + Utils.getString(cursor, UnsentForms.PATIENT_UUID, ""), + Utils.getString(cursor, UnsentForms.FORM_CONTENTS, ""))); + } + return unsentForms; + } + } + /** * Checks if the file path is valid. If so, it returns {@code true}. Otherwise returns * {@code false} @@ -462,74 +537,15 @@ private static Cursor getCursorAtRightPosition(final Context context, final Uri return instanceCursor; } - - private static void sendFormToServer(String patientUuid, String xml, - Response.Listener successListener, - Response.ErrorListener errorListener) { + + private static void submitFormToServer(String patientUuid, String xml, + Response.Listener successListener, + Response.ErrorListener errorListener) { OpenMrsXformsConnection connection = new OpenMrsXformsConnection(App.getConnectionDetails()); connection.postXformInstance(patientUuid, xml, successListener, errorListener); } - /** Tries to submit all unsent forms to the server . Returns {@code true} if there are no more - * unsent forms. Otherwise returns {@code false}. - */ - public static final boolean resendFormsToServer(final ContentResolver contentResolver) { - final boolean hasUnsubmittedForms[] = new boolean[]{false}; - final List forms = getUnsetForms(contentResolver); - - //A sync barrier to wait for all returning async submissions - final CountDownLatch countDownLatch = new CountDownLatch(forms.size()); - for(final UnsentForm unsentForm : forms) { - sendFormToServer(unsentForm.patientUuid, unsentForm.formContents, - new Response.Listener() { - @Override public void onResponse(JSONObject response) { - LOG.i("Created new encounter successfully on server. " + response.toString()); - deleteUnsentForm(unsentForm.uuid, contentResolver); - countDownLatch.countDown(); - - } - }, new Response.ErrorListener() { - @Override public void onErrorResponse(VolleyError error) { - //Just log it and flag returning valeue as pendent. It is not necessary to - // keep its content, since this form is already persisted. - LOG.e(error, format("Error resubmitting %s form to server ", unsentForm.uuid)); - hasUnsubmittedForms[0] = true; - countDownLatch.countDown(); - } - }); - } - try { - countDownLatch.await(); - } catch (InterruptedException e) { - LOG.e("Interrupted whilst waiting for unsubmitted forms to be uploaded", e); - return false; - } - - return !hasUnsubmittedForms[0]; - } - - public static void deleteUnsentForm(final String uuid, final ContentResolver contentResolver) { - LOG.i("Removing the unsent form from the db"); - contentResolver.delete(UnsentForms.CONTENT_URI, format("%s='%s'", UnsentForms.UUID, uuid), - null); - } - - /** Returns all local forms which were NOT submitted to the server yet*/ - public static List getUnsetForms(final ContentResolver contentResolver) { - try (Cursor cursor = contentResolver.query(UnsentForms.CONTENT_URI, - new String[]{UnsentForms.UUID, UnsentForms.PATIENT_UUID, UnsentForms.FORM_CONTENTS}, - null, null, null)) { - List unsentForms = new ArrayList<>(); - while (cursor.moveToNext()) { - unsentForms.add(new UnsentForm(Utils.getString(cursor, UnsentForms.UUID, null), - Utils.getString(cursor, UnsentForms.PATIENT_UUID, ""), - Utils.getString(cursor, UnsentForms.FORM_CONTENTS, ""))); - } - return unsentForms; - } - } - private static void handleSubmitError(VolleyError error) { SubmitXformFailedEvent.Reason reason = SubmitXformFailedEvent.Reason.UNKNOWN; @@ -609,9 +625,10 @@ private static String readFromPath(String path) { * {@link #updateObservationCache} to see how the observation itself is saved into db. * The {@link org.projectbuendia.client.sync.controllers.ObservationsSyncPhaseRunnable} * will check if there are unsent observations, and if is the case, it will try to resend it - * prior to pull new ones (See {@link #resendFormsToServer}). + * prior to pull new ones (See {@link #submitUnsetFormsToServer}). */ private static void saveUnsentForm(String patientUuid, String xml, ContentResolver resolver) { + //FIXME: Add snackbar alerting user resolver.insert(UnsentForms.CONTENT_URI, new UnsentForm(UUID.randomUUID().toString(), patientUuid, xml).toContentValues()); } From 87e476c1ea51cc1bed774e3defb3891a9fe7ea7a Mon Sep 17 00:00:00 2001 From: Fabian Tamp Date: Tue, 19 Jan 2016 10:36:10 +0800 Subject: [PATCH 41/69] Enable multidex to temporarily avert the dexocalypse. - Bump build tools version to latest. This is required for Multidex. - Delete ic_launcher from OdkCollect, required because later versions of the AAPT build tool handle duplicate resources as errors, and this file was causing the build to fail. - Add dependency upon the legacy HTTP library. API 23 deprecated Apache HTTP, but we're still using it for Health check code. - Update OdkCollect to latest build tools as well, because it was separately including the legacy Apache HTTP library, which was resulting in conflicts from having the same JAR included twice. Updating to the latest build tools means that it can use the same legacy library as `:app`, which eliminates the conflict. - Update support library versions to 23 so that they work with the latest build tools. - Enable Multidex. Tested on API 21 (L) and 19 (KK) - Remove Guava as a direct dependency, and delete code that used it from `JsonPatient`. Note that Guava is still included by Pebble as a transitive dependency, but I don't want developers on this project to be able to use it in code we control, so we have the flexibility to remove the dependency one day. See https://slack-files.com/T02T5LNM4-F0JQ1UDRV-716ebe431f for details. --- app/build.gradle | 25 ++++++++++++++---- .../java/org/projectbuendia/client/App.java | 9 +++++++ .../client/json/JsonPatient.java | 15 ----------- build.gradle | 2 +- third_party/odkcollect/build.gradle | 14 ++++++---- .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 19388 -> 0 bytes 6 files changed, 39 insertions(+), 26 deletions(-) delete mode 100644 third_party/odkcollect/src/main/res/drawable-xxhdpi/ic_launcher.png diff --git a/app/build.gradle b/app/build.gradle index 86797d56..99163db3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,6 +32,11 @@ android { preDexLibraries = false javaMaxHeapSize = '4g' } + + // Enable multidex support. + defaultConfig { + multiDexEnabled true + } } dependencies { // Build plugins @@ -44,10 +49,9 @@ dependencies { compile project(':third_party:odkcollect') // External dependencies - compile 'com.android.support:appcompat-v7:22.2.0' - compile 'com.android.support:support-annotations:22.2.0' + compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:support-annotations:23.1.1' compile 'com.google.code.gson:gson:2.3' // JSON parser - compile 'com.google.guava:guava:18.0' // Google common libraries compile 'com.jakewharton:butterknife:5.1.2' // View injection compile 'com.mcxiaoke.volley:library:1.0.6' // HTTP framework compile 'com.joanzapata.android:android-iconify:1.0.8' // Font-based icons @@ -62,6 +66,9 @@ dependencies { // Testing androidTestCompile 'com.android.support.test:runner:0.3' + // Explicitly add this dep at 23.1.1, because the above entry depends on 22.2.0, and the + // discrepancy can introduce differences in behaviour between prod and test. + androidTestCompile 'com.android.support:support-annotations:23.1.1' // Espresso androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2' androidTestCompile 'com.android.support.test.espresso:espresso-web:2.2' @@ -71,6 +78,11 @@ dependencies { androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.0' androidTestCompile 'com.google.dexmaker:dexmaker:1.0' androidTestCompile 'org.mockito:mockito-core:1.9.5' + + // Multidex. + // NOTE: This is temporary only! See https://slack-files.com/T02T5LNM4-F0JQ1UDRV-716ebe431f + // for more information. + compile 'com.android.support:multidex:1.0.1' } apply plugin: 'spoon' @@ -170,8 +182,11 @@ logger.info("Default package server root URL: ${packageServerRootUrl}") logger.info("Database encryption password: ${encryptionPassword}") android { - compileSdkVersion 21 - buildToolsVersion '19.1.0' + compileSdkVersion 23 + buildToolsVersion '23.0.2' + // TODO: Port the various health checks to use HttpURLConnection instead and remove this + // dependency. + useLibrary 'org.apache.http.legacy' sourceSets.main { jniLibs.srcDir 'libs' diff --git a/app/src/main/java/org/projectbuendia/client/App.java b/app/src/main/java/org/projectbuendia/client/App.java index d4f1f61d..b84f7d96 100644 --- a/app/src/main/java/org/projectbuendia/client/App.java +++ b/app/src/main/java/org/projectbuendia/client/App.java @@ -12,7 +12,9 @@ package org.projectbuendia.client; import android.app.Application; +import android.content.Context; import android.preference.PreferenceManager; +import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; @@ -85,6 +87,13 @@ public static synchronized OpenMrsConnectionDetails getConnectionDetails() { mHealthMonitor.start(); } + @Override + public void attachBaseContext(Context base) { + // Set up Multidex. + super.attachBaseContext(base); + MultiDex.install(this); + } + public void inject(Object obj) { mObjectGraph.inject(obj); } diff --git a/app/src/main/java/org/projectbuendia/client/json/JsonPatient.java b/app/src/main/java/org/projectbuendia/client/json/JsonPatient.java index feb54edf..a36c932b 100644 --- a/app/src/main/java/org/projectbuendia/client/json/JsonPatient.java +++ b/app/src/main/java/org/projectbuendia/client/json/JsonPatient.java @@ -11,8 +11,6 @@ package org.projectbuendia.client.json; -import com.google.common.base.MoreObjects; - import org.joda.time.LocalDate; import java.io.Serializable; @@ -34,17 +32,4 @@ public class JsonPatient implements Serializable { public JsonPatient() { } - - @Override public String toString() { - return MoreObjects.toStringHelper(this) - .add("uuid", uuid) - .add("voided", voided) - .add("id", id) - .add("given_name", given_name) - .add("family_name", family_name) - .add("sex", sex) - .add("birthdate", birthdate.toString()) - .add("assigned_location", assigned_location) - .toString(); - } } diff --git a/build.gradle b/build.gradle index ab62e909..346dadb7 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:1.2.3' + classpath 'com.android.tools.build:gradle:1.5.0' } } diff --git a/third_party/odkcollect/build.gradle b/third_party/odkcollect/build.gradle index c63b3bbc..785f3b64 100644 --- a/third_party/odkcollect/build.gradle +++ b/third_party/odkcollect/build.gradle @@ -7,8 +7,13 @@ buildscript { apply plugin: 'com.android.library' android { - compileSdkVersion 21 - buildToolsVersion "19.1.0" + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + // Upon switching to API 23, we can use this to include the Apache HTTP jar instead of + // including it manually. + useLibrary 'org.apache.http.legacy' + defaultConfig { minSdkVersion 19 targetSdkVersion 21 @@ -32,8 +37,8 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:21.0.0' - compile 'com.android.support:support-v4:21.0.0' + compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:support-v4:23.1.1' compile 'com.google.code.gson:gson:2.3' compile 'commons-io:commons-io:2.4' compile 'joda-time:joda-time:2.3' @@ -43,6 +48,5 @@ dependencies { compile('org.apache.james:apache-mime4j:0.7.2') { exclude group: 'commons-logging', module: 'commons-logging' } - compile files('libs/httpclientandroidlib-4.2.1.jar') compile files('libs/javarosa-libraries-2014-04-29.jar') } diff --git a/third_party/odkcollect/src/main/res/drawable-xxhdpi/ic_launcher.png b/third_party/odkcollect/src/main/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index 4df18946442ed763bd52cf3adca31617848656fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19388 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 From ec77630cee728a9bdc63112b5eeb7237153501c2 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Mon, 18 Jan 2016 22:30:58 -0800 Subject: [PATCH 42/69] Commenting call to resent cached forms prior to submit the new one due deadlock issue. It must be fixed later --- .../client/ui/OdkActivityLauncher.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index f2115b30..90df83c2 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -19,6 +19,7 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; +import android.os.Looper; import com.android.volley.Response; import com.android.volley.TimeoutError; @@ -340,10 +341,11 @@ public static boolean sendOdkResultToServer( * sent all together in a future moment. * */ - if(!submitUnsetFormsToServer(App.getInstance().getContentResolver())) { - saveUnsentForm(patientUuid, xml, context.getContentResolver()); - return false; - } + //FIXME: Figure out a way to call submitUnsetFormsToServer without blocking the main thread +// if(!submitUnsetFormsToServer(App.getInstance().getContentResolver())) { +// saveUnsentForm(patientUuid, xml, context.getContentResolver()); +// return false; +// } submitFormToServer(patientUuid, xml, new Response.Listener() { @@ -374,6 +376,11 @@ public static boolean sendOdkResultToServer( * unsent forms. Otherwise returns {@code false}. */ public static final boolean submitUnsetFormsToServer(final ContentResolver contentResolver) { + if (Looper.getMainLooper() == Looper.myLooper()) { + // We're on the main thread + throw new RuntimeException("This call is blocking, you should not call it from the main thread"); + } + final boolean hasUnsubmittedForms[] = new boolean[]{false}; final List forms = getUnsetForms(contentResolver); @@ -401,7 +408,7 @@ public static final boolean submitUnsetFormsToServer(final ContentResolver conte }); } try { - //Waiting until all forms submissions return from server + //Waiting until all form submissions return from server countDownLatch.await(); } catch (InterruptedException e) { LOG.e("Interrupted whilst waiting for unsubmitted forms to be uploaded", e); From 16dea7ce5155789c7e21733fbda6684897f47fe8 Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Tue, 19 Jan 2016 00:29:46 -0800 Subject: [PATCH 43/69] Adding snackbar message to offline form submission --- .../client/ui/OdkActivityLauncher.java | 13 ++++----- .../client/ui/chart/PatientChartActivity.java | 27 +++++++++++++------ .../ui/chart/PatientChartController.java | 4 ++- app/src/main/res/values-fr/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index 90df83c2..dc8ca439 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -41,6 +41,7 @@ import org.odk.collect.android.utilities.FileUtils; import org.projectbuendia.client.App; import org.projectbuendia.client.AppSettings; +import org.projectbuendia.client.R; import org.projectbuendia.client.events.FetchXformFailedEvent; import org.projectbuendia.client.events.SubmitXformFailedEvent; import org.projectbuendia.client.events.SubmitXformSucceededEvent; @@ -295,7 +296,7 @@ private static OpenMrsXformIndexEntry findUuid( * @param data the incoming intent */ public static boolean sendOdkResultToServer( - final Context context, + final BaseActivity context, final AppSettings settings, @Nullable final String patientUuid, Intent data) { @@ -553,7 +554,7 @@ private static void submitFormToServer(String patientUuid, String xml, connection.postXformInstance(patientUuid, xml, successListener, errorListener); } - private static void handleSubmitError(VolleyError error) { + private static void handleSubmitError(final VolleyError error) { SubmitXformFailedEvent.Reason reason = SubmitXformFailedEvent.Reason.UNKNOWN; if (error instanceof TimeoutError) { @@ -634,10 +635,10 @@ private static String readFromPath(String path) { * will check if there are unsent observations, and if is the case, it will try to resend it * prior to pull new ones (See {@link #submitUnsetFormsToServer}). */ - private static void saveUnsentForm(String patientUuid, String xml, ContentResolver resolver) { - //FIXME: Add snackbar alerting user - resolver.insert(UnsentForms.CONTENT_URI, new UnsentForm(UUID.randomUUID().toString(), - patientUuid, xml).toContentValues()); + private static void saveUnsentForm(final String patientUuid, final String xml, + final ContentResolver contentResolver) { + contentResolver.insert(UnsentForms.CONTENT_URI, new UnsentForm( + UUID.randomUUID().toString(), patientUuid, xml).toContentValues()); } /** diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java index 42c4943f..20fac2c4 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java @@ -316,7 +316,7 @@ private final class Ui implements PatientChartController.Ui { day >= 1 ? getResources().getString(R.string.day_n, day) : "–"); day = Utils.dayNumberSince(firstSymptomsDate, LocalDate.now()); mSymptomOnsetDaysView.setValue( - day >= 1 ? getResources().getString(R.string.day_n, day) : "–"); + day >= 1 ? getResources().getString(R.string.day_n, day) : "–"); } // TODO/cleanup: We don't need this special logic for the Ebola PCR test results @@ -330,9 +330,9 @@ private final class Ui implements PatientChartController.Ui { Obs pcrLObservation = observations.get(ConceptUuids.PCR_L_UUID); Obs pcrNpObservation = observations.get(ConceptUuids.PCR_NP_UUID); mPcr.setIconDrawable( - new IconDrawable(PatientChartActivity.this, Iconify.IconValue.fa_flask) - .color(0x00000000) - .sizeDp(36)); + new IconDrawable(PatientChartActivity.this, Iconify.IconValue.fa_flask) + .color(0x00000000) + .sizeDp(36)); if ((pcrLObservation == null || pcrLObservation.valueName == null) && (pcrNpObservation == null || pcrNpObservation.valueName == null)) { mPcr.setValue("–"); @@ -419,9 +419,9 @@ public void updatePatientLocationUi(LocationTree locationTree, Patient patient) mPatientLocationView.setValue(locationText); mPatientLocationView.setIconDrawable( - new IconDrawable(PatientChartActivity.this, Iconify.IconValue.fa_map_marker) - .color(0x00000000) - .sizeDp(36)); + new IconDrawable(PatientChartActivity.this, Iconify.IconValue.fa_map_marker) + .color(0x00000000) + .sizeDp(36)); } @Override public void updatePatientDetailsUi(Patient patient) { @@ -437,7 +437,7 @@ public void updatePatientLocationUi(LocationTree locationTree, Patient patient) labels.add("F"); } labels.add(patient.birthdate == null ? "age unknown" - : Utils.birthdateToAge(patient.birthdate, getResources())); // TODO/i18n + : Utils.birthdateToAge(patient.birthdate, getResources())); // TODO/i18n String sexAge = Joiner.on(", ").join(labels); PatientChartActivity.this.setTitle(id + ". " + fullName + SEPARATOR_DOT + sexAge); } @@ -450,6 +450,17 @@ public void updatePatientLocationUi(LocationTree locationTree, Patient patient) BigToast.show(PatientChartActivity.this, errorMessageResource); } + @Override public void showFormSubmissionError(int errorMessageResource) { + PatientChartActivity.this.snackBar(errorMessageResource, + R.string.submit_xform_resubmit, + new View.OnClickListener() { + @Override public void onClick(View view) { + OdkActivityLauncher.submitUnsetFormsToServer( + PatientChartActivity.this.getContentResolver()); + } + }, 995, false); //TODO: Define proper priority + } + @Override public synchronized void fetchAndShowXform( int requestCode, String formUuid, org.odk.collect.android.model.Patient patient, Preset preset) { diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java index 54d6b85d..858f0fb5 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java @@ -168,6 +168,8 @@ void updateTilesAndGrid( /** Displays an error with the given resource and optional substitution args. */ void showError(int errorResource, Object... args); + void showFormSubmissionError(int errorMessageResource); + /** Starts a new form activity to collect observations from the user. */ void fetchAndShowXform( int requestCode, String formUuid, org.odk.collect.android.model.Patient patient, @@ -693,7 +695,7 @@ public void onEventMainThread(SubmitXformFailedEvent event) { default: errorMessageResource = R.string.submit_xform_failed_unknown_reason; } - mUi.showError(errorMessageResource); + mUi.showFormSubmissionError(errorMessageResource); } public void onEventMainThread(FetchXformSucceededEvent event) { diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2008db39..1a82ec96 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -221,6 +221,8 @@ S\'il vous plaît patienter pendant que les données du patient et l\'emplacemen Echec de soumission de formulaire. Echec de soumission - erreur d\'authentication. Connexion interrompue après le délai envoyant formulaire au serveur. + Error submitting form to the server: It will be saved to be resent later. + Resoumettre Envoi de formulaire S\'il vous plaît patienter pendant que les données du formulaire sont soumis. État de conscience diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a1c8bd47..1ac15191 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -222,6 +222,8 @@ Something went wrong submitting the form. Error submitting form: could not authenticate with server. Timed out while sending form to the server. + Error submitting form to the server: It will be saved to be resent later. + Resubmit Submitting form Please wait while the form data is submitted. Consciousness From 3e89deb5ae3d0ca1872d649267ce78baa007c60c Mon Sep 17 00:00:00 2001 From: Leonardo Lima de Vasconcellos Date: Tue, 19 Jan 2016 19:58:10 -0200 Subject: [PATCH 44/69] This fixes #225 --- app/src/main/assets/chart.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/chart.html b/app/src/main/assets/chart.html index 24379136..cdd57984 100644 --- a/app/src/main/assets/chart.html +++ b/app/src/main/assets/chart.html @@ -118,7 +118,7 @@
{{order.medication}}
-
{{order.dosage}} 
+
{{order.dosage}} {{order.frequency != null ? order.frequency + 'x daily' : ''}}
{% set previousActive = false %} {% set future = false %} From 2c459bf310e2424152fe9b028ee8f2652580d16a Mon Sep 17 00:00:00 2001 From: Vinicius Boson Kairala Date: Wed, 20 Jan 2016 00:40:35 -0800 Subject: [PATCH 45/69] Creating AsyncTasks to avoid resend forms from Main Thread --- .../client/events/SubmitXformFailedEvent.java | 3 +- .../client/ui/OdkActivityLauncher.java | 12 +++--- .../client/ui/chart/PatientChartActivity.java | 41 ++++++++++++++++--- .../ui/chart/PatientChartController.java | 9 ++-- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 51 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/projectbuendia/client/events/SubmitXformFailedEvent.java b/app/src/main/java/org/projectbuendia/client/events/SubmitXformFailedEvent.java index f5508b82..4c0940d0 100644 --- a/app/src/main/java/org/projectbuendia/client/events/SubmitXformFailedEvent.java +++ b/app/src/main/java/org/projectbuendia/client/events/SubmitXformFailedEvent.java @@ -25,7 +25,8 @@ public enum Reason { SERVER_BAD_ENDPOINT, SERVER_TIMEOUT, SERVER_ERROR, - CLIENT_ERROR + CLIENT_ERROR, + PENDENT_FORM_SUBMISSION } public SubmitXformFailedEvent(Reason reason, @Nullable Exception exception) { diff --git a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java index dc8ca439..6a157087 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java +++ b/app/src/main/java/org/projectbuendia/client/ui/OdkActivityLauncher.java @@ -80,6 +80,8 @@ .CONTENT_ITEM_TYPE; import static org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns.INSTANCE_FILE_PATH; + +import static org.projectbuendia.client.events.SubmitXformFailedEvent.Reason.PENDENT_FORM_SUBMISSION; import static org.projectbuendia.client.providers.Contracts.UnsentForms; /** Convenience class for launching ODK to display an Xform. */ @@ -342,11 +344,11 @@ public static boolean sendOdkResultToServer( * sent all together in a future moment. * */ - //FIXME: Figure out a way to call submitUnsetFormsToServer without blocking the main thread -// if(!submitUnsetFormsToServer(App.getInstance().getContentResolver())) { -// saveUnsentForm(patientUuid, xml, context.getContentResolver()); -// return false; -// } + if(!submitUnsetFormsToServer(App.getInstance().getContentResolver())) { + saveUnsentForm(patientUuid, xml, context.getContentResolver()); + EventBus.getDefault().post(new SubmitXformFailedEvent(PENDENT_FORM_SUBMISSION, null)); + return false; + } submitFormToServer(patientUuid, xml, new Response.Listener() { diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java index 20fac2c4..34688dd7 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java @@ -12,9 +12,11 @@ package org.projectbuendia.client.ui.chart; import android.app.ProgressDialog; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.graphics.Point; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.view.Menu; @@ -95,6 +97,7 @@ public final class PatientChartActivity extends BaseLoggedInActivity { private boolean mIsFetchingXform = false; private ProgressDialog mFormLoadingDialog; private ProgressDialog mFormSubmissionDialog; + private Ui mUi; @Inject AppModel mAppModel; @Inject EventBus mEventBus; @@ -231,10 +234,11 @@ public void onPageFinished(WebView view, String url) { }); mChartRenderer = new ChartRenderer(mGridWebView, getResources()); + mUi = new Ui(); + final OdkResultSender odkResultSender = new OdkResultSender() { - @Override public boolean sendOdkResultToServer(String patientUuid, Intent data) { - return OdkActivityLauncher.sendOdkResultToServer(PatientChartActivity.this, - mSettings, patientUuid, data); + @Override public void sendOdkResultToServer(String patientUuid, Intent data) { + new SubmitOdkFormAsyncTask().execute(patientUuid, data, mUi); } }; final MinimalHandler minimalHandler = new MinimalHandler() { @@ -248,7 +252,7 @@ public void onPageFinished(WebView view, String url) { mAppModel, new EventBusWrapper(mEventBus), mCrudEventBusProvider.get(), - new Ui(), + mUi, getIntent().getStringExtra("uuid"), odkResultSender, mChartDataHelper, @@ -298,6 +302,32 @@ private String getFormattedPcrString(double pcrValue) { String.format("%1$.1f", pcrValue); } + private class SubmitOdkFormAsyncTask extends AsyncTask { + private Ui ui; + + @Override protected Boolean doInBackground(Object... params) { + this.ui = (Ui) params[2]; + return OdkActivityLauncher.sendOdkResultToServer(PatientChartActivity.this, + mSettings, (String)params[0] /**patientUuid*/, (Intent)params[1] /**data*/); + } + + protected void onPostExecute(Boolean result) { + ui.showFormSubmissionDialog(result); + } + } + + private class submitUnsetFormsAsyncTask extends AsyncTask { + @Override protected Boolean doInBackground(ContentResolver... params) { + return OdkActivityLauncher.submitUnsetFormsToServer((ContentResolver)params[0]); + } + + protected void onPostExecute(Boolean result) { + if(result) { + PatientChartActivity.this.snackBarDismiss(R.string.submit_xform_resubmit); + } + } + } + private final class Ui implements PatientChartController.Ui { @Override public void setTitle(String title) { PatientChartActivity.this.setTitle(title); @@ -455,8 +485,7 @@ public void updatePatientLocationUi(LocationTree locationTree, Patient patient) R.string.submit_xform_resubmit, new View.OnClickListener() { @Override public void onClick(View view) { - OdkActivityLauncher.submitUnsetFormsToServer( - PatientChartActivity.this.getContentResolver()); + new submitUnsetFormsAsyncTask().execute(PatientChartActivity.this.getContentResolver()); } }, 995, false); //TODO: Define proper priority } diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java index 858f0fb5..fb47b1e1 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java @@ -187,7 +187,7 @@ void showOrderExecutionDialog(org.projectbuendia.client.sync.Order order, Interv /** Sends ODK form data. */ public interface OdkResultSender { - boolean sendOdkResultToServer( + void sendOdkResultToServer( @Nullable String patientUuid, Intent data); } @@ -294,9 +294,7 @@ public void onXFormResult(final int requestCode, final int resultCode, final Int "form", request.formUuid, "patient_uuid", request.patientUuid); if(isSubmissionCanceled) return; - final boolean isSubmittingForm = mOdkResultSender.sendOdkResultToServer(request.patientUuid, - data); - mUi.showFormSubmissionDialog(isSubmittingForm); + mOdkResultSender.sendOdkResultToServer(request.patientUuid, data); } FormRequest popFormRequest(int requestIndex) { @@ -692,6 +690,9 @@ public void onEventMainThread(SubmitXformFailedEvent event) { case SERVER_TIMEOUT: errorMessageResource = R.string.submit_xform_failed_server_timeout; break; + case PENDENT_FORM_SUBMISSION: + errorMessageResource = R.string.submit_xform_failed_pendent_form_submission; + break; default: errorMessageResource = R.string.submit_xform_failed_unknown_reason; } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1a82ec96..d5fc3e41 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -222,6 +222,7 @@ S\'il vous plaît patienter pendant que les données du patient et l\'emplacemen Echec de soumission - erreur d\'authentication. Connexion interrompue après le délai envoyant formulaire au serveur. Error submitting form to the server: It will be saved to be resent later. + There are still not submitted forms. Resoumettre Envoi de formulaire S\'il vous plaît patienter pendant que les données du formulaire sont soumis. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ac15191..9b06f08f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -223,6 +223,7 @@ Error submitting form: could not authenticate with server. Timed out while sending form to the server. Error submitting form to the server: It will be saved to be resent later. + There are still not submitted forms. Resubmit Submitting form Please wait while the form data is submitted. From e9367621be97064b7299739af2b93980d4daf08a Mon Sep 17 00:00:00 2001 From: Dan Cunningham Date: Wed, 20 Jan 2016 13:22:20 +0000 Subject: [PATCH 46/69] Make start and stop (end) times for cell accessible via javascript --- app/src/main/assets/chart.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/assets/chart.html b/app/src/main/assets/chart.html index 24379136..df825fa4 100644 --- a/app/src/main/assets/chart.html +++ b/app/src/main/assets/chart.html @@ -77,6 +77,8 @@ {% set class = summaryValue | format_values(row.item.cssClass) %} {% set style = summaryValue | format_values(row.item.cssStyle) %} Echec de soumission - erreur d\'authentication. Connexion interrompue après le délai envoyant formulaire au serveur. Error submitting form to the server: It will be saved to be resent later. - There are still not submitted forms. + There are still pending forms to be submitted. Resoumettre Envoi de formulaire S\'il vous plaît patienter pendant que les données du formulaire sont soumis. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b06f08f..f9c49ec0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -223,7 +223,7 @@ Error submitting form: could not authenticate with server. Timed out while sending form to the server. Error submitting form to the server: It will be saved to be resent later. - There are still not submitted forms. + There are still pending forms to be submitted. Resubmit Submitting form Please wait while the form data is submitted. From c234d9e36c36a14ad98934bfa84602e9c74ecaf1 Mon Sep 17 00:00:00 2001 From: Fabian Tamp Date: Tue, 19 Jan 2016 11:54:32 +0800 Subject: [PATCH 48/69] Add a notes panel to the patient chart screen. - Clean up adding encounters. - Previously, there was functionality to try and "guess" the type of observation. Remove this, the server has knowledge of concept type and can figure it out from the question UUID, and value type isn't used on the client. - Allow an `enterer_uuid` to be specified when an encounter is created, which allows the encounter to be attributed to a user. - Change representation of observations in an encounter from a Map of Question UUIDs --> Values to a List of JsonObservations. This means that we can process them consistently to the way the Observation syncing code does. - Add a notes panel, which slides up from the bottom of the patient chart activity and allows notes to be entered by the user. --- app/build.gradle | 2 + .../events/data/EncounterAddFailedEvent.java | 5 +- .../client/json/JsonEncounter.java | 6 +- .../client/models/AppModel.java | 8 +- .../client/models/Encounter.java | 67 +++--- .../client/models/PatientDelta.java | 37 ++- .../client/models/tasks/AddEncounterTask.java | 32 ++- .../org/projectbuendia/client/net/Server.java | 4 +- .../client/ui/chart/PatientChartActivity.java | 110 ++++++++- .../ui/chart/PatientChartController.java | 64 ++++- .../chart/PatientObservationsListAdapter.java | 142 +++++++++++ .../client/ui/lists/LocationListFragment.java | 1 + .../res/layout/fragment_patient_chart.xml | 220 +++++++++++++----- .../notes_list_adapter_note_template.xml | 23 ++ app/src/main/res/values/colors.xml | 4 + app/src/main/res/values/dimens.xml | 14 ++ app/src/main/res/values/strings.xml | 12 + 17 files changed, 601 insertions(+), 150 deletions(-) create mode 100644 app/src/main/java/org/projectbuendia/client/ui/chart/PatientObservationsListAdapter.java create mode 100644 app/src/main/res/layout/notes_list_adapter_note_template.xml create mode 100644 app/src/main/res/values/dimens.xml diff --git a/app/build.gradle b/app/build.gradle index 99163db3..5fc6c6de 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,6 +63,8 @@ dependencies { compile 'com.mitchellbosecke:pebble:1.5.1' // HTML templating compile 'org.slf4j:slf4j-simple:1.7.12' // HTML templating dependency compile 'org.apache.commons:commons-lang3:3.4' + // Magic sliding panel that we use for the notes view. + compile 'com.sothree.slidinguppanel:library:3.2.1' // Testing androidTestCompile 'com.android.support.test:runner:0.3' diff --git a/app/src/main/java/org/projectbuendia/client/events/data/EncounterAddFailedEvent.java b/app/src/main/java/org/projectbuendia/client/events/data/EncounterAddFailedEvent.java index 546d4583..6da7bcd7 100644 --- a/app/src/main/java/org/projectbuendia/client/events/data/EncounterAddFailedEvent.java +++ b/app/src/main/java/org/projectbuendia/client/events/data/EncounterAddFailedEvent.java @@ -12,6 +12,7 @@ package org.projectbuendia.client.events.data; import org.projectbuendia.client.events.DefaultCrudEventBus; +import org.projectbuendia.client.models.Encounter; /** * An event bus event indicating that adding an encounter failed. @@ -21,6 +22,7 @@ public class EncounterAddFailedEvent { public final Reason reason; public final Exception exception; + public final Encounter encounter; public enum Reason { UNKNOWN, @@ -33,7 +35,8 @@ public enum Reason { FAILED_TO_FETCH_SAVED_OBSERVATION } - public EncounterAddFailedEvent(Reason reason, Exception exception) { + public EncounterAddFailedEvent(Encounter encounter, Reason reason, Exception exception) { + this.encounter = encounter; this.reason = reason; this.exception = exception; } diff --git a/app/src/main/java/org/projectbuendia/client/json/JsonEncounter.java b/app/src/main/java/org/projectbuendia/client/json/JsonEncounter.java index 8e75de1a..4e043b4e 100644 --- a/app/src/main/java/org/projectbuendia/client/json/JsonEncounter.java +++ b/app/src/main/java/org/projectbuendia/client/json/JsonEncounter.java @@ -13,15 +13,13 @@ import org.joda.time.DateTime; -import java.util.Map; +import java.util.List; /** JSON representation of an OpenMRS Encounter; call Serializers.registerTo before use. */ public class JsonEncounter { public String patient_uuid; public String uuid; public DateTime timestamp; - public String enterer_id; - /** A {conceptUuid: value} map, where value can be a number, string, or answer UUID. */ - public Map observations; + public List observations; public String[] order_uuids; // orders executed during this encounter } diff --git a/app/src/main/java/org/projectbuendia/client/models/AppModel.java b/app/src/main/java/org/projectbuendia/client/models/AppModel.java index 5eb7a738..c607f275 100644 --- a/app/src/main/java/org/projectbuendia/client/models/AppModel.java +++ b/app/src/main/java/org/projectbuendia/client/models/AppModel.java @@ -36,6 +36,8 @@ import org.projectbuendia.client.utils.Logger; import org.projectbuendia.client.utils.Utils; +import javax.annotation.Nullable; + import de.greenrobot.event.NoSubscriberEvent; /** @@ -194,10 +196,10 @@ public void deleteOrder(CrudEventBus bus, String orderUuid) { * Asynchronously adds an encounter that records an order as executed, posting a * {@link ItemCreatedEvent} when complete. */ - public void addOrderExecutedEncounter(CrudEventBus bus, Patient patient, String orderUuid) { + public void addOrderExecutedEncounter( + CrudEventBus bus, Patient patient, String orderUuid, @Nullable String userUuid) { addEncounter(bus, patient, new Encounter( - patient.uuid, null, DateTime.now(), null, new String[]{orderUuid} - )); + patient.uuid, null, DateTime.now(), null, new String[]{orderUuid}, userUuid)); } /** diff --git a/app/src/main/java/org/projectbuendia/client/models/Encounter.java b/app/src/main/java/org/projectbuendia/client/models/Encounter.java index 2a05ab19..5d8da8ec 100644 --- a/app/src/main/java/org/projectbuendia/client/models/Encounter.java +++ b/app/src/main/java/org/projectbuendia/client/models/Encounter.java @@ -18,14 +18,13 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.projectbuendia.client.net.Server; import org.projectbuendia.client.json.JsonEncounter; +import org.projectbuendia.client.json.JsonObservation; +import org.projectbuendia.client.net.Server; import org.projectbuendia.client.providers.Contracts.Observations; -import org.projectbuendia.client.utils.Logger; import java.util.ArrayList; import java.util.List; -import java.util.Map; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -37,10 +36,6 @@ *
* https://wiki.openmrs.org/display/docs/Encounters+and+observations" * - *

- *

NOTE: Because of lack of typing info from the server, {@link Encounter} attempts to - * determine the most appropriate type, but this typing is not guaranteed to succeed; also, - * currently only DATE and UUID (coded) types are supported. */ @Immutable public class Encounter extends Base { @@ -50,7 +45,7 @@ public class Encounter extends Base { public final DateTime timestamp; public final Observation[] observations; public final String[] orderUuids; - private static final Logger LOG = Logger.create(); + public final @Nullable String userUuid; /** * Creates a new Encounter for the given patient. @@ -65,13 +60,15 @@ public Encounter( @Nullable String encounterUuid, DateTime timestamp, Observation[] observations, - String[] orderUuids) { + String[] orderUuids, + @Nullable String userUuid) { id = encounterUuid; this.patientUuid = patientUuid; this.encounterUuid = id; this.timestamp = timestamp; this.observations = observations == null ? new Observation[] {} : observations; this.orderUuids = orderUuids == null ? new String[] {} : orderUuids; + this.userUuid = userUuid; } /** @@ -79,18 +76,17 @@ public Encounter( * {@link JsonEncounter} object and corresponding patient UUID. */ public static Encounter fromJson(String patientUuid, JsonEncounter encounter) { - List observations = new ArrayList(); + List observations = new ArrayList<>(); if (encounter.observations != null) { - for (Map.Entry observation : encounter.observations.entrySet()) { + for (JsonObservation observation : encounter.observations) { observations.add(new Observation( - (String) observation.getKey(), - (String) observation.getValue(), - Observation.estimatedTypeFor((String) observation.getValue()) + observation.concept_uuid, + observation.value )); } } return new Encounter(patientUuid, encounter.uuid, encounter.timestamp, - observations.toArray(new Observation[observations.size()]), encounter.order_uuids); + observations.toArray(new Observation[observations.size()]), encounter.order_uuids, null); } /** Serializes this into a {@link JSONObject}. */ @@ -101,12 +97,8 @@ public JSONObject toJson() throws JSONException { if (observations.length > 0) { JSONArray observationsJson = new JSONArray(); for (Observation obs : observations) { - JSONObject observationJson = new JSONObject(); - observationJson.put(Server.OBSERVATION_QUESTION_UUID, obs.conceptUuid); - String valueKey = obs.type == Observation.Type.DATE ? - Server.OBSERVATION_ANSWER_DATE : Server.OBSERVATION_ANSWER_UUID; - observationJson.put(valueKey, obs.value); - observationsJson.put(observationJson); + + observationsJson.put(obs.toJson()); } json.put(Server.ENCOUNTER_OBSERVATIONS_KEY, observationsJson); } @@ -117,6 +109,7 @@ public JSONObject toJson() throws JSONException { } json.put(Server.ENCOUNTER_ORDER_UUIDS, orderUuidsJson); } + json.put(Server.ENCOUNTER_USER_UUID, userUuid); return json; } @@ -153,31 +146,23 @@ public ContentValues[] toContentValuesArray() { public static final class Observation { public final String conceptUuid; public final String value; - public final Type type; - - /** Data type of the observation. */ - public enum Type { - DATE, - NON_DATE - } - public Observation(String conceptUuid, String value, Type type) { + public Observation(String conceptUuid, String value) { this.conceptUuid = conceptUuid; this.value = value; - this.type = type; } - /** - * Produces a best guess for the type of a given value, since the server doesn't give us - * typing information. - */ - public static Type estimatedTypeFor(String value) { + public JSONObject toJson() { + JSONObject observationJson = new JSONObject(); try { - new DateTime(Long.parseLong(value)); - return Type.DATE; - } catch (Exception e) { - return Type.NON_DATE; + observationJson.put(Server.OBSERVATION_QUESTION_UUID, conceptUuid); + observationJson.put(Server.OBSERVATION_ANSWER, value); + } catch (JSONException jsonException) { + // Should never occur, JSONException is only thrown for a null key or an invalid + // numeric value, neither of which will occur in this API. + throw new RuntimeException(jsonException); } + return observationJson; } } @@ -208,11 +193,11 @@ public Loader(String patientUuid) { String value = cursor.getString(cursor.getColumnIndex(Observations.VALUE)); observations.add(new Observation( cursor.getString(cursor.getColumnIndex(Observations.CONCEPT_UUID)), - value, Observation.estimatedTypeFor(value) + value )); } return new Encounter(mPatientUuid, encounterUuid, new DateTime(millis), - observations.toArray(new Observation[observations.size()]), null); + observations.toArray(new Observation[observations.size()]), null, null); } } } diff --git a/app/src/main/java/org/projectbuendia/client/models/PatientDelta.java b/app/src/main/java/org/projectbuendia/client/models/PatientDelta.java index 0cf8b7a7..50b839c9 100644 --- a/app/src/main/java/org/projectbuendia/client/models/PatientDelta.java +++ b/app/src/main/java/org/projectbuendia/client/models/PatientDelta.java @@ -15,13 +15,12 @@ import com.google.common.base.Optional; -import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.projectbuendia.client.net.Server; import org.projectbuendia.client.json.JsonPatient; +import org.projectbuendia.client.net.Server; import org.projectbuendia.client.providers.Contracts; import org.projectbuendia.client.utils.Logger; import org.projectbuendia.client.utils.Utils; @@ -103,24 +102,24 @@ public boolean toJson(JSONObject json) { JSONArray observations = new JSONArray(); if (admissionDate.isPresent()) { - JSONObject observation = new JSONObject(); - observation.put(Server.OBSERVATION_QUESTION_UUID, ConceptUuids.ADMISSION_DATE_UUID); - observation.put( - Server.OBSERVATION_ANSWER_DATE, - Utils.toString(admissionDate.get())); - observations.put(observation); + JSONObject jsonObs = + new Encounter.Observation( + ConceptUuids.ADMISSION_DATE_UUID, + Utils.toString(admissionDate.get())) + .toJson(); + + observations.put(jsonObs); } if (firstSymptomDate.isPresent()) { - JSONObject observation = new JSONObject(); - observation.put(Server.OBSERVATION_QUESTION_UUID, ConceptUuids.FIRST_SYMPTOM_DATE_UUID); - observation.put( - Server.OBSERVATION_ANSWER_DATE, - Utils.toString(firstSymptomDate.get())); - observations.put(observation); - } - if (observations != null) { - json.put(Server.ENCOUNTER_OBSERVATIONS_KEY, observations); + JSONObject jsonObs = + new Encounter.Observation( + ConceptUuids.FIRST_SYMPTOM_DATE_UUID, + Utils.toString(firstSymptomDate.get())) + .toJson(); + + observations.put(jsonObs); } + json.put(Server.ENCOUNTER_OBSERVATIONS_KEY, observations); if (assignedLocationUuid.isPresent()) { json.put( @@ -141,8 +140,4 @@ private static JSONObject getLocationObject(String assignedLocationUuid) throws location.put("uuid", assignedLocationUuid); return location; } - - private static long getTimestamp(DateTime dateTime) { - return dateTime.toInstant().getMillis()/1000; - } } diff --git a/app/src/main/java/org/projectbuendia/client/models/tasks/AddEncounterTask.java b/app/src/main/java/org/projectbuendia/client/models/tasks/AddEncounterTask.java index 66a48658..0503d076 100644 --- a/app/src/main/java/org/projectbuendia/client/models/tasks/AddEncounterTask.java +++ b/app/src/main/java/org/projectbuendia/client/models/tasks/AddEncounterTask.java @@ -90,7 +90,8 @@ public AddEncounterTask( try { jsonEncounter = future.get(); } catch (InterruptedException e) { - return new EncounterAddFailedEvent(EncounterAddFailedEvent.Reason.INTERRUPTED, e); + return new EncounterAddFailedEvent( + mEncounter, EncounterAddFailedEvent.Reason.INTERRUPTED, e); } catch (ExecutionException e) { LOG.e(e, "Server error while adding encounter"); @@ -106,7 +107,8 @@ public AddEncounterTask( } LOG.e("Error response: %s", ((VolleyError) e.getCause()).networkResponse); - return new EncounterAddFailedEvent(reason, (VolleyError) e.getCause()); + return new EncounterAddFailedEvent( + mEncounter, reason, (VolleyError) e.getCause()); } if (jsonEncounter.uuid == null) { @@ -114,10 +116,16 @@ public AddEncounterTask( "Although the server reported an encounter successfully added, it did not " + "return a UUID for that encounter. This indicates a server error."); - return new EncounterAddFailedEvent( - EncounterAddFailedEvent.Reason.FAILED_TO_SAVE_ON_SERVER, null /*exception*/); + return new EncounterAddFailedEvent(mEncounter, + EncounterAddFailedEvent.Reason.FAILED_TO_SAVE_ON_SERVER, null /*exception*/); } + // TODO: the encounter database saving code here doesn't correctly attribute observations to + // the user that created them, despite the fact that this data is sent from the server. + // This will be remedied on the next sync. + // Instead of adding a workaround here, we should unify the code that deals with + // observations as part of encounters and the code that deals with observations as entities + // that get synced. Encounter encounter = Encounter.fromJson(mPatient.uuid, jsonEncounter); ContentValues[] values = encounter.toContentValuesArray(); if (values.length > 0) { @@ -126,9 +134,9 @@ public AddEncounterTask( if (inserted != values.length) { LOG.w("Inserted %d observations for encounter. Expected: %d", inserted, encounter.observations.length); - return new EncounterAddFailedEvent( - EncounterAddFailedEvent.Reason.INVALID_NUMBER_OF_OBSERVATIONS_SAVED, - null /*exception*/); + return new EncounterAddFailedEvent(mEncounter, + EncounterAddFailedEvent.Reason.INVALID_NUMBER_OF_OBSERVATIONS_SAVED, + null /*exception*/); } } else { LOG.w("Encounter was sent to the server but contained no observations."); @@ -151,8 +159,8 @@ public AddEncounterTask( "Although an encounter add ostensibly succeeded, no UUID was set for the newly-" + "added encounter. This indicates a programming error."); - mBus.post(new EncounterAddFailedEvent( - EncounterAddFailedEvent.Reason.UNKNOWN, null /*exception*/)); + mBus.post(new EncounterAddFailedEvent(mEncounter, + EncounterAddFailedEvent.Reason.UNKNOWN, null /*exception*/)); return; } @@ -179,9 +187,9 @@ public void onEventMainThread(ItemFetchedEvent event) { } public void onEventMainThread(ItemFetchFailedEvent event) { - mBus.post(new EncounterAddFailedEvent( - EncounterAddFailedEvent.Reason.FAILED_TO_FETCH_SAVED_OBSERVATION, - new Exception(event.error))); + mBus.post(new EncounterAddFailedEvent(mEncounter, + EncounterAddFailedEvent.Reason.FAILED_TO_FETCH_SAVED_OBSERVATION, + new Exception(event.error))); mBus.unregister(this); } } diff --git a/app/src/main/java/org/projectbuendia/client/net/Server.java b/app/src/main/java/org/projectbuendia/client/net/Server.java index 4c1e6353..c83e4bb0 100644 --- a/app/src/main/java/org/projectbuendia/client/net/Server.java +++ b/app/src/main/java/org/projectbuendia/client/net/Server.java @@ -42,9 +42,9 @@ public interface Server { public static final String ENCOUNTER_OBSERVATIONS_KEY = "observations"; public static final String ENCOUNTER_TIMESTAMP = "timestamp"; public static final String ENCOUNTER_ORDER_UUIDS = "order_uuids"; + public static final String ENCOUNTER_USER_UUID = "enterer_uuid"; public static final String OBSERVATION_QUESTION_UUID = "question_uuid"; - public static final String OBSERVATION_ANSWER_DATE = "answer_date"; - public static final String OBSERVATION_ANSWER_UUID = "answer_uuid"; + public static final String OBSERVATION_ANSWER = "answer_value"; /** * Logs an event by sending a dummy request to the server. (The server logs diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java index 3888f8ab..f3fed7bd 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartActivity.java @@ -12,24 +12,34 @@ package org.projectbuendia.client.ui.chart; import android.app.ActionBar; +import android.app.LoaderManager; import android.app.ProgressDialog; import android.content.Context; +import android.content.CursorLoader; import android.content.Intent; +import android.content.Loader; +import android.database.Cursor; import android.graphics.Point; import android.os.Bundle; import android.os.Handler; +import android.text.Editable; +import android.text.TextWatcher; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; import android.webkit.WebView; import android.webkit.WebViewClient; +import android.widget.EditText; +import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; import com.google.common.base.Joiner; import com.joanzapata.android.iconify.IconDrawable; import com.joanzapata.android.iconify.Iconify; +import com.sothree.slidinguppanel.SlidingUpPanelLayout; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -104,13 +114,17 @@ public final class PatientChartActivity extends BaseLoggedInActivity { @Inject SyncManager mSyncManager; @Inject ChartDataHelper mChartDataHelper; @Inject AppSettings mSettings; - @InjectView(R.id.patient_chart_root) ViewGroup mRootView; + @InjectView(R.id.patient_chart_root) SlidingUpPanelLayout mRootView; @InjectView(R.id.attribute_location) PatientAttributeView mPatientLocationView; @InjectView(R.id.attribute_admission_days) PatientAttributeView mAdmissionDaysView; @InjectView(R.id.attribute_symptoms_onset_days) PatientAttributeView mSymptomOnsetDaysView; @InjectView(R.id.attribute_pcr) PatientAttributeView mPcr; @InjectView(R.id.patient_chart_pregnant) TextView mPatientPregnantOrIvView; @InjectView(R.id.chart_webview) WebView mGridWebView; + @InjectView(R.id.notes_panel_list) ListView mNotesList; + @InjectView(R.id.notes_panel_text_entry) EditText mAddNoteEntryText; + @InjectView(R.id.notes_panel_btn_save) View mAddNoteButton; + @InjectView(R.id.notes_panel_submit_spinner) View mAddNoteWaitingSpinner; private static final String EN_DASH = "\u2013"; @@ -194,6 +208,16 @@ public static void start(Context caller, String uuid) { return super.onOptionsItemSelected(item); } + @Override + public void onBackPressed() { + // If the notes view is open, collapse it before navigating back up to the parent activity. + if (mRootView.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + mRootView.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } else { + super.onBackPressed(); + } + } + @Override protected void onCreateImpl(Bundle savedInstanceState) { super.onCreateImpl(savedInstanceState); setContentView(R.layout.fragment_patient_chart); @@ -274,6 +298,72 @@ public void onPageFinished(WebView view, String url) { }); initChartMenu(); + + // Hide IME if the notes panel closes. + mRootView.setPanelSlideListener(new SlidingUpPanelLayout.SimplePanelSlideListener() { + @Override + public void onPanelCollapsed(View panel) { + View view = getCurrentFocus(); + if (view != null) { + InputMethodManager imm = + (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + }); + // Set up an adapter for the notes list, and register callbacks with the LoaderManager + // so that the list updates automatically. + PatientObservationsListAdapter adapter = new PatientObservationsListAdapter(this); + mNotesList.setAdapter(adapter); + getLoaderManager().initLoader(0, null, + new PatientObservationsListAdapter.ObservationsListLoaderCallbacks( + this, + getIntent().getStringExtra("uuid"), + ConceptUuids.NOTES_UUID, + adapter)); + + mNotesList.setEmptyView(findViewById(R.id.notes_panel_list_empty)); + mAddNoteEntryText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + mAddNoteButton.setEnabled(s.length() > 0); + } + }); + // Trigger the text changed listener. + mAddNoteEntryText.setText(""); + setNoteSubmissionState(false); + + mAddNoteButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mController.addNote(mAddNoteEntryText.getText().toString()); + // Lock out the text box and the button. + setNoteSubmissionState(true); + } + }); + } + + private void setNoteSubmissionState(boolean isSubmitting) { + if (isSubmitting) { + // Replace the "Submit" button with a spinner + mAddNoteButton.setVisibility(View.INVISIBLE); + mAddNoteWaitingSpinner.setVisibility(View.VISIBLE); + // Disable text entry. + mAddNoteEntryText.setEnabled(false); + } else { + mAddNoteButton.setVisibility(View.VISIBLE); + mAddNoteWaitingSpinner.setVisibility(View.INVISIBLE); + // Enable text entry. + mAddNoteEntryText.setEnabled(true); + } } private void initChartMenu() { @@ -517,6 +607,22 @@ public void updatePatientLocationUi(LocationTree locationTree, Patient patient) .show(getSupportFragmentManager(), null); } + @Override + public void indicateNoteSubmitted() { + setNoteSubmissionState(false); + mAddNoteEntryText.setText(""); + //TODO: scroll to bottom to show the newly added note. + } + + @Override + public void indicateNoteSubmissionFailed() { + setNoteSubmissionState(false); + Toast.makeText( + PatientChartActivity.this, + "Failed to submit note.", + Toast.LENGTH_SHORT).show(); + } + @Override public void showOrderExecutionDialog( Order order, Interval interval, List executionTimes) { OrderExecutionDialogFragment.newInstance(order, interval, executionTimes) diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java index 3da03e12..12b58605 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientChartController.java @@ -38,6 +38,7 @@ import org.projectbuendia.client.events.actions.VoidObservationsRequestEvent; import org.projectbuendia.client.events.data.AppLocationTreeFetchedEvent; import org.projectbuendia.client.events.data.EncounterAddFailedEvent; +import org.projectbuendia.client.events.data.ItemCreatedEvent; import org.projectbuendia.client.events.data.ItemDeletedEvent; import org.projectbuendia.client.events.data.ItemFetchedEvent; import org.projectbuendia.client.events.data.PatientUpdateFailedEvent; @@ -74,7 +75,6 @@ final class PatientChartController implements ChartRenderer.GridJsInterface { private static final Logger LOG = Logger.create(); - private static final boolean DEBUG = true; private static final String KEY_PENDING_UUIDS = "pendingUuids"; // Form UUIDs specific to Ebola deployments. @@ -99,7 +99,6 @@ final class PatientChartController implements ChartRenderer.GridJsInterface { // the savedInstanceState. // TODO: Use a map for this instead of an array. private final String[] mPatientUuids; - private int mNextIndex = 0; private Patient mPatient = Patient.builder().build(); private LocationTree mLocationTree; @@ -129,6 +128,8 @@ final class PatientChartController implements ChartRenderer.GridJsInterface { // Store chart's last scroll position private Point mLastScrollPosition; + private Encounter mPendingNotesEncounter; + public Point getLastScrollPosition() { return mLastScrollPosition; } @@ -185,6 +186,8 @@ void showOrderExecutionDialog(Order order, Interval interval, List executionTimes); void showEditPatientDialog(Patient patient); void showObservationsDialog(ArrayList obs); + void indicateNoteSubmitted(); + void indicateNoteSubmissionFailed(); } /** Sends ODK form data. */ @@ -361,6 +364,25 @@ public void onEditPatientPressed() { mUi.showEditPatientDialog(mPatient); } + public void addNote(String note) { + Observation observation = new Observation( + ConceptUuids.NOTES_UUID, + note); + JsonUser user = App.getUserManager().getActiveUser(); + String userId = user == null ? null : user.id; + mPendingNotesEncounter = new Encounter( + mPatientUuid, + null, // Encounter UUID + DateTime.now(), + new Observation[]{observation}, + null, // Order UUIDs + userId); + mAppModel.addEncounter( + mCrudEventBus, + mPatient, + mPendingNotesEncounter); + } + private boolean dialogShowing() { return (mAssignGeneralConditionDialog != null && mAssignGeneralConditionDialog.isShowing()) || (mAssignLocationDialog != null && mAssignLocationDialog.isShowing()); @@ -482,6 +504,8 @@ public void showAssignGeneralConditionDialog( public void setCondition(String newConditionUuid) { LOG.v("Assigning general condition: %s", newConditionUuid); + JsonUser user = App.getUserManager().getActiveUser(); + String userId = user == null ? null : user.id; Encounter encounter = new Encounter( mPatientUuid, null, // encounter UUID, which the server will generate @@ -489,9 +513,8 @@ public void setCondition(String newConditionUuid) { new Observation[] { new Observation( ConceptUuids.GENERAL_CONDITION_UUID, - newConditionUuid, - Observation.Type.NON_DATE) - }, null); + newConditionUuid) + }, null, userId); mAppModel.addEncounter(mCrudEventBus, mPatient, encounter); } @@ -602,6 +625,12 @@ public void onEventMainThread(SyncSucceededEvent event) { } public void onEventMainThread(EncounterAddFailedEvent event) { + if (event.encounter == mPendingNotesEncounter) { + mUi.indicateNoteSubmissionFailed(); + mPendingNotesEncounter = null; + return; + } + if (mAssignGeneralConditionDialog != null) { mAssignGeneralConditionDialog.dismiss(); mAssignGeneralConditionDialog = null; @@ -639,6 +668,27 @@ public void onEventMainThread(EncounterAddFailedEvent event) { mUi.showError(messageResource, exceptionMessage); } + public void onEventMainThread(ItemCreatedEvent event) { + if (objectIsNoteCreationEncounter(event.item)) { + mUi.indicateNoteSubmitted(); + mPendingNotesEncounter = null; + } + } + + /** + * There's no reference equality after an item has been created, and our data model + * is a mess so we can't use .equals(), so we do a "close enough" comparison to work out + * if a note was submitted. + */ + private boolean objectIsNoteCreationEncounter(Object object) { + if (!(object instanceof Encounter)) { + return false; + } + Encounter encounter = (Encounter) object; + return encounter.observations.length != 0 + && ConceptUuids.NOTES_UUID.equals(encounter.observations[0].conceptUuid); + } + // We get a ItemFetchedEvent when the initial patient data is loaded // from SQLite or after an edit has been successfully posted to the server. public void onEventMainThread(ItemFetchedEvent event) { @@ -787,7 +837,9 @@ public void onEventMainThread(VoidObservationsRequestEvent event) { public void onEventMainThread(OrderExecutionSaveRequestedEvent event) { Order order = mOrdersByUuid.get(event.orderUuid); if (order != null) { - mAppModel.addOrderExecutedEncounter(mCrudEventBus, mPatient, order.uuid); + JsonUser user = App.getUserManager().getActiveUser(); + String userId = user == null ? null : user.id; + mAppModel.addOrderExecutedEncounter(mCrudEventBus, mPatient, order.uuid, userId); } } } diff --git a/app/src/main/java/org/projectbuendia/client/ui/chart/PatientObservationsListAdapter.java b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientObservationsListAdapter.java new file mode 100644 index 00000000..e4eecf37 --- /dev/null +++ b/app/src/main/java/org/projectbuendia/client/ui/chart/PatientObservationsListAdapter.java @@ -0,0 +1,142 @@ +package org.projectbuendia.client.ui.chart; + +import android.app.LoaderManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.TextView; + +import org.projectbuendia.client.R; +import org.projectbuendia.client.providers.Contracts; +import org.projectbuendia.client.providers.Contracts.Observations; + +import java.util.Date; + +/** + * A {@link android.widget.ListAdapter} that displays observations for a given patient, matching a + * given concept UUID. + *

+ * TODO: This adapter currently does some database queries on the main thread - we should + * offload these to a background thread for performance reasons. + */ +public class PatientObservationsListAdapter extends CursorAdapter { + + private static final String[] PROJECTION = new String[] { + "rowid AS _id", + Observations.ENTERER_UUID, + Observations.ENCOUNTER_MILLIS, + Observations.VALUE, + }; + + private final ContentResolver mContentResolver; + + public PatientObservationsListAdapter(Context context) { + super(context, null, 0); + mContentResolver = context.getContentResolver(); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(context); + return inflater.inflate(R.layout.notes_list_adapter_note_template, parent, false); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + // Obtain data from cursor + Date encounterTimestamp = new Date(cursor.getLong( + cursor.getColumnIndexOrThrow(Observations.ENCOUNTER_MILLIS))); + String value = cursor.getString( + cursor.getColumnIndexOrThrow(Observations.VALUE)); + String entererUuid = cursor.getString( + cursor.getColumnIndexOrThrow(Observations.ENTERER_UUID)); + String enterer = getUsersNameFromUuid(entererUuid); + + // Obtain view references + ViewGroup viewGroup = (ViewGroup) view; + TextView metaLine = (TextView) viewGroup.findViewById(R.id.meta); + TextView content = (TextView) viewGroup.findViewById(R.id.observation_content); + + // Set content + metaLine.setText(context.getResources().getString( + enterer == null + ? R.string.notes_list_metadata_format_no_user_info + : R.string.notes_list_metadata_format, + encounterTimestamp, enterer)); + content.setText(value); + } + + /** + * Returns the users' full name from a UUID. Note that this performs a database query, and so + * ideally calls should be kept off the main thread. It does not perform a network request to + * check for new users on the server. + * + * @param uuid The uuid of the user whose name to return. Note that if {@code null} is passed, + * {@code null} will be returned. + * @return the users' name, if a user was found matching this UUID. {@code null} otherwise. + */ + public @Nullable String getUsersNameFromUuid(@Nullable String uuid) { + if (uuid == null) { + return null; + } + try (Cursor cursor = mContentResolver.query( + Contracts.Users.CONTENT_URI.buildUpon().appendPath(uuid).build(), + new String[]{Contracts.Users.FULL_NAME}, + null, + null, + null)) { + if (cursor == null || !cursor.moveToFirst()) { + // Either there wasn't a cursor, or the result set was empty. + // This is a user we don't know about. + return null; + } + return cursor.getString(0); + } + } + + public static class ObservationsListLoaderCallbacks + implements LoaderManager.LoaderCallbacks { + + private final Context mContext; + private final String mPatientUuid; + private final String mConceptUuid; + private final CursorAdapter mAdapter; + + public ObservationsListLoaderCallbacks( + Context context, String patientUuid, String conceptUuid, CursorAdapter adapter) { + mContext = context; + mPatientUuid = patientUuid; + mConceptUuid = conceptUuid; + mAdapter = adapter; + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new CursorLoader(mContext, + Observations.CONTENT_URI, + PROJECTION, + Observations.PATIENT_UUID + " = ? AND " + + Observations.CONCEPT_UUID + " = ? ", + new String[]{mPatientUuid, mConceptUuid}, + Observations.ENCOUNTER_MILLIS); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + mAdapter.swapCursor(data); + } + + @Override + public void onLoaderReset(Loader loader) { + mAdapter.swapCursor(null); + } + } +} diff --git a/app/src/main/java/org/projectbuendia/client/ui/lists/LocationListFragment.java b/app/src/main/java/org/projectbuendia/client/ui/lists/LocationListFragment.java index b150ef9e..ccf6c5dd 100644 --- a/app/src/main/java/org/projectbuendia/client/ui/lists/LocationListFragment.java +++ b/app/src/main/java/org/projectbuendia/client/ui/lists/LocationListFragment.java @@ -12,6 +12,7 @@ package org.projectbuendia.client.ui.lists; import android.os.Bundle; +import android.os.Debug; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; diff --git a/app/src/main/res/layout/fragment_patient_chart.xml b/app/src/main/res/layout/fragment_patient_chart.xml index ef05ba39..4abfb802 100644 --- a/app/src/main/res/layout/fragment_patient_chart.xml +++ b/app/src/main/res/layout/fragment_patient_chart.xml @@ -9,69 +9,173 @@ OR CONDITIONS OF ANY KIND, either express or implied. See the License for specific language governing permissions and limitations under the License. --> - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +