Skip to content

Commit

Permalink
Merge branch 'support_conflict_resolution_for_partial_uploads'
Browse files Browse the repository at this point in the history
  • Loading branch information
simonpoole committed Sep 9, 2024
2 parents 5948c1f + bae6477 commit f862def
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 40 deletions.
109 changes: 81 additions & 28 deletions src/androidTest/java/de/blau/android/osm/UploadConflictTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,20 +111,49 @@ 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));
Node 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 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), 10000));

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
Expand All @@ -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);
Expand All @@ -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));
Expand All @@ -191,53 +221,48 @@ 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();
} catch (UiObjectNotFoundException e1) {
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) {
Expand All @@ -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
*
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/de/blau/android/Logic.java
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
43 changes: 32 additions & 11 deletions src/main/java/de/blau/android/dialogs/UploadConflict.java
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -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<OsmElement> elements;

private class RestartHandler implements PostAsyncActionHandler {
private final String errorMessage;
Expand All @@ -92,7 +99,7 @@ public void onSuccess() {
((Main) activity).invalidateMap();
}
if (App.getDelegator().hasChanges()) {
ReviewAndUpload.showDialog(activity, null);
ReviewAndUpload.showDialog(activity, elements);
}
}

Expand All @@ -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<OsmElement> 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);
Expand All @@ -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<OsmElement> 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);
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
//
Expand All @@ -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<String, String> mergedTags = MergeAction.mergeTags(elementOnServer, elementLocal);
Expand Down Expand Up @@ -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));
}
}
}

0 comments on commit f862def

Please sign in to comment.