From 96fbdf31b150dd8189e501fad6fd1dacdbc2c42e Mon Sep 17 00:00:00 2001 From: simonpoole Date: Sun, 8 Sep 2024 10:43:58 +0200 Subject: [PATCH] Support conflict resolution for partial uploads --- .../blau/android/osm/UploadConflictTest.java | 109 +++++++++++++----- src/main/java/de/blau/android/Logic.java | 2 +- .../blau/android/dialogs/UploadConflict.java | 43 +++++-- 3 files changed, 114 insertions(+), 40 deletions(-) diff --git a/src/androidTest/java/de/blau/android/osm/UploadConflictTest.java b/src/androidTest/java/de/blau/android/osm/UploadConflictTest.java index 0c8178af37..c96141b2f8 100644 --- a/src/androidTest/java/de/blau/android/osm/UploadConflictTest.java +++ b/src/androidTest/java/de/blau/android/osm/UploadConflictTest.java @@ -111,7 +111,7 @@ public void teardown() { */ @Test public void versionConflictUseLocal() { - versionConflict("conflict1", new String[] { "conflictdownload1" }, false, R.string.upload_conflict_message_version); + conflict("conflict1", new String[] { "conflictdownload1" }, false, R.string.upload_conflict_message_version); assertTrue(TestUtils.clickText(device, false, main.getString(R.string.resolve), true)); assertTrue(TestUtils.clickText(device, false, main.getString(R.string.use_local_version), true)); assertTrue(TestUtils.findText(device, false, main.getString(R.string.confirm_upload_title), 5000)); @@ -119,12 +119,41 @@ public void versionConflictUseLocal() { assertEquals(7, n.getOsmVersion()); // version should now be the same as the server } + /** + * Version conflict use the local element, just uploading a selection + */ + @Test + public void versionConflictUseLocalSelection() { + loadDataAndFixtures("conflict1", new String[] { "conflictdownload1" }, false); + + UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + + TestUtils.unlock(device); + Node n = (Node) App.getDelegator().getOsmElement(Node.NAME, 101792984L); + TestUtils.clickAtCoordinates(device, main.getMap(), n.getLon(), n.getLat()); + + assertTrue(TestUtils.findText(device, false, main.getString(R.string.actionmode_nodeselect), 1000)); + + assertTrue(TestUtils.clickOverflowButton(device)); + TestUtils.scrollTo(main.getString(R.string.menu_upload_element), false); + assertTrue(TestUtils.clickText(device, false, main.getString(R.string.menu_upload_element), true)); + + uploadDialog(R.string.upload_conflict_message_version, device); + + assertTrue(TestUtils.clickText(device, false, main.getString(R.string.resolve), true)); + assertTrue(TestUtils.clickText(device, false, main.getString(R.string.use_local_version), true)); + assertTrue(TestUtils.findText(device, false, main.getString(R.string.confirm_upload_title), 5000)); + assertFalse(TestUtils.findText(device, false, "Kindhauserstrasse")); + n = (Node) App.getDelegator().getOsmElement(Node.NAME, 101792984L); + assertEquals(7, n.getOsmVersion()); // version should now be the same as the server + } + /** * Version conflict use the server element */ @Test public void versionConflictUseServer() { - versionConflict("conflict1", new String[] { "conflictdownload1" }, false, R.string.upload_conflict_message_version); + conflict("conflict1", new String[] { "conflictdownload1" }, false, R.string.upload_conflict_message_version); Node n = (Node) App.getDelegator().getOsmElement(Node.NAME, 101792984L); assertNotNull(n); assertEquals(6, n.getOsmVersion()); // version should now be server and not in the API @@ -151,7 +180,7 @@ public void versionConflictUseServer() { */ @Test public void severElementAlreadyDeleted() { - versionConflict("conflict2", new String[] { "410", "200" }, false, -1); + conflict("conflict2", new String[] { "410", "200" }, false, -1); try { final MockWebServer server = mockServer.server(); server.takeRequest(10L, TimeUnit.SECONDS); @@ -171,11 +200,12 @@ public void severElementAlreadyDeleted() { */ @Test public void severElementInUse() { - versionConflict("conflict3", new String[] { "way-210461100", "way-210461100-nodes", "relation-12345", "relation-12345", "empty" }, false, -1); + conflict("conflict3", new String[] { "way-210461100", "way-210461100-nodes", "relation-12345", "relation-12345", "empty" }, false, -1); Way w = App.getDelegator().getApiStorage().getWay(210461100L); assertNotNull(w); assertEquals(OsmElement.STATE_DELETED, w.getState()); + assertTrue(TestUtils.findText(device, false, "12345", 10000, true)); assertTrue(TestUtils.clickText(device, false, main.getString(R.string.resolve), true)); assertTrue(TestUtils.clickText(device, false, main.getString(R.string.deleting_references_on_server), true)); assertTrue(TestUtils.findText(device, false, main.getString(R.string.confirm_upload_title), 20000)); @@ -191,46 +221,38 @@ public void severElementInUse() { */ @Test public void referencesMissing() { - versionConflict("conflict4", new String[] { "way-27009604", "way-27009604-nodes", "nodes-deleted" }, false, + conflict("conflict4", new String[] { "way-27009604", "way-27009604-nodes", "nodes-deleted" }, false, R.string.upload_conflict_message_missing_references); Way w = App.getDelegator().getApiStorage().getWay(27009604L); assertTrue(TestUtils.clickText(device, false, main.getString(R.string.cancel), true)); } /** - * Upload to changes (mock-)server and wait for version conflict dialog + * Upload to changes (mock-)server and wait for conflict dialog * * @param conflictReponse the response * @param fixtures name of additional fixtures with the response to the upload * @param userDetails if true enqueue user details - * @param waitForDialog wait for the conflict dialog if true + * @param titleRes title to expect */ - private void versionConflict(@NonNull String conflictReponse, @NonNull String[] fixtures, boolean userDetails, int titleRes) { - final CountDownLatch signal = new CountDownLatch(1); - Logic logic = App.getLogic(); - - ClassLoader loader = Thread.currentThread().getContextClassLoader(); - InputStream is = loader.getResourceAsStream("test1.osm"); - logic.readOsmFile(main, is, false, new SignalHandler(signal)); - SignalUtils.signalAwait(signal, TIMEOUT); - - mockServer.enqueue("capabilities1"); // for whatever reason this gets asked for twice - mockServer.enqueue("capabilities1"); - mockServer.enqueue("changeset1"); - mockServer.enqueue(conflictReponse); - if (userDetails) { - mockServer.enqueue("userdetails"); - } - for (String fixture : fixtures) { - mockServer.enqueue(fixture); - } + private void conflict(@NonNull String conflictReponse, @NonNull String[] fixtures, boolean userDetails, int titleRes) { + loadDataAndFixtures(conflictReponse, fixtures, userDetails); TestUtils.clickMenuButton(device, main.getString(R.string.menu_transfer), false, true); - UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); TestUtils.clickText(device, false, main.getString(R.string.menu_transfer_upload), true, false); // menu item - UiSelector uiSelector = new UiSelector().className("android.widget.Button").instance(1); // dialog upload button + uploadDialog(titleRes, device); + } + + /** + * FIllout comment and source fields and upload + * + * @param titleRes title to expect + * @param device UiDevice + */ + private void uploadDialog(int titleRes, UiDevice device) { + UiSelector uiSelector = new UiSelector().resourceId("android:id/button1"); // dialog upload button UiObject button = device.findObject(uiSelector); try { button.click(); @@ -238,6 +260,9 @@ private void versionConflict(@NonNull String conflictReponse, @NonNull String[] fail(e1.getMessage()); } fillCommentAndSource(instrumentation, device); + TestUtils.sleep(); + uiSelector = new UiSelector().resourceId("android:id/button1"); + button = device.findObject(uiSelector); try { button.clickAndWaitForNewWindow(); } catch (UiObjectNotFoundException e1) { @@ -248,6 +273,34 @@ private void versionConflict(@NonNull String conflictReponse, @NonNull String[] } } + /** + * Load the data and fixtures + * + * @param conflictReponse the response + * @param fixtures name of additional fixtures with the response to the upload + * @param userDetails if true enqueue user details + */ + private void loadDataAndFixtures(String conflictReponse, String[] fixtures, boolean userDetails) { + final CountDownLatch signal = new CountDownLatch(1); + Logic logic = App.getLogic(); + + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + InputStream is = loader.getResourceAsStream("test1.osm"); + logic.readOsmFile(main, is, false, new SignalHandler(signal)); + SignalUtils.signalAwait(signal, TIMEOUT); + + mockServer.enqueue("capabilities1"); // for whatever reason this gets asked for twice + mockServer.enqueue("capabilities1"); + mockServer.enqueue("changeset1"); + mockServer.enqueue(conflictReponse); + if (userDetails) { + mockServer.enqueue("userdetails"); + } + for (String fixture : fixtures) { + mockServer.enqueue(fixture); + } + } + /** * Fill our comment and source fields * diff --git a/src/main/java/de/blau/android/Logic.java b/src/main/java/de/blau/android/Logic.java index 2b95b0cd9c..049d1b79cf 100644 --- a/src/main/java/de/blau/android/Logic.java +++ b/src/main/java/de/blau/android/Logic.java @@ -4402,7 +4402,7 @@ protected void onPostExecute(UploadResult result) { } else if (conflict instanceof ApiResponse.ChangesetLocked) { ErrorAlert.showDialog(activity, ErrorCodes.UPLOAD_PROBLEM, result.getMessage()); } else { - UploadConflict.showDialog(activity, conflict); + UploadConflict.showDialog(activity, conflict, elements); } break; case ErrorCodes.INVALID_LOGIN: diff --git a/src/main/java/de/blau/android/dialogs/UploadConflict.java b/src/main/java/de/blau/android/dialogs/UploadConflict.java index 59a44475bc..5def07645b 100644 --- a/src/main/java/de/blau/android/dialogs/UploadConflict.java +++ b/src/main/java/de/blau/android/dialogs/UploadConflict.java @@ -1,6 +1,10 @@ package de.blau.android.dialogs; +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -54,6 +58,7 @@ import de.blau.android.util.InfoDialogFragment; import de.blau.android.util.ScreenMessage; import de.blau.android.util.ThemeUtils; +import de.blau.android.util.Util; /** * Dialog to resolve upload conflicts one by one @@ -62,14 +67,16 @@ * */ public class UploadConflict extends ImmersiveDialogFragment { - - private static final String DEBUG_TAG = UploadConflict.class.getSimpleName().substring(0, Math.min(23, UploadConflict.class.getSimpleName().length())); + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, UploadConflict.class.getSimpleName().length()); + private static final String DEBUG_TAG = UploadConflict.class.getSimpleName().substring(0, TAG_LEN); private static final String CONFLICT_KEY = "uploadresult"; + private static final String ELEMENTS_KEY = "elements"; private static final String TAG = "fragment_upload_conflict"; - private Conflict conflict; + private Conflict conflict; + private List elements; private class RestartHandler implements PostAsyncActionHandler { private final String errorMessage; @@ -92,7 +99,7 @@ public void onSuccess() { ((Main) activity).invalidateMap(); } if (App.getDelegator().hasChanges()) { - ReviewAndUpload.showDialog(activity, null); + ReviewAndUpload.showDialog(activity, elements); } } @@ -106,14 +113,15 @@ public void onError(@Nullable AsyncResult result) { * Show a dialog after a conflict has been detected and allow the user to fix it * * @param activity the calling Activity + * @param elements optional list of elements in upload * @param result the UploadResult */ - public static void showDialog(@NonNull FragmentActivity activity, @NonNull Conflict conflict) { + public static void showDialog(@NonNull FragmentActivity activity, @NonNull Conflict conflict, @Nullable List elements) { dismissDialog(activity); FragmentManager fm = activity.getSupportFragmentManager(); try { - UploadConflict uploadConflictDialogFragment = newInstance(conflict); + UploadConflict uploadConflictDialogFragment = newInstance(conflict, elements); uploadConflictDialogFragment.show(fm, TAG); } catch (IllegalStateException isex) { Log.e(DEBUG_TAG, "dismissDialog", isex); @@ -132,15 +140,20 @@ private static void dismissDialog(@NonNull FragmentActivity activity) { /** * Construct a new UploadConflict dialog * - * @param result an UploadResult + * @param conflict an COnflict object with the relevant info + * @param elements optional list of elements in upload + * * @return an UploadConflict dialog */ @NonNull - private static UploadConflict newInstance(@NonNull final Conflict conflict) { + private static UploadConflict newInstance(@NonNull final Conflict conflict, List elements) { UploadConflict f = new UploadConflict(); Bundle args = new Bundle(); args.putSerializable(CONFLICT_KEY, conflict); + if (elements != null) { + args.putSerializable(ELEMENTS_KEY, new ArrayList<>(elements)); + } f.setArguments(args); f.setShowsDialog(true); @@ -154,8 +167,10 @@ public void onCreate(@Nullable Bundle savedInstanceState) { if (savedInstanceState != null) { Log.d(DEBUG_TAG, "restoring from saved state"); conflict = de.blau.android.util.Util.getSerializeable(savedInstanceState, CONFLICT_KEY, Conflict.class); + elements = Util.getSerializeableArrayList(savedInstanceState, ELEMENTS_KEY, OsmElement.class); } else { conflict = de.blau.android.util.Util.getSerializeable(getArguments(), CONFLICT_KEY, Conflict.class); + elements = Util.getSerializeableArrayList(getArguments(), ELEMENTS_KEY, OsmElement.class); } } @@ -218,13 +233,16 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) { logic.createCheckpoint(activity, R.string.undo_action_fix_conflict); delegator.undoLast(elementLocal); if (delegator.getApiElementCount() > 0) { - ReviewAndUpload.showDialog(activity, null); + ReviewAndUpload.showDialog(activity, elements); } }); resolveActions.put(res.getString(R.string.deleting_references_on_server), () -> { logic.createCheckpoint(activity, R.string.undo_action_fix_conflict); // first undelete delegator.removeFromUpload(elementLocal, OsmElement.STATE_UNCHANGED); + if (elements != null) { + elements.remove(elementLocal); + } delegator.insertElementSafe(elementLocal); // now download referring elements for (long id : usedByElementIds) { @@ -248,7 +266,7 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) { default: throw new IllegalStateException("Unknown element type"); } - ReviewAndUpload.showDialog(activity, null); + ReviewAndUpload.showDialog(activity, elements); }); } else if (conflict instanceof ApiResponse.VersionConflict) { // @@ -262,7 +280,7 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) { activity.getString(R.string.toast_download_server_version_failed, elementLocal.getDescription())); resolveActions.put(res.getString(R.string.use_local_version), () -> { logic.fixElementWithConflict(activity, elementOnServer.getOsmVersion(), elementLocal, elementOnServer, true); - ReviewAndUpload.showDialog(activity, null); + ReviewAndUpload.showDialog(activity, elements); }); resolveActions.put(res.getString(R.string.merge_tags_in_to_server), () -> { Map mergedTags = MergeAction.mergeTags(elementOnServer, elementLocal); @@ -512,5 +530,8 @@ public static TableRow createMissingReferenceRow(@NonNull Context context, @NonN public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putSerializable(CONFLICT_KEY, conflict); + if (elements != null) { + outState.putSerializable(ELEMENTS_KEY, new ArrayList<>(elements)); + } } }