Prevent filled form from being edited if sending is canceled or crashes
Some users have been reporting that they are seeing failures to send submissions because the server has already received a different submission with the same instanceID. Error message from Central:

> A submission already exists with this ID, but with different XML. Resubmissions to attach additional multimedia must resubmit an identical xml_submission_file.

See [this report](https://forum.getodk.org/t/odk-central-upload-error-a-submission-already-exists-with-this-id-but-with-different-xml/33596/5?u=ln).

In https://github.com/getodk/collect/issues/4589, we greatly improved the situation by not allowing edits of failed submissions. I believe that users are now seeing this issue when they have long uploads that they manually cancel or experience crashes during sending.

We can probably do something like change the send status to failed as soon as the XML of the submission is sent. That way, if the user cancels or gets a crash then, they aren't in an inconsistent state.
diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderListActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderListActivity.java
index f635db279..a11179117 100644
--- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderListActivity.java
+++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderListActivity.java
@@ -126,14 +126,15 @@ public class InstanceUploaderListActivity extends InstanceListActivity implement
             return;
         }
 
-        int checkedItemCount = getCheckedCount();
+        long[] instanceIds = listView.getCheckedItemIds();
 
-        if (checkedItemCount > 0) {
-            // items selected
-            uploadSelectedFiles();
+        if (instanceIds.length > 0) {
+            selectedInstances.clear();
             setAllToCheckedState(listView, false);
             toggleButtonLabel(findViewById(R.id.toggle_button), listView);
             binding.uploadButton.setEnabled(false);
+
+            uploadSelectedFiles(instanceIds);
         } else {
             // no items selected
             ToastUtils.showLongToast(this, R.string.noselect_error);
@@ -205,9 +206,7 @@ public class InstanceUploaderListActivity extends InstanceListActivity implement
         binding.uploadButton.setText(R.string.send_selected_data);
     }
 
-    private void uploadSelectedFiles() {
-        long[] instanceIds = listView.getCheckedItemIds();
-
+    private void uploadSelectedFiles(long[] instanceIds) {
         String server = settingsProvider.getUnprotectedSettings().getString(KEY_PROTOCOL);
 
         if (server.equalsIgnoreCase(ProjectKeys.PROTOCOL_GOOGLE_SHEETS)) {
diff --git a/collect_app/src/main/java/org/odk/collect/android/gdrive/InstanceGoogleSheetsUploader.java b/collect_app/src/main/java/org/odk/collect/android/gdrive/InstanceGoogleSheetsUploader.java
index 118490563..fba87e6cc 100644
--- a/collect_app/src/main/java/org/odk/collect/android/gdrive/InstanceGoogleSheetsUploader.java
+++ b/collect_app/src/main/java/org/odk/collect/android/gdrive/InstanceGoogleSheetsUploader.java
@@ -88,6 +88,8 @@ public class InstanceGoogleSheetsUploader extends InstanceUploader {
 
     @Override
     public String uploadOneSubmission(Instance instance, String spreadsheetUrl) throws FormUploadException {
+        markSubmissionFailed(instance);
+
         File instanceFile = new File(instance.getInstanceFilePath());
         if (!instanceFile.exists()) {
             throw new FormUploadException(FAIL + "instance XML file does not exist!");
@@ -103,7 +105,6 @@ public class InstanceGoogleSheetsUploader extends InstanceUploader {
 
             Form form = forms.get(0);
             if (form.getBASE64RSAPublicKey() != null) {
-                submissionComplete(instance, false);
                 throw new FormUploadException(getLocalizedString(Collect.getInstance(), R.string.google_sheets_encrypted_message));
             }
 
@@ -120,15 +121,11 @@ public class InstanceGoogleSheetsUploader extends InstanceUploader {
                 key = PropertyUtils.genUUID();
             }
             insertRows(instance, instanceElement, null, key, instanceFile, spreadsheet.getSheets().get(0).getProperties().getTitle());
-        } catch (FormUploadException e) {
-            submissionComplete(instance, false);
-            throw e;
         } catch (GoogleJsonResponseException e) {
-            submissionComplete(instance, false);
             throw new FormUploadException(getErrorMessageFromGoogleJsonResponseException(e));
         }
 
-        submissionComplete(instance, true);
+        markSubmissionComplete(instance);
         // Google Sheets can't provide a custom success message
         return null;
     }
diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceSubmitter.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceSubmitter.kt
index a5ec75bd4..4b4c4e218 100644
--- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceSubmitter.kt
+++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceSubmitter.kt
@@ -96,7 +96,6 @@ class InstanceSubmitter(
         return InstanceServerUploader(
             httpInterface,
             WebCredentialsUtils(generalSettings),
-            HashMap(),
             generalSettings
         )
     }
diff --git a/collect_app/src/main/java/org/odk/collect/android/tasks/InstanceServerUploaderTask.java b/collect_app/src/main/java/org/odk/collect/android/tasks/InstanceServerUploaderTask.java
index 940404770..78f99f4f2 100644
--- a/collect_app/src/main/java/org/odk/collect/android/tasks/InstanceServerUploaderTask.java
+++ b/collect_app/src/main/java/org/odk/collect/android/tasks/InstanceServerUploaderTask.java
@@ -25,7 +25,6 @@ import org.odk.collect.android.upload.FormUploadAuthRequestedException;
 import org.odk.collect.android.upload.FormUploadException;
 import org.odk.collect.android.utilities.WebCredentialsUtils;
 
-import java.util.HashMap;
 import java.util.List;
 
 import javax.inject.Inject;
@@ -62,7 +61,7 @@ public class InstanceServerUploaderTask extends InstanceUploaderTask {
     public Outcome doInBackground(Long... instanceIdsToUpload) {
         Outcome outcome = new Outcome();
 
-        InstanceServerUploader uploader = new InstanceServerUploader(httpInterface, webCredentialsUtils, new HashMap<>(), settingsProvider.getUnprotectedSettings());
+        InstanceServerUploader uploader = new InstanceServerUploader(httpInterface, webCredentialsUtils, settingsProvider.getUnprotectedSettings());
         List<Instance> instancesToUpload = uploader.getInstancesFromIds(instanceIdsToUpload);
 
         String deviceId = new PropertyManager().getSingularProperty(PropertyManager.PROPMGR_DEVICE_ID);
diff --git a/collect_app/src/main/java/org/odk/collect/android/upload/InstanceServerUploader.java b/collect_app/src/main/java/org/odk/collect/android/upload/InstanceServerUploader.java
index 0eaf97bdc..04feea07f 100644
--- a/collect_app/src/main/java/org/odk/collect/android/upload/InstanceServerUploader.java
+++ b/collect_app/src/main/java/org/odk/collect/android/upload/InstanceServerUploader.java
@@ -37,6 +37,7 @@ import java.net.URI;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -51,15 +52,14 @@ public class InstanceServerUploader extends InstanceUploader {
 
     private final OpenRosaHttpInterface httpInterface;
     private final WebCredentialsUtils webCredentialsUtils;
-    private final Map<Uri, Uri> uriRemap;
     private final Settings generalSettings;
+    private final Map<Uri, Uri> uriRemap = new HashMap<>();
 
     public InstanceServerUploader(OpenRosaHttpInterface httpInterface,
                                   WebCredentialsUtils webCredentialsUtils,
-                                  Map<Uri, Uri> uriRemap, Settings generalSettings) {
+                                  Settings generalSettings) {
         this.httpInterface = httpInterface;
         this.webCredentialsUtils = webCredentialsUtils;
-        this.uriRemap = uriRemap;
         this.generalSettings = generalSettings;
     }
 
@@ -71,6 +71,8 @@ public class InstanceServerUploader extends InstanceUploader {
      */
     @Override
     public String uploadOneSubmission(Instance instance, String urlString) throws FormUploadException {
+        markSubmissionFailed(instance);
+
         Uri submissionUri = Uri.parse(urlString);
 
         long contentLength = 10000000L;
@@ -84,7 +86,6 @@ public class InstanceServerUploader extends InstanceUploader {
                     submissionUri.toString());
         } else {
             if (submissionUri.getHost() == null) {
-                submissionComplete(instance, false);
                 throw new FormUploadException(FAIL + "Host name may not be null");
             }
 
@@ -92,7 +93,6 @@ public class InstanceServerUploader extends InstanceUploader {
             try {
                 uri = URI.create(submissionUri.toString());
             } catch (IllegalArgumentException e) {
-                submissionComplete(instance, false);
                 Timber.d(e.getMessage() != null ? e.getMessage() : e.toString());
                 throw new FormUploadException(getLocalizedString(Collect.getInstance(), R.string.url_error));
             }
@@ -113,13 +113,11 @@ public class InstanceServerUploader extends InstanceUploader {
                 }
 
             } catch (Exception e) {
-                submissionComplete(instance, false);
                 throw new FormUploadException(FAIL
                         + (e.getMessage() != null ? e.getMessage() : e.toString()));
             }
 
             if (headResult.getStatusCode() == HttpsURLConnection.HTTP_UNAUTHORIZED) {
-                submissionComplete(instance, false);
                 throw new FormUploadAuthRequestedException(getLocalizedString(Collect.getInstance(), R.string.server_auth_credentials, submissionUri.getHost()),
                         submissionUri);
             } else if (headResult.getStatusCode() == HttpsURLConnection.HTTP_NO_CONTENT) {
@@ -140,20 +138,17 @@ public class InstanceServerUploader extends InstanceUploader {
                         } else {
                             // Don't follow a redirection attempt to a different host.
                             // We can't tell if this is a spoof or not.
-                            submissionComplete(instance, false);
                             throw new FormUploadException(FAIL
                                     + "Unexpected redirection attempt to a different host: "
                                     + newURI.toString());
                         }
                     } catch (Exception e) {
-                        submissionComplete(instance, false);
                         throw new FormUploadException(FAIL + urlString + " " + e.toString());
                     }
                 }
             } else {
                 if (headResult.getStatusCode() >= HttpsURLConnection.HTTP_OK
                         && headResult.getStatusCode() < HttpsURLConnection.HTTP_MULT_CHOICE) {
-                    submissionComplete(instance, false);
                     throw new FormUploadException("Failed to send to " + uri + ". Is this an OpenRosa " +
                             "submission endpoint? If you have a web proxy you may need to log in to " +
                             "your network.\n\nHEAD request result status code: " + headResult.getStatusCode());
@@ -175,7 +170,6 @@ public class InstanceServerUploader extends InstanceUploader {
         }
 
         if (!instanceFile.exists() && !submissionFile.exists()) {
-            submissionComplete(instance, false);
             throw new FormUploadException(FAIL + "instance XML file does not exist!");
         }
 
@@ -216,17 +210,15 @@ public class InstanceServerUploader extends InstanceUploader {
                     }
 
                 }
-                submissionComplete(instance, false);
                 throw exception;
             }
 
         } catch (Exception e) {
-            submissionComplete(instance, false);
             throw new FormUploadException(FAIL + "Generic Exception: "
                     + (e.getMessage() != null ? e.getMessage() : e.toString()));
         }
 
-        submissionComplete(instance, true);
+        markSubmissionComplete(instance);
 
         if (messageParser.isValid()) {
             return messageParser.getMessageResponse();
diff --git a/collect_app/src/main/java/org/odk/collect/android/upload/InstanceUploader.java b/collect_app/src/main/java/org/odk/collect/android/upload/InstanceUploader.java
index f6d7253d2..00bd7b233 100644
--- a/collect_app/src/main/java/org/odk/collect/android/upload/InstanceUploader.java
+++ b/collect_app/src/main/java/org/odk/collect/android/upload/InstanceUploader.java
@@ -22,7 +22,6 @@ import org.odk.collect.android.formmanagement.InstancesAppState;
 import org.odk.collect.android.injection.DaggerUtils;
 import org.odk.collect.android.utilities.InstancesRepositoryProvider;
 import org.odk.collect.forms.instances.Instance;
-import org.odk.collect.forms.instances.InstancesRepository;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -62,26 +61,30 @@ public abstract class InstanceUploader {
         List<Instance> instances = new ArrayList<>();
 
         for (Long id : instanceDatabaseIds) {
-            instances.add(new InstancesRepositoryProvider(Collect.getInstance()).get().get(id));
+            instances.add(instancesRepositoryProvider.get().get(id));
         }
 
         return instances;
     }
 
-    public void submissionComplete(Instance instance, boolean successful) {
-        InstancesRepository instancesRepository = instancesRepositoryProvider.get();
-
-        if (successful) {
-            instancesRepository.save(new Instance.Builder(instance)
-                    .status(Instance.STATUS_SUBMITTED)
-                    .build()
-            );
-        } else {
-            instancesRepository.save(new Instance.Builder(instance)
-                    .status(Instance.STATUS_SUBMISSION_FAILED)
-                    .build()
-            );
-        }
+    public void markSubmissionFailed(Instance instance) {
+        instancesRepositoryProvider
+                .get()
+                .save(new Instance.Builder(instance)
+                        .status(Instance.STATUS_SUBMISSION_FAILED)
+                        .build()
+                );
+
+        instancesAppState.update();
+    }
+
+    public void markSubmissionComplete(Instance instance) {
+        instancesRepositoryProvider
+                .get()
+                .save(new Instance.Builder(instance)
+                        .status(Instance.STATUS_SUBMITTED)
+                        .build()
+                );
 
         instancesAppState.update();
     }
['collect_app/src/main/java/org/odk/collect/android/gdrive/InstanceGoogleSheetsUploader.java', 'collect_app/src/main/java/org/odk/collect/android/tasks/InstanceServerUploaderTask.java', 'collect_app/src/main/java/org/odk/collect/android/upload/InstanceServerUploader.java', 'collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceSubmitter.kt', 'collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderListActivity.java', 'collect_app/src/main/java/org/odk/collect/android/upload/InstanceUploader.java']