Repository: google/ground-android
Issue: [Add/edit observation] Changes not reflected on view observation page on save

1. Edit an existing observation
2. Click save

User is returned to view observation page. Instead, they should be redirected to the feature sheet for consistency with "add observation" flow.

Changed files:
gnd/src/main/java/com/google/android/gnd/repository/ObservationRepository.java
gnd/src/main/java/com/google/android/gnd/ui/editobservation/EditObservationViewModel.java
gnd/src/main/java/com/google/android/gnd/persistence/local/room/RoomLocalDataStore.java
gnd/src/test/java/com/google/android/gnd/persistence/local/LocalDataStoreTest.java

diff --git a/gnd/src/main/java/com/google/android/gnd/persistence/local/room/RoomLocalDataStore.java b/gnd/src/main/java/com/google/android/gnd/persistence/local/room/RoomLocalDataStore.java
index 4d2f66d83..ea7f16df9 100644
--- a/gnd/src/main/java/com/google/android/gnd/persistence/local/room/RoomLocalDataStore.java
+++ b/gnd/src/main/java/com/google/android/gnd/persistence/local/room/RoomLocalDataStore.java
@@ -42,6 +42,7 @@ import com.google.android.gnd.model.layer.Layer;
 import com.google.android.gnd.model.observation.Observation;
 import com.google.android.gnd.model.observation.ObservationMutation;
 import com.google.android.gnd.model.observation.ResponseMap;
+import com.google.android.gnd.model.observation.ResponseMap.Builder;
 import com.google.android.gnd.persistence.local.LocalDataStore;
 import com.google.android.gnd.persistence.local.room.converter.ResponseDeltasConverter;
 import com.google.android.gnd.persistence.local.room.converter.ResponseMapConverter;
@@ -86,6 +87,7 @@ import io.reactivex.Flowable;
 import io.reactivex.Maybe;
 import io.reactivex.Observable;
 import io.reactivex.Single;
+import io.reactivex.SingleSource;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -431,13 +433,8 @@ public class RoomLocalDataStore implements LocalDataStore {
     long clientTimestamp = lastMutation.getClientTimestamp();
     Timber.v("Merging observation " + this + " with mutations " + mutations);
     ObservationEntity.Builder builder = observation.toBuilder();
-    ResponseMap.Builder responseMap =
-        ResponseMapConverter.fromString(form, observation.getResponses()).toBuilder();
-    for (ObservationMutationEntity mutation : mutations) {
-      // Merge changes to responses.
-      responseMap.applyDeltas(
-          ResponseDeltasConverter.fromString(form, mutation.getResponseDeltas()));
-    }
+    builder.setResponses(
+        ResponseMapConverter.toString(applyMutations(form, observation, mutations)));
     // Update modified user and time.
     AuditInfoEntity lastModified =
         AuditInfoEntity.builder()
@@ -449,6 +446,18 @@ public class RoomLocalDataStore implements LocalDataStore {
     return builder.build();
   }
 
+  private ResponseMap applyMutations(
+      Form form, ObservationEntity observation, List<ObservationMutationEntity> mutations) {
+    Builder responseMap =
+        ResponseMapConverter.fromString(form, observation.getResponses()).toBuilder();
+    for (ObservationMutationEntity mutation : mutations) {
+      // Merge changes to responses.
+      responseMap.applyDeltas(
+          ResponseDeltasConverter.fromString(form, mutation.getResponseDeltas()));
+    }
+    return responseMap.build();
+  }
+
   private Completable apply(FeatureMutation mutation) throws LocalDataStoreException {
     switch (mutation.getType()) {
       case CREATE:
@@ -539,13 +548,23 @@ public class RoomLocalDataStore implements LocalDataStore {
     return observationDao
         .findById(mutation.getObservationId())
         .doOnSubscribe(__ -> Timber.v("Applying mutation: %s", mutation))
-        // Emit NoSuchElementException if not found.
-        .toSingle()
+        .switchIfEmpty(fallbackObservation(mutation))
         .map(obs -> applyMutations(mutation.getForm(), obs, ImmutableList.of(mutationEntity), user))
         .flatMapCompletable(obs -> observationDao.insertOrUpdate(obs).subscribeOn(schedulers.io()))
         .subscribeOn(schedulers.io());
   }
 
+  /**
+   * Returns a source which creates an observation based on the provided mutation. Used in rare
+   * cases when the observation is no longer in the local db, but the user is updating rather than
+   * creating a new observation. In these cases creation metadata is unknown, so empty audit info is
+   * used.
+   */
+  private SingleSource<ObservationEntity> fallbackObservation(ObservationMutation mutation) {
+    return em ->
+        em.onSuccess(ObservationEntity.fromMutation(mutation, AuditInfo.builder().build()));
+  }
+
   private Completable markObservationForDeletion(
       ObservationEntity entity, ObservationMutation mutation) {
     return observationDao
diff --git a/gnd/src/main/java/com/google/android/gnd/repository/ObservationRepository.java b/gnd/src/main/java/com/google/android/gnd/repository/ObservationRepository.java
index c11eb2343..31253940a 100644
--- a/gnd/src/main/java/com/google/android/gnd/repository/ObservationRepository.java
+++ b/gnd/src/main/java/com/google/android/gnd/repository/ObservationRepository.java
@@ -156,7 +156,7 @@ public class ObservationRepository {
     return applyAndEnqueue(observationMutation);
   }
 
-  public Completable addObservationMutation(
+  public Completable createOrUpdateObservation(
       Observation observation, ImmutableList<ResponseDelta> responseDeltas, boolean isNew) {
     ObservationMutation observationMutation =
         ObservationMutation.builder()
diff --git a/gnd/src/main/java/com/google/android/gnd/ui/editobservation/EditObservationViewModel.java b/gnd/src/main/java/com/google/android/gnd/ui/editobservation/EditObservationViewModel.java
index e9c0c7e4a..d35366e68 100644
--- a/gnd/src/main/java/com/google/android/gnd/ui/editobservation/EditObservationViewModel.java
+++ b/gnd/src/main/java/com/google/android/gnd/ui/editobservation/EditObservationViewModel.java
@@ -277,7 +277,7 @@ public class EditObservationViewModel extends AbstractViewModel {
     }
 
     return observationRepository
-        .addObservationMutation(originalObservation, getResponseDeltas(), isNew)
+        .createOrUpdateObservation(originalObservation, getResponseDeltas(), isNew)
         .doOnSubscribe(__ -> isSaving.postValue(true))
         .doOnComplete(() -> isSaving.postValue(false))
         .toSingleDefault(Event.create(SaveResult.SAVED));
diff --git a/gnd/src/test/java/com/google/android/gnd/persistence/local/LocalDataStoreTest.java b/gnd/src/test/java/com/google/android/gnd/persistence/local/LocalDataStoreTest.java
index c93b90ea1..ef4a3f6fb 100644
--- a/gnd/src/test/java/com/google/android/gnd/persistence/local/LocalDataStoreTest.java
+++ b/gnd/src/test/java/com/google/android/gnd/persistence/local/LocalDataStoreTest.java
@@ -131,7 +131,7 @@ public class LocalDataStoreTest {
                   ResponseDelta.builder()
                       .setFieldId("field id")
                       .setFieldType(Field.Type.TEXT_FIELD)
-                      .setNewResponse(TextResponse.fromString("response for field id"))
+                      .setNewResponse(TextResponse.fromString("updated response"))
                       .build()))
           .setClientTimestamp(new Date())
           .build();
@@ -437,7 +437,7 @@ public class LocalDataStoreTest {
             .values()
             .get(0)
             .getResponses();
-    assertThat("foo value").isEqualTo(responses.getResponse("field id").get().toString());
+    assertThat("updated response").isEqualTo(responses.getResponse("field id").get().toString());
   }
 
   @Test