diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/system/BundleExt.kt b/androidshared/src/main/java/org/odk/collect/androidshared/system/BundleExt.kt new file mode 100644 index 00000000000..908d467f4c8 --- /dev/null +++ b/androidshared/src/main/java/org/odk/collect/androidshared/system/BundleExt.kt @@ -0,0 +1,12 @@ +package org.odk.collect.androidshared.system + +import android.os.Bundle +import android.os.Parcelable +import androidx.core.os.BundleCompat + +object BundleExt { + + inline fun Bundle.getParcelableExtraCompat(name: String): T? { + return BundleCompat.getParcelable(this, name, T::class.java) + } +} \ No newline at end of file diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java index 5d6261991e7..345a8506a27 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java @@ -168,6 +168,7 @@ import org.odk.collect.android.widgets.datetime.DateTimeWidget; import org.odk.collect.android.widgets.datetime.pickers.CustomDatePickerDialog; import org.odk.collect.android.widgets.datetime.pickers.CustomTimePickerDialog; +import org.odk.collect.android.widgets.geo.GeoPointMapDialogFragment; import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver; import org.odk.collect.android.widgets.items.SelectOneFromMapDialogFragment; import org.odk.collect.android.widgets.utilities.ExternalAppRecordingRequester; @@ -451,6 +452,7 @@ public void onCreate(Bundle savedInstanceState) { .forClass(BackgroundAudioPermissionDialogFragment.class, () -> new BackgroundAudioPermissionDialogFragment(viewModelFactory)) .forClass(SelectOneFromMapDialogFragment.class, () -> new SelectOneFromMapDialogFragment(viewModelFactory)) .forClass(GeoPolyDialogFragment.class, () -> new GeoPolyDialogFragment(viewModelFactory, scheduler)) + .forClass(GeoPointMapDialogFragment.class, () -> new GeoPointMapDialogFragment(viewModelFactory)) .forClass(RangePickerDialogFragment.class, () -> new RangePickerDialogFragment(viewModelFactory)) .build()); diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/GeoPointMapWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/GeoPointMapWidget.java index 1841eebe880..aface474560 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/GeoPointMapWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/GeoPointMapWidget.java @@ -57,7 +57,7 @@ protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int a binding.geoAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize); - binding.simpleButton.setOnClickListener(v -> geoDataRequester.requestGeoPoint(prompt, answerText, waitingForDataRegistry)); + binding.simpleButton.setOnClickListener(v -> geoDataRequester.requestGeoPoint(prompt, waitingForDataRegistry)); answerText = prompt.getAnswerText(); String answerToDisplay = GeoWidgetUtils.getGeoPointAnswerToDisplay(getContext(), answerText); diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/GeoPointWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/GeoPointWidget.java index bd443d78461..b6c6adcd00a 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/GeoPointWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/GeoPointWidget.java @@ -58,7 +58,7 @@ protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int a if (prompt.isReadOnly()) { binding.simpleButton.setVisibility(GONE); } else { - binding.simpleButton.setOnClickListener(v -> geoDataRequester.requestGeoPoint(prompt, answerText, waitingForDataRegistry)); + binding.simpleButton.setOnClickListener(v -> geoDataRequester.requestGeoPoint(prompt, waitingForDataRegistry)); } answerText = prompt.getAnswerText(); diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/geo/GeoPointMapDialogFragment.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/geo/GeoPointMapDialogFragment.kt new file mode 100644 index 00000000000..247bff1ee58 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/geo/GeoPointMapDialogFragment.kt @@ -0,0 +1,68 @@ +package org.odk.collect.android.widgets.geo + +import androidx.lifecycle.ViewModelProvider +import org.javarosa.core.model.data.GeoPointData +import org.javarosa.form.api.FormEntryPrompt +import org.odk.collect.android.utilities.Appearances +import org.odk.collect.android.utilities.FormEntryPromptUtils +import org.odk.collect.android.widgets.interfaces.SelectChoiceLoader +import org.odk.collect.android.widgets.utilities.BindAttributes +import org.odk.collect.android.widgets.utilities.WidgetAnswerDialogFragment +import org.odk.collect.geo.GeoUtils.parseGeometryPoint +import org.odk.collect.geo.GeoUtils.toMapPoint +import org.odk.collect.geo.geopoint.GeoPointMapFragment + +class GeoPointMapDialogFragment( + viewModelFactory: ViewModelProvider.Factory +) : + WidgetAnswerDialogFragment( + GeoPointMapFragment::class, + viewModelFactory + ) { + + override fun onCreateFragment( + prompt: FormEntryPrompt, + selectChoiceLoader: SelectChoiceLoader + ): GeoPointMapFragment { + childFragmentManager.setFragmentResultListener( + GeoPointMapFragment.REQUEST_GEOPOINT, + this + ) { _, result -> + val geoPoint = result.getString(GeoPointMapFragment.RESULT_GEOPOINT) + + if (geoPoint != null) { + onAnswer(geoPoint) + } else { + dismiss() + } + } + + val retainMockAccuracy = + FormEntryPromptUtils.getBindAttribute(prompt, BindAttributes.ALLOW_MOCK_ACCURACY) + .toBoolean() + + val inputPoint = when (val answer = prompt.answerValue) { + is GeoPointData -> answer.toMapPoint() + null -> null + else -> throw IllegalArgumentException() + } + + val draggable = Appearances.hasAppearance(prompt, Appearances.PLACEMENT_MAP) + return GeoPointMapFragment( + inputPoint, + draggable, + prompt.isReadOnly, + retainMockAccuracy + ) + } + + private fun onAnswer(geoString: String) { + val answer = if (geoString.isBlank()) { + null + } else { + GeoPointData(parseGeometryPoint(geoString)) + } + + onAnswer(answer) + } +} \ No newline at end of file diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/GeoDataRequester.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/GeoDataRequester.kt index fa97a44bb8c..88ff04d6d7f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/GeoDataRequester.kt +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/GeoDataRequester.kt @@ -6,7 +6,6 @@ import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry interface GeoDataRequester { fun requestGeoPoint( prompt: FormEntryPrompt, - answerText: String?, waitingForDataRegistry: WaitingForDataRegistry ) diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/ActivityGeoDataRequester.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/ActivityGeoDataRequester.kt index 5136f7d546c..c8c6ec43233 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/ActivityGeoDataRequester.kt +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/ActivityGeoDataRequester.kt @@ -8,16 +8,14 @@ import org.javarosa.form.api.FormEntryPrompt import org.odk.collect.android.utilities.Appearances import org.odk.collect.android.utilities.ApplicationConstants import org.odk.collect.android.utilities.FormEntryPromptUtils +import org.odk.collect.android.widgets.geo.GeoPointMapDialogFragment import org.odk.collect.android.widgets.geo.GeoPolyDialogFragment import org.odk.collect.android.widgets.interfaces.GeoDataRequester import org.odk.collect.android.widgets.utilities.BindAttributes.ALLOW_MOCK_ACCURACY import org.odk.collect.androidshared.ui.DialogFragmentUtils -import org.odk.collect.geo.Constants.EXTRA_DRAGGABLE_ONLY import org.odk.collect.geo.Constants.EXTRA_READ_ONLY import org.odk.collect.geo.Constants.EXTRA_RETAIN_MOCK_ACCURACY import org.odk.collect.geo.geopoint.GeoPointActivity -import org.odk.collect.geo.geopoint.GeoPointMapActivity -import org.odk.collect.geo.geopoly.GeoPolyUtils.parseGeometry import org.odk.collect.permissions.PermissionListener import org.odk.collect.permissions.PermissionsProvider import java.lang.Boolean.parseBoolean @@ -29,7 +27,6 @@ class ActivityGeoDataRequester( override fun requestGeoPoint( prompt: FormEntryPrompt, - answerText: String?, waitingForDataRegistry: WaitingForDataRegistry ) { permissionsProvider.requestEnabledLocationPermissions( @@ -38,50 +35,49 @@ class ActivityGeoDataRequester( override fun granted() { waitingForDataRegistry.waitForData(prompt.index) - val bundle = Bundle().also { - val parsedGeometry = parseGeometry(answerText) - if (parsedGeometry.isNotEmpty()) { - it.putParcelable( - GeoPointMapActivity.EXTRA_LOCATION, - parsedGeometry[0] + if (isMapsAppearance(prompt)) { + DialogFragmentUtils.showIfNotShowing( + GeoPointMapDialogFragment::class.java, + bundleOf(WidgetAnswerDialogFragment.ARG_FORM_INDEX to prompt.index), + activity.supportFragmentManager + ) + } else { + val bundle = Bundle().also { + val accuracyThreshold = + FormEntryPromptUtils.getAdditionalAttribute( + prompt, + "accuracyThreshold" + ) + val unacceptableAccuracyThreshold = + FormEntryPromptUtils.getAdditionalAttribute( + prompt, + "unacceptableAccuracyThreshold" + ) + + it.putFloat( + GeoPointActivity.EXTRA_ACCURACY_THRESHOLD, + accuracyThreshold?.toFloatOrNull() ?: DEFAULT_ACCURACY_THRESHOLD ) - } - val accuracyThreshold = - FormEntryPromptUtils.getAdditionalAttribute(prompt, "accuracyThreshold") - val unacceptableAccuracyThreshold = - FormEntryPromptUtils.getAdditionalAttribute( - prompt, - "unacceptableAccuracyThreshold" + it.putFloat( + GeoPointActivity.EXTRA_UNACCEPTABLE_ACCURACY_THRESHOLD, + unacceptableAccuracyThreshold?.toFloatOrNull() + ?: DEFAULT_UNACCEPTABLE_ACCURACY_THRESHOLD ) - it.putFloat( - GeoPointActivity.EXTRA_ACCURACY_THRESHOLD, - accuracyThreshold?.toFloatOrNull() ?: DEFAULT_ACCURACY_THRESHOLD - ) - - it.putFloat( - GeoPointActivity.EXTRA_UNACCEPTABLE_ACCURACY_THRESHOLD, - unacceptableAccuracyThreshold?.toFloatOrNull() - ?: DEFAULT_UNACCEPTABLE_ACCURACY_THRESHOLD - ) + it.putBoolean(EXTRA_RETAIN_MOCK_ACCURACY, getAllowMockAccuracy(prompt)) + it.putBoolean(EXTRA_READ_ONLY, prompt.isReadOnly) + } - it.putBoolean(EXTRA_RETAIN_MOCK_ACCURACY, getAllowMockAccuracy(prompt)) - it.putBoolean(EXTRA_READ_ONLY, prompt.isReadOnly) - it.putBoolean(EXTRA_DRAGGABLE_ONLY, hasPlacementMapAppearance(prompt)) - } + val intent = Intent(activity, GeoPointActivity::class.java).also { + it.putExtras(bundle) + } - val intent = Intent( - activity, - if (isMapsAppearance(prompt)) GeoPointMapActivity::class.java else GeoPointActivity::class.java - ).also { - it.putExtras(bundle) + activity.startActivityForResult( + intent, + ApplicationConstants.RequestCodes.LOCATION_CAPTURE + ) } - - activity.startActivityForResult( - intent, - ApplicationConstants.RequestCodes.LOCATION_CAPTURE - ) } } ) diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/GeoPointMapWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/GeoPointMapWidgetTest.java index fb13d1fee88..a48a8713a95 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/GeoPointMapWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/GeoPointMapWidgetTest.java @@ -186,7 +186,7 @@ public void buttonClick_requestsGeoPoint() { FormEntryPrompt prompt = promptWithAnswer(answer); GeoPointMapWidget widget = createWidget(prompt); widget.binding.simpleButton.performClick(); - verify(geoDataRequester).requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry); + verify(geoDataRequester).requestGeoPoint(prompt, waitingForDataRegistry); } @Test @@ -196,7 +196,7 @@ public void buttonClick_requestsGeoPoint_whenAnswerIsCleared() { widget.clearAnswer(); widget.binding.simpleButton.performClick(); - verify(geoDataRequester).requestGeoPoint(prompt, null, waitingForDataRegistry); + verify(geoDataRequester).requestGeoPoint(prompt, waitingForDataRegistry); } @Test @@ -206,7 +206,7 @@ public void buttonClick_requestsGeoPoint_whenAnswerIsUpdated() { widget.setData(answer); widget.binding.simpleButton.performClick(); - verify(geoDataRequester).requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry); + verify(geoDataRequester).requestGeoPoint(prompt, waitingForDataRegistry); } private GeoPointMapWidget createWidget(FormEntryPrompt prompt) { diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/GeoPointWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/GeoPointWidgetTest.java index 09881ff1a8e..9913e6f3d2b 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/GeoPointWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/GeoPointWidgetTest.java @@ -176,7 +176,7 @@ public void buttonClick_requestsGeoPoint() { FormEntryPrompt prompt = promptWithAnswer(answer); GeoPointWidget widget = createWidget(prompt); widget.binding.simpleButton.performClick(); - verify(geoDataRequester).requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry); + verify(geoDataRequester).requestGeoPoint(prompt, waitingForDataRegistry); } @Test @@ -186,7 +186,7 @@ public void buttonClick_requestsGeoPoint_whenAnswerIsCleared() { widget.clearAnswer(); widget.binding.simpleButton.performClick(); - verify(geoDataRequester).requestGeoPoint(prompt, null, waitingForDataRegistry); + verify(geoDataRequester).requestGeoPoint(prompt, waitingForDataRegistry); } @Test @@ -196,7 +196,7 @@ public void buttonClick_requestsGeoPoint_whenAnswerIsUpdated() { widget.setData(answer); widget.binding.simpleButton.performClick(); - verify(geoDataRequester).requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry); + verify(geoDataRequester).requestGeoPoint(prompt, waitingForDataRegistry); } private GeoPointWidget createWidget(FormEntryPrompt prompt) { diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/utilities/ActivityGeoDataRequesterTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/utilities/ActivityGeoDataRequesterTest.java index 60431911274..473a92b8f79 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/utilities/ActivityGeoDataRequesterTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/utilities/ActivityGeoDataRequesterTest.java @@ -9,10 +9,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes.LOCATION_CAPTURE; -import static org.odk.collect.android.widgets.support.GeoWidgetHelpers.getRandomDoubleArray; import static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer; -import static org.odk.collect.geo.Constants.EXTRA_DRAGGABLE_ONLY; -import static org.odk.collect.geo.Constants.EXTRA_READ_ONLY; import static org.odk.collect.geo.Constants.EXTRA_RETAIN_MOCK_ACCURACY; import static org.robolectric.Shadows.shadowOf; import static java.util.Arrays.asList; @@ -26,19 +23,15 @@ import org.javarosa.core.model.FormIndex; import org.javarosa.core.model.QuestionDef; -import org.javarosa.core.model.data.GeoPointData; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.form.api.FormEntryPrompt; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.odk.collect.android.fakes.FakePermissionsProvider; -import org.odk.collect.android.utilities.Appearances; import org.odk.collect.android.widgets.geo.GeoPolyDialogFragment; import org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry; import org.odk.collect.geo.geopoint.GeoPointActivity; -import org.odk.collect.geo.geopoint.GeoPointMapActivity; -import org.odk.collect.maps.MapPoint; import org.odk.collect.testshared.MockDialogFragment; import org.odk.collect.testshared.MockFragmentFactory; import org.robolectric.Robolectric; @@ -49,7 +42,6 @@ public class ActivityGeoDataRequesterTest { private final FakePermissionsProvider permissionsProvider = new FakePermissionsProvider(); private final FakeWaitingForDataRegistry waitingForDataRegistry = new FakeWaitingForDataRegistry(); - private final GeoPointData answer = new GeoPointData(getRandomDoubleArray()); private FragmentActivity testActivity; private ShadowActivity shadowActivity; @@ -79,7 +71,7 @@ public void setUp() { @Test public void whenPermissionIsNotGranted_requestGeoPoint_doesNotLaunchAnyIntent() { permissionsProvider.setPermissionGranted(false); - activityGeoDataRequester.requestGeoPoint(prompt, "", waitingForDataRegistry); + activityGeoDataRequester.requestGeoPoint(prompt, waitingForDataRegistry); assertNull(shadowActivity.getNextStartedActivity()); assertTrue(waitingForDataRegistry.waiting.isEmpty()); @@ -95,13 +87,13 @@ public void whenPermissionIsNotGranted_requestGeoTrace_doesNotOpenDialog() { @Test public void whenPermissionIGranted_requestGeoPoint_setsFormIndexWaitingForData() { - activityGeoDataRequester.requestGeoPoint(prompt, "", waitingForDataRegistry); + activityGeoDataRequester.requestGeoPoint(prompt, waitingForDataRegistry); assertTrue(waitingForDataRegistry.waiting.contains(formIndex)); } @Test public void requestGeoPoint_launchesCorrectIntent() { - activityGeoDataRequester.requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry); + activityGeoDataRequester.requestGeoPoint(prompt, waitingForDataRegistry); Intent startedIntent = shadowActivity.getNextStartedActivity(); assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class)); @@ -112,35 +104,11 @@ public void requestGeoPoint_launchesCorrectIntent() { assertThat(bundle.getFloat(GeoPointActivity.EXTRA_UNACCEPTABLE_ACCURACY_THRESHOLD), equalTo(ActivityGeoDataRequester.DEFAULT_UNACCEPTABLE_ACCURACY_THRESHOLD)); } - @Test - public void requestGeoPoint_whenAnswerIsPresent_addsToIntent() { - activityGeoDataRequester.requestGeoPoint(prompt, "1.0 2.0 3 4", waitingForDataRegistry); - Intent startedIntent = shadowActivity.getNextStartedActivity(); - - assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class)); - assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE); - - Bundle bundle = startedIntent.getExtras(); - assertThat(bundle.getParcelable(GeoPointMapActivity.EXTRA_LOCATION), equalTo(new MapPoint(1.0, 2.0, 3, 4))); - } - - @Test - public void requestGeoPoint_whenAnswerIsPresentButInvalid_doesNotAddToIntent() { - activityGeoDataRequester.requestGeoPoint(prompt, "something", waitingForDataRegistry); - Intent startedIntent = shadowActivity.getNextStartedActivity(); - - assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class)); - assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE); - - Bundle bundle = startedIntent.getExtras(); - assertThat(bundle.getParcelable(GeoPointMapActivity.EXTRA_LOCATION), equalTo(null)); - } - @Test public void whenWidgetHasAccuracyValue_requestGeoPoint_launchesCorrectIntent() { when(questionDef.getAdditionalAttribute(null, "accuracyThreshold")).thenReturn("10"); - activityGeoDataRequester.requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry); + activityGeoDataRequester.requestGeoPoint(prompt, waitingForDataRegistry); Intent startedIntent = shadowActivity.getNextStartedActivity(); assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class)); @@ -154,7 +122,7 @@ public void whenWidgetHasAccuracyValue_requestGeoPoint_launchesCorrectIntent() { public void whenWidgetHasInvalidAccuracyValue_requestGeoPoint_launchesCorrectIntentWithDefaultThreshold() { when(questionDef.getAdditionalAttribute(null, "accuracyThreshold")).thenReturn("blah"); - activityGeoDataRequester.requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry); + activityGeoDataRequester.requestGeoPoint(prompt, waitingForDataRegistry); Intent startedIntent = shadowActivity.getNextStartedActivity(); assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class)); @@ -168,7 +136,7 @@ public void whenWidgetHasInvalidAccuracyValue_requestGeoPoint_launchesCorrectInt public void whenWidgetHasUnacceptableAccuracyValue_requestGeoPoint_launchesCorrectIntent() { when(questionDef.getAdditionalAttribute(null, "unacceptableAccuracyThreshold")).thenReturn("20"); - activityGeoDataRequester.requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry); + activityGeoDataRequester.requestGeoPoint(prompt, waitingForDataRegistry); Intent startedIntent = shadowActivity.getNextStartedActivity(); assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class)); @@ -182,7 +150,7 @@ public void whenWidgetHasUnacceptableAccuracyValue_requestGeoPoint_launchesCorre public void whenWidgetHasInvalidUnacceptableAccuracyValue_requestGeoPoint_launchesCorrectIntentWithDefaultThreshold() { when(questionDef.getAdditionalAttribute(null, "unacceptableAccuracyThreshold")).thenReturn("blah"); - activityGeoDataRequester.requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry); + activityGeoDataRequester.requestGeoPoint(prompt, waitingForDataRegistry); Intent startedIntent = shadowActivity.getNextStartedActivity(); assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class)); @@ -192,55 +160,6 @@ public void whenWidgetHasInvalidUnacceptableAccuracyValue_requestGeoPoint_launch assertThat(bundle.getFloat(GeoPointActivity.EXTRA_UNACCEPTABLE_ACCURACY_THRESHOLD), equalTo(ActivityGeoDataRequester.DEFAULT_UNACCEPTABLE_ACCURACY_THRESHOLD)); } - @Test - public void whenWidgetHasMapsAppearance_requestGeoPoint_launchesCorrectIntent() { - when(prompt.getAppearanceHint()).thenReturn(Appearances.MAPS); - - activityGeoDataRequester.requestGeoPoint(prompt, "", waitingForDataRegistry); - Intent startedIntent = shadowActivity.getNextStartedActivity(); - - assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointMapActivity.class)); - assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE); - - Bundle bundle = startedIntent.getExtras(); - assertThat(bundle.getDoubleArray(GeoPointMapActivity.EXTRA_LOCATION), equalTo(null)); - assertThat(bundle.getBoolean(EXTRA_READ_ONLY), equalTo(false)); - assertThat(bundle.getBoolean(EXTRA_DRAGGABLE_ONLY), equalTo((Object) false)); - } - - @Test - public void whenWidgetHasMapsAppearance_andIsReadOnly_requestGeoPoint_launchesCorrectIntent() { - when(prompt.getAppearanceHint()).thenReturn(Appearances.MAPS); - - when(prompt.isReadOnly()).thenReturn(true); - activityGeoDataRequester.requestGeoPoint(prompt, "", waitingForDataRegistry); - Intent startedIntent = shadowActivity.getNextStartedActivity(); - - assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointMapActivity.class)); - assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE); - - Bundle bundle = startedIntent.getExtras(); - assertThat(bundle.getDoubleArray(GeoPointMapActivity.EXTRA_LOCATION), equalTo(null)); - assertThat(bundle.getBoolean(EXTRA_READ_ONLY), equalTo(true)); - assertThat(bundle.getBoolean(EXTRA_DRAGGABLE_ONLY), equalTo((Object) false)); - } - - @Test - public void whenWidgetHasPlacementMapAppearance_requestGeoPoint_launchesCorrectIntent() { - when(prompt.getAppearanceHint()).thenReturn(Appearances.PLACEMENT_MAP); - - activityGeoDataRequester.requestGeoPoint(prompt, "", waitingForDataRegistry); - Intent startedIntent = shadowActivity.getNextStartedActivity(); - - assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointMapActivity.class)); - assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE); - - Bundle bundle = startedIntent.getExtras(); - assertThat(bundle.getDoubleArray(GeoPointMapActivity.EXTRA_LOCATION), equalTo(null)); - assertThat(bundle.getBoolean(EXTRA_READ_ONLY), equalTo(false)); - assertThat(bundle.getBoolean(EXTRA_DRAGGABLE_ONLY), equalTo((Object) true)); - } - @Test public void requestGeoTrace_opensDialog() { activityGeoDataRequester.requestGeoPoly(prompt); @@ -254,7 +173,7 @@ public void requestGeoPoint_whenWidgetHasAllowMockAccuracy_addsItToIntent() { when(prompt.getBindAttributes()) .thenReturn(asList(TreeElement.constructAttributeElement("odk", "allow-mock-accuracy", "true"))); - activityGeoDataRequester.requestGeoPoint(prompt, "1.0 2.0 3 4", waitingForDataRegistry); + activityGeoDataRequester.requestGeoPoint(prompt, waitingForDataRegistry); Intent startedIntent = shadowActivity.getNextStartedActivity(); assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class)); @@ -263,7 +182,7 @@ public void requestGeoPoint_whenWidgetHasAllowMockAccuracy_addsItToIntent() { when(prompt.getBindAttributes()) .thenReturn(asList(TreeElement.constructAttributeElement("odk", "allow-mock-accuracy", "false"))); - activityGeoDataRequester.requestGeoPoint(prompt, "1.0 2.0 3 4", waitingForDataRegistry); + activityGeoDataRequester.requestGeoPoint(prompt, waitingForDataRegistry); startedIntent = shadowActivity.getNextStartedActivity(); assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class)); diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/utilities/GeoPointMapDialogFragmentTest.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/utilities/GeoPointMapDialogFragmentTest.kt new file mode 100644 index 00000000000..efa0848d337 --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/utilities/GeoPointMapDialogFragmentTest.kt @@ -0,0 +1,144 @@ +package org.odk.collect.android.widgets.utilities + +import android.R +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.notNullValue +import org.javarosa.core.model.Constants +import org.javarosa.core.model.data.GeoPointData +import org.javarosa.core.model.data.GeoShapeData +import org.javarosa.core.model.data.GeoTraceData +import org.javarosa.form.api.FormEntryController +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.odk.collect.android.formentry.FormEntryViewModel +import org.odk.collect.android.javarosawrapper.FailedValidationResult +import org.odk.collect.android.javarosawrapper.SuccessValidationResult +import org.odk.collect.android.javarosawrapper.ValidationResult +import org.odk.collect.android.support.CollectHelpers +import org.odk.collect.android.support.MockFormEntryPromptBuilder +import org.odk.collect.android.utilities.Appearances +import org.odk.collect.android.widgets.geo.GeoPointMapDialogFragment +import org.odk.collect.android.widgets.geo.GeoPolyDialogFragment +import org.odk.collect.android.widgets.geo.ReferenceGeometryMappableData +import org.odk.collect.android.widgets.items.GeoSelectChoiceElements +import org.odk.collect.android.widgets.support.FormElementFixtures.selectChoice +import org.odk.collect.android.widgets.support.FormElementFixtures.treeElement +import org.odk.collect.android.widgets.utilities.AdditionalAttributes.INCREMENTAL +import org.odk.collect.android.widgets.utilities.WidgetAnswerDialogFragment.Companion.ARG_FORM_INDEX +import org.odk.collect.android.widgets.viewmodels.QuestionViewModel +import org.odk.collect.androidshared.ui.DisplayString +import org.odk.collect.androidshared.ui.FragmentFactoryBuilder +import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule +import org.odk.collect.geo.GeoUtils.toMapPoint +import org.odk.collect.geo.geopoint.GeoPointMapFragment +import org.odk.collect.geo.geopoly.GeoPolyFragment +import org.odk.collect.geo.geopoly.GeoPolyFragment.OutputMode +import org.odk.collect.geo.items.MappableItem +import org.odk.collect.maps.MapPoint +import org.odk.collect.testshared.FakeScheduler +import org.odk.collect.testshared.getOrAwaitValue + +@RunWith(AndroidJUnit4::class) +class GeoPointMapDialogFragmentTest { + + private var prompt = MockFormEntryPromptBuilder().build() + private val formEntryViewModel = mock { + on { getQuestionPrompt(prompt.index) } doReturn prompt + } + + private val viewModelFactory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class, extras: CreationExtras): T { + return when (modelClass) { + FormEntryViewModel::class.java -> formEntryViewModel as T + else -> throw IllegalArgumentException() + } + } + } + + @get:Rule + val launcherRule = + FragmentScenarioLauncherRule( + FragmentFactoryBuilder() + .forClass(GeoPointMapDialogFragment::class) { + GeoPointMapDialogFragment(viewModelFactory) + }.build() + ) + + @Before + fun setup() { + CollectHelpers.setupDemoProject() + } + + @Test + fun `configures GeoPointMapFragment with answer`() { + val answer = GeoPointData(doubleArrayOf(5.0, 6.0, 7.0, 8.0)) + prompt = MockFormEntryPromptBuilder(prompt) + .withAnswer(answer) + .build() + + launcherRule.launchAndAssertOnChild( + GeoPointMapDialogFragment::class, + bundleOf(ARG_FORM_INDEX to prompt.index) + ) { + assertThat(it.inputPoint, equalTo(answer.toMapPoint())) + } + } + + @Test + fun `configures GeoPointMapFragment as not draggable for maps appearance`() { + prompt = MockFormEntryPromptBuilder(prompt) + .withAppearance(Appearances.MAPS) + .build() + + launcherRule.launchAndAssertOnChild( + GeoPointMapDialogFragment::class, + bundleOf(ARG_FORM_INDEX to prompt.index) + ) { + assertThat(it.draggable, equalTo(false)) + } + } + + @Test + fun `configures GeoPointMapFragment as draggable for placement map appearance`() { + prompt = MockFormEntryPromptBuilder(prompt) + .withAppearance(Appearances.PLACEMENT_MAP) + .build() + + launcherRule.launchAndAssertOnChild( + GeoPointMapDialogFragment::class, + bundleOf(ARG_FORM_INDEX to prompt.index) + ) { + assertThat(it.draggable, equalTo(true)) + } + } + + @Test + fun `configures GeoPointMapFragment as read only when prompt is`() { + prompt = MockFormEntryPromptBuilder(prompt) + .withReadOnly(true) + .build() + + launcherRule.launchAndAssertOnChild( + GeoPointMapDialogFragment::class, + bundleOf(ARG_FORM_INDEX to prompt.index) + ) { + assertThat(it.readOnly, equalTo(true)) + } + } +} diff --git a/geo/src/main/AndroidManifest.xml b/geo/src/main/AndroidManifest.xml index f7b1bc5811f..df3afddb5e7 100644 --- a/geo/src/main/AndroidManifest.xml +++ b/geo/src/main/AndroidManifest.xml @@ -2,10 +2,6 @@ - - diff --git a/geo/src/main/java/org/odk/collect/geo/DaggerSetup.kt b/geo/src/main/java/org/odk/collect/geo/DaggerSetup.kt index 197b997f452..5821ee7d481 100644 --- a/geo/src/main/java/org/odk/collect/geo/DaggerSetup.kt +++ b/geo/src/main/java/org/odk/collect/geo/DaggerSetup.kt @@ -10,7 +10,7 @@ import dagger.Provides import org.odk.collect.async.Scheduler import org.odk.collect.geo.geopoint.GeoPointActivity import org.odk.collect.geo.geopoint.GeoPointDialogFragment -import org.odk.collect.geo.geopoint.GeoPointMapActivity +import org.odk.collect.geo.geopoint.GeoPointMapFragment import org.odk.collect.geo.geopoint.GeoPointViewModelFactory import org.odk.collect.geo.geopoint.LocationTrackerGeoPointViewModel import org.odk.collect.geo.geopoly.GeoPolyFragment @@ -44,11 +44,11 @@ interface GeoDependencyComponent { fun build(): GeoDependencyComponent } - fun inject(geoPointMapActivity: GeoPointMapActivity) fun inject(geoPointDialogFragment: GeoPointDialogFragment) fun inject(geoPointActivity: GeoPointActivity) fun inject(selectionMapFragment: SelectionMapFragment) fun inject(geoPolyFragment: GeoPolyFragment) + fun inject(geoPointMapFragment: GeoPointMapFragment) val scheduler: Scheduler val locationTracker: LocationTracker diff --git a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java deleted file mode 100644 index 0141dadc846..00000000000 --- a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java +++ /dev/null @@ -1,427 +0,0 @@ -/* - * Copyright (C) 2011 University of Washington - * - * 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 distributed 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 the specific language governing permissions and limitations under - * the License. - */ - -package org.odk.collect.geo.geopoint; - -import static org.odk.collect.androidshared.ui.EdgeToEdge.setView; -import static org.odk.collect.geo.Constants.EXTRA_DRAGGABLE_ONLY; -import static org.odk.collect.geo.Constants.EXTRA_READ_ONLY; -import static org.odk.collect.geo.Constants.EXTRA_RETAIN_MOCK_ACCURACY; -import static org.odk.collect.geo.GeoActivityUtils.requireLocationPermissions; -import static org.odk.collect.geo.GeoUtils.showCurrentLocation; -import static org.odk.collect.geo.GeoUtils.toMapPoint; -import static org.odk.collect.location.tracker.LocationTrackerKt.getCurrentLocation; -import static org.odk.collect.maps.MapFragmentKt.addMarker; - -import android.annotation.SuppressLint; -import android.content.Intent; -import android.graphics.Color; -import android.os.Bundle; -import android.view.View; -import android.view.Window; -import android.widget.ImageButton; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentContainerView; - -import org.odk.collect.androidshared.ui.DialogFragmentUtils; -import org.odk.collect.androidshared.ui.FragmentFactoryBuilder; -import org.odk.collect.androidshared.ui.ToastUtils; -import org.odk.collect.async.Scheduler; -import org.odk.collect.externalapp.ExternalAppUtils; -import org.odk.collect.geo.GeoDependencyComponentProvider; -import org.odk.collect.geo.R; -import org.odk.collect.location.Location; -import org.odk.collect.location.tracker.LocationTracker; -import org.odk.collect.maps.MapFragment; -import org.odk.collect.maps.MapFragmentFactory; -import org.odk.collect.maps.MapPoint; -import org.odk.collect.maps.circles.CurrentLocationDelegate; -import org.odk.collect.maps.layers.OfflineMapLayersPickerBottomSheetDialogFragment; -import org.odk.collect.maps.layers.ReferenceLayerRepository; -import org.odk.collect.maps.markers.MarkerDescription; -import org.odk.collect.maps.markers.MarkerIconDescription; -import org.odk.collect.settings.SettingsProvider; -import org.odk.collect.strings.localization.LocalizedActivity; -import org.odk.collect.webpage.WebPageService; - -import java.util.Arrays; - -import javax.inject.Inject; - -import kotlin.Unit; -import timber.log.Timber; - -/** - * Allow the user to indicate a location by placing a marker on a map, either - * by touching a point on the map or by tapping a button to place the marker - * at the current location (obtained from GPS or other location sensors). - */ -public class GeoPointMapActivity extends LocalizedActivity { - - public static final String POINT_KEY = "point"; - - public static final String IS_DRAGGED_KEY = "is_dragged"; - public static final String CAPTURE_LOCATION_KEY = "capture_location"; - public static final String FOUND_FIRST_LOCATION_KEY = "found_first_location"; - public static final String SET_CLEAR_KEY = "set_clear"; - public static final String POINT_FROM_INTENT_KEY = "point_from_intent"; - public static final String INTENT_READ_ONLY_KEY = "intent_read_only"; - public static final String INTENT_DRAGGABLE_KEY = "intent_draggable"; - public static final String IS_POINT_LOCKED_KEY = "is_point_locked"; - - public static final String PLACE_MARKER_BUTTON_ENABLED_KEY = "place_marker_button_enabled"; - public static final String ZOOM_BUTTON_ENABLED_KEY = "zoom_button_enabled"; - public static final String CLEAR_BUTTON_ENABLED_KEY = "clear_button_enabled"; - public static final String LOCATION_STATUS_VISIBILITY_KEY = "location_status_visibility"; - public static final String LOCATION_INFO_VISIBILITY_KEY = "location_info_visibility"; - - public static final String EXTRA_LOCATION = "gp"; - public static final String MARKER_COLOR = "#52C268"; - - protected Bundle previousState; - - @Inject - MapFragmentFactory mapFragmentFactory; - - @Inject - ReferenceLayerRepository referenceLayerRepository; - - @Inject - Scheduler scheduler; - - @Inject - SettingsProvider settingsProvider; - - @Inject - WebPageService webPageService; - - @Inject - LocationTracker locationTracker; - - private MapFragment map; - private int featureId = -1; // will be a positive featureId once map is ready - - private AccuracyStatusView locationStatus; - - private MapPoint location; - private ImageButton placeMarkerButton; - - private boolean isDragged; - - private ImageButton zoomButton; - private ImageButton clearButton; - - private boolean captureLocation; - - /** - * True if a tap on the clear button removed an existing marker and - * no new marker has been placed. - */ - private boolean setClear; - - /** - * True if the current point came from the intent. - */ - private boolean pointFromIntent; - - /** - * True if the intent requested for the point to be read-only. - */ - private boolean intentReadOnly; - - /** - * True if the intent requested for the marker to be draggable. - */ - private boolean intentDraggable; - - /** - * While true, the point cannot be moved by dragging or long-pressing. - */ - private boolean isPointLocked; - - private final CurrentLocationDelegate currentLocationDelegate = new CurrentLocationDelegate(); - - @Override - public void onCreate(Bundle savedInstanceState) { - ((GeoDependencyComponentProvider) getApplication()).getGeoDependencyComponent().inject(this); - getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() - .forClass(MapFragment.class, () -> (Fragment) mapFragmentFactory.createMapFragment()) - .forClass(OfflineMapLayersPickerBottomSheetDialogFragment.class, () -> new OfflineMapLayersPickerBottomSheetDialogFragment(getActivityResultRegistry(), referenceLayerRepository, scheduler, settingsProvider, webPageService)) - .build() - ); - super.onCreate(savedInstanceState); - - requireLocationPermissions(this); - - previousState = savedInstanceState; - - requestWindowFeature(Window.FEATURE_NO_TITLE); - try { - setView(this, R.layout.geopoint_layout, false); - } catch (NoClassDefFoundError e) { - Timber.e(e, "Google maps not accessible due to: %s ", e.getMessage()); - ToastUtils.showShortToast(org.odk.collect.strings.R.string.google_play_services_error_occured); - finish(); - return; - } - - locationStatus = findViewById(R.id.status_section); - placeMarkerButton = findViewById(R.id.place_marker); - zoomButton = findViewById(R.id.zoom); - - MapFragment mapFragment = ((FragmentContainerView) findViewById(R.id.map_container)).getFragment(); - mapFragment.init(this::initMap, this::finish); - } - - @Override - protected void onSaveInstanceState(Bundle state) { - super.onSaveInstanceState(state); - if (map == null) { - // initMap() is called asynchronously, so map can be null if the activity - // is stopped (e.g. by screen rotation) before initMap() gets to run. - // In this case, preserve any provided instance state. - if (previousState != null) { - state.putAll(previousState); - } - return; - } - - state.putParcelable(POINT_KEY, map.getMarkerPoint(featureId)); - - // Flags - state.putBoolean(IS_DRAGGED_KEY, isDragged); - state.putBoolean(CAPTURE_LOCATION_KEY, captureLocation); - state.putBoolean(SET_CLEAR_KEY, setClear); - state.putBoolean(POINT_FROM_INTENT_KEY, pointFromIntent); - state.putBoolean(INTENT_READ_ONLY_KEY, intentReadOnly); - state.putBoolean(INTENT_DRAGGABLE_KEY, intentDraggable); - state.putBoolean(IS_POINT_LOCKED_KEY, isPointLocked); - - // UI state - state.putBoolean(PLACE_MARKER_BUTTON_ENABLED_KEY, placeMarkerButton.isEnabled()); - state.putBoolean(ZOOM_BUTTON_ENABLED_KEY, zoomButton.isEnabled()); - state.putBoolean(CLEAR_BUTTON_ENABLED_KEY, clearButton.isEnabled()); - state.putInt(LOCATION_STATUS_VISIBILITY_KEY, locationStatus.getVisibility()); - } - - public void returnLocation() { - String result = null; - - if (setClear || (intentReadOnly && featureId == -1)) { - result = ""; - } else if (isDragged || intentReadOnly || pointFromIntent) { - result = formatResult(map.getMarkerPoint(featureId)); - } else if (location != null) { - result = formatResult(location); - } - - if (result != null) { - ExternalAppUtils.returnSingleValue(this, result); - } else { - finish(); - } - } - - @SuppressLint("MissingPermission") // Permission handled in Constructor - public void initMap(MapFragment newMapFragment) { - map = newMapFragment; - map.setDragEndListener(this::onDragEnd); - map.setLongPressListener(this::onLongPress); - - ImageButton acceptLocation = findViewById(R.id.accept_location); - acceptLocation.setOnClickListener(v -> returnLocation()); - - placeMarkerButton.setEnabled(false); - placeMarkerButton.setOnClickListener(v -> { - Location currentLocation = getCurrentLocation(locationTracker); - - if (currentLocation != null) { - MapPoint mapPoint = toMapPoint(currentLocation); - placeMarker(mapPoint); - zoomToMarker(true); - } - }); - - // Focuses on marked location - zoomButton.setEnabled(false); - zoomButton.setOnClickListener(v -> currentLocationDelegate.zoomToCurrentLocation(map)); - - // Menu Layer Toggle - findViewById(R.id.layer_menu).setOnClickListener(v -> { - DialogFragmentUtils.showIfNotShowing(OfflineMapLayersPickerBottomSheetDialogFragment.class, getSupportFragmentManager()); - }); - - clearButton = findViewById(R.id.clear); - clearButton.setEnabled(false); - clearButton.setOnClickListener(v -> { - clear(); - locationStatus.setVisibility(View.VISIBLE); - pointFromIntent = false; - }); - - Intent intent = getIntent(); - if (intent != null && intent.getExtras() != null) { - intentDraggable = intent.getBooleanExtra(EXTRA_DRAGGABLE_ONLY, false); - if (!intentDraggable) { - // Not Draggable, set text for Map else leave as placement-map text - locationStatus.setTitle(getString(org.odk.collect.strings.R.string.geopoint_no_draggable_instruction)); - } - - intentReadOnly = intent.getBooleanExtra(EXTRA_READ_ONLY, false); - if (intentReadOnly) { - captureLocation = true; - clearButton.setEnabled(false); - } - - if (intent.hasExtra(EXTRA_LOCATION)) { - MapPoint point = intent.getParcelableExtra(EXTRA_LOCATION); - - // If the point is initially set from the intent, the "place marker" - // button, dragging, and long-pressing are all initially disabled. - // To enable them, the user must clear the marker and add a new one. - isPointLocked = true; - placeMarker(point); - placeMarkerButton.setEnabled(false); - - captureLocation = true; - pointFromIntent = true; - locationStatus.setVisibility(View.GONE); - zoomButton.setEnabled(true); - zoomToMarker(false); - } - } - - if (previousState != null) { - restoreFromInstanceState(previousState); - } - - boolean retainMockAccuracy = getIntent().getBooleanExtra(EXTRA_RETAIN_MOCK_ACCURACY, false); - showCurrentLocation(map, locationTracker, currentLocationDelegate, retainMockAccuracy, mapPoint -> { - onLocationChanged(mapPoint); - return Unit.INSTANCE; - }); - } - - protected void restoreFromInstanceState(Bundle state) { - isDragged = state.getBoolean(IS_DRAGGED_KEY, false); - captureLocation = state.getBoolean(CAPTURE_LOCATION_KEY, false); - setClear = state.getBoolean(SET_CLEAR_KEY, false); - pointFromIntent = state.getBoolean(POINT_FROM_INTENT_KEY, false); - intentReadOnly = state.getBoolean(INTENT_READ_ONLY_KEY, false); - intentDraggable = state.getBoolean(INTENT_DRAGGABLE_KEY, false); - isPointLocked = state.getBoolean(IS_POINT_LOCKED_KEY, false); - - // Restore the marker and dialog after the flags, because they use some of them. - MapPoint point = state.getParcelable(POINT_KEY); - if (point != null) { - placeMarker(point); - } - - // Restore the flags again, because placeMarker() and clear() modify some of them. - isDragged = state.getBoolean(IS_DRAGGED_KEY, false); - captureLocation = state.getBoolean(CAPTURE_LOCATION_KEY, false); - setClear = state.getBoolean(SET_CLEAR_KEY, false); - pointFromIntent = state.getBoolean(POINT_FROM_INTENT_KEY, false); - intentReadOnly = state.getBoolean(INTENT_READ_ONLY_KEY, false); - intentDraggable = state.getBoolean(INTENT_DRAGGABLE_KEY, false); - isPointLocked = state.getBoolean(IS_POINT_LOCKED_KEY, false); - - placeMarkerButton.setEnabled(state.getBoolean(PLACE_MARKER_BUTTON_ENABLED_KEY, false)); - zoomButton.setEnabled(state.getBoolean(ZOOM_BUTTON_ENABLED_KEY, false)); - clearButton.setEnabled(state.getBoolean(CLEAR_BUTTON_ENABLED_KEY, false)); - - locationStatus.setVisibility(state.getInt(LOCATION_STATUS_VISIBILITY_KEY, View.GONE)); - } - - public void onLocationChanged(MapPoint point) { - if (setClear) { - placeMarkerButton.setEnabled(true); - } - - if (point != null) { - enableZoomButton(true); - - if (!captureLocation && !setClear) { - placeMarker(point); - placeMarkerButton.setEnabled(true); - } - - locationStatus.setAccuracy(new LocationAccuracy.Improving((float) point.accuracy)); - } - } - - public String formatResult(MapPoint point) { - return String.format("%s %s %s %s", point.latitude, point.longitude, point.altitude, point.accuracy); - } - - public void onDragEnd(int draggedFeatureId) { - if (draggedFeatureId == featureId) { - isDragged = true; - captureLocation = true; - setClear = false; - map.setCenter(map.getMarkerPoint(featureId), true); - } - } - - public void onLongPress(MapPoint point) { - if (intentDraggable && !intentReadOnly && !isPointLocked) { - placeMarker(point); - enableZoomButton(true); - isDragged = true; - } - } - - private void enableZoomButton(boolean shouldEnable) { - if (zoomButton != null) { - zoomButton.setEnabled(shouldEnable); - } - } - - public void zoomToMarker(boolean animate) { - map.zoomToPoint(map.getMarkerPoint(featureId), animate); - } - - private void clear() { - map.clearFeatures(Arrays.asList(featureId)); - featureId = -1; - clearButton.setEnabled(false); - placeMarkerButton.setEnabled(true); - - isPointLocked = false; - isDragged = false; - captureLocation = false; - setClear = true; - } - - /** - * Places the marker and enables the button to remove it. - */ - private void placeMarker(@NonNull MapPoint point) { - this.location = point; - - if (featureId != -1) { - map.clearFeatures(Arrays.asList(featureId)); - } - - MarkerIconDescription.DrawableResource iconDescription = new MarkerIconDescription.DrawableResource(org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big, Color.parseColor(MARKER_COLOR)); - featureId = addMarker(map, new MarkerDescription(point, intentDraggable && !intentReadOnly && !isPointLocked, MapFragment.IconAnchor.BOTTOM, iconDescription)); - if (!intentReadOnly) { - clearButton.setEnabled(true); - } - captureLocation = true; - setClear = false; - } -} diff --git a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapFragment.kt new file mode 100644 index 00000000000..9a532a89dcd --- /dev/null +++ b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapFragment.kt @@ -0,0 +1,436 @@ +/* + * Copyright (C) 2011 University of Washington + * + * 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 distributed 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 the specific language governing permissions and limitations under + * the License. + */ +package org.odk.collect.geo.geopoint + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import androidx.core.graphics.toColorInt +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentContainerView +import org.odk.collect.androidshared.system.BundleExt.getParcelableExtraCompat +import org.odk.collect.androidshared.ui.DialogFragmentUtils.showIfNotShowing +import org.odk.collect.androidshared.ui.FragmentFactoryBuilder +import org.odk.collect.async.Scheduler +import org.odk.collect.geo.GeoDependencyComponentProvider +import org.odk.collect.geo.GeoUtils.showCurrentLocation +import org.odk.collect.geo.GeoUtils.toMapPoint +import org.odk.collect.geo.R +import org.odk.collect.geo.geopoint.LocationAccuracy.Improving +import org.odk.collect.location.tracker.LocationTracker +import org.odk.collect.location.tracker.getCurrentLocation +import org.odk.collect.maps.MapFragment +import org.odk.collect.maps.MapFragmentFactory +import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.addMarker +import org.odk.collect.maps.circles.CurrentLocationDelegate +import org.odk.collect.maps.layers.OfflineMapLayersPickerBottomSheetDialogFragment +import org.odk.collect.maps.layers.ReferenceLayerRepository +import org.odk.collect.maps.markers.MarkerDescription +import org.odk.collect.maps.markers.MarkerIconDescription +import org.odk.collect.settings.SettingsProvider +import org.odk.collect.webpage.WebPageService +import javax.inject.Inject + +class GeoPointMapFragment( + val inputPoint: MapPoint?, + val draggable: Boolean, + val readOnly: Boolean, + val retainMockAccuracy: Boolean, +) : Fragment() { + + @Inject + lateinit var mapFragmentFactory: MapFragmentFactory + + @Inject + lateinit var referenceLayerRepository: ReferenceLayerRepository + + @Inject + lateinit var scheduler: Scheduler + + @Inject + lateinit var settingsProvider: SettingsProvider + + @Inject + lateinit var webPageService: WebPageService + + @Inject + lateinit var locationTracker: LocationTracker + + private var previousState: Bundle? = null + + private var map: MapFragment? = null + private var featureId = -1 // will be a positive featureId once map is ready + + private var locationStatus: AccuracyStatusView? = null + + private var location: MapPoint? = null + private var placeMarkerButton: ImageButton? = null + + private var isDragged = false + + private var zoomButton: ImageButton? = null + private var clearButton: ImageButton? = null + + private var captureLocation = false + + /** + * True if a tap on the clear button removed an existing marker and + * no new marker has been placed. + */ + private var setClear = false + + /** + * True if the current point came from the intent. + */ + private var pointFromIntent = false + + /** + * While true, the point cannot be moved by dragging or long-pressing. + */ + private var isPointLocked = false + + private val currentLocationDelegate = CurrentLocationDelegate() + + override fun onAttach(context: Context) { + super.onAttach(context) + (context.applicationContext as GeoDependencyComponentProvider) + .geoDependencyComponent.inject(this) + + childFragmentManager.fragmentFactory = FragmentFactoryBuilder() + .forClass(MapFragment::class.java) { mapFragmentFactory.createMapFragment() as Fragment } + .forClass(OfflineMapLayersPickerBottomSheetDialogFragment::class.java) { + OfflineMapLayersPickerBottomSheetDialogFragment( + requireActivity().activityResultRegistry, + referenceLayerRepository, + scheduler, + settingsProvider, + webPageService + ) + } + .build() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + previousState = savedInstanceState + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.geopoint_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + locationStatus = view.findViewById(R.id.status_section) + ?: throw IllegalStateException("Status section not found") + placeMarkerButton = view.findViewById(R.id.place_marker) + zoomButton = view.findViewById(R.id.zoom) + + val mapFragment: MapFragment = + (view.findViewById(R.id.map_container) as FragmentContainerView).getFragment() + mapFragment.init( + { newMapFragment: MapFragment -> this.initMap(newMapFragment) }, + { cancel() } + ) + } + + override fun onSaveInstanceState(state: Bundle) { + super.onSaveInstanceState(state) + if (map == null) { + // initMap() is called asynchronously, so map can be null if the fragment + // is stopped (e.g. by screen rotation) before initMap() gets to run. + // In this case, preserve any provided instance state. + if (previousState != null) { + state.putAll(previousState) + } + return + } + + state.putParcelable(POINT_KEY, map!!.getMarkerPoint(featureId)) + + // Flags + state.putBoolean(IS_DRAGGED_KEY, isDragged) + state.putBoolean(CAPTURE_LOCATION_KEY, captureLocation) + state.putBoolean(SET_CLEAR_KEY, setClear) + state.putBoolean(POINT_FROM_INTENT_KEY, pointFromIntent) + state.putBoolean(IS_POINT_LOCKED_KEY, isPointLocked) + + // UI state + state.putBoolean(PLACE_MARKER_BUTTON_ENABLED_KEY, placeMarkerButton!!.isEnabled) + state.putBoolean(ZOOM_BUTTON_ENABLED_KEY, zoomButton!!.isEnabled) + state.putBoolean(CLEAR_BUTTON_ENABLED_KEY, clearButton!!.isEnabled) + state.putInt(LOCATION_STATUS_VISIBILITY_KEY, locationStatus!!.visibility) + } + + private fun returnLocation() { + var result: String? = null + + if (setClear || (readOnly && featureId == -1)) { + result = "" + } else if (isDragged || readOnly || pointFromIntent) { + result = formatResult(map!!.getMarkerPoint(featureId)!!) + } else if (location != null) { + result = formatResult(location!!) + } + + if (result != null) { + parentFragmentManager.setFragmentResult( + REQUEST_GEOPOINT, + bundleOf(RESULT_GEOPOINT to result) + ) + } else { + cancel() + } + } + + @SuppressLint("MissingPermission") // Permission handled in Constructor + private fun initMap(newMapFragment: MapFragment?) { + map = newMapFragment + map!!.setDragEndListener { draggedFeatureId: Int -> + this.onDragEnd(draggedFeatureId) + } + map!!.setLongPressListener { point: MapPoint -> this.onLongPress(point) } + + val acceptLocation = view?.findViewById(R.id.accept_location) + acceptLocation?.setOnClickListener { returnLocation() } + + placeMarkerButton!!.isEnabled = false + placeMarkerButton!!.setOnClickListener { + val currentLocation = locationTracker.getCurrentLocation() + if (currentLocation != null) { + val mapPoint = currentLocation.toMapPoint() + placeMarker(mapPoint) + zoomToMarker(true) + } + } + + // Focuses on marked location + zoomButton!!.isEnabled = false + zoomButton!!.setOnClickListener { + currentLocationDelegate.zoomToCurrentLocation(map!!) + } + + // Menu Layer Toggle + view?.findViewById(R.id.layer_menu)?.setOnClickListener { + showIfNotShowing( + OfflineMapLayersPickerBottomSheetDialogFragment::class.java, + childFragmentManager + ) + } + + clearButton = + view?.findViewById(R.id.clear) ?: throw IllegalStateException("Clear button not found") + clearButton!!.isEnabled = false + clearButton!!.setOnClickListener { + clear() + locationStatus!!.visibility = View.VISIBLE + pointFromIntent = false + } + + if (!draggable) { + // Not Draggable, set text for Map else leave as placement-map text + locationStatus!!.title = + getString(org.odk.collect.strings.R.string.geopoint_no_draggable_instruction) + } + + if (readOnly) { + captureLocation = true + clearButton!!.isEnabled = false + } + + if (inputPoint != null) { + // If the point is initially set, the "place marker" + // button, dragging, and long-pressing are all initially disabled. + // To enable them, the user must clear the marker and add a new one. + isPointLocked = true + placeMarker(inputPoint) + placeMarkerButton!!.isEnabled = false + + captureLocation = true + pointFromIntent = true + locationStatus!!.visibility = View.GONE + zoomButton!!.isEnabled = true + zoomToMarker(false) + } + + if (previousState != null) { + restoreFromInstanceState(previousState!!) + } + + map!!.showCurrentLocation( + locationTracker, + currentLocationDelegate, + retainMockAccuracy + ) { mapPoint: MapPoint? -> + onLocationChanged(mapPoint) + } + } + + private fun restoreFromInstanceState(state: Bundle) { + isDragged = state.getBoolean(IS_DRAGGED_KEY, false) + captureLocation = state.getBoolean(CAPTURE_LOCATION_KEY, false) + setClear = state.getBoolean(SET_CLEAR_KEY, false) + pointFromIntent = state.getBoolean(POINT_FROM_INTENT_KEY, false) + isPointLocked = state.getBoolean(IS_POINT_LOCKED_KEY, false) + + // Restore the marker and dialog after the flags, because they use some of them. + val point = state.getParcelableExtraCompat(POINT_KEY) + if (point != null) { + placeMarker(point) + } + + // Restore the flags again, because placeMarker() and clear() modify some of them. + isDragged = state.getBoolean(IS_DRAGGED_KEY, false) + captureLocation = state.getBoolean(CAPTURE_LOCATION_KEY, false) + setClear = state.getBoolean(SET_CLEAR_KEY, false) + pointFromIntent = state.getBoolean(POINT_FROM_INTENT_KEY, false) + isPointLocked = state.getBoolean(IS_POINT_LOCKED_KEY, false) + + placeMarkerButton!!.isEnabled = state.getBoolean(PLACE_MARKER_BUTTON_ENABLED_KEY, false) + zoomButton!!.isEnabled = state.getBoolean(ZOOM_BUTTON_ENABLED_KEY, false) + clearButton!!.isEnabled = state.getBoolean(CLEAR_BUTTON_ENABLED_KEY, false) + + locationStatus!!.visibility = state.getInt(LOCATION_STATUS_VISIBILITY_KEY, View.GONE) + } + + private fun onLocationChanged(point: MapPoint?) { + if (setClear) { + placeMarkerButton!!.isEnabled = true + } + + if (point != null) { + enableZoomButton() + + if (!captureLocation && !setClear) { + placeMarker(point) + placeMarkerButton!!.isEnabled = true + } + + locationStatus!!.accuracy = Improving(point.accuracy.toFloat()) + } + } + + private fun formatResult(point: MapPoint): String { + return String.format( + "%s %s %s %s", + point.latitude, + point.longitude, + point.altitude, + point.accuracy + ) + } + + private fun cancel() { + getParentFragmentManager().setFragmentResult(REQUEST_GEOPOINT, Bundle.EMPTY) + } + + private fun onDragEnd(draggedFeatureId: Int) { + if (draggedFeatureId == featureId) { + isDragged = true + captureLocation = true + setClear = false + map!!.setCenter(map!!.getMarkerPoint(featureId), true) + } + } + + private fun onLongPress(point: MapPoint) { + if (draggable && !readOnly && !isPointLocked) { + placeMarker(point) + enableZoomButton() + isDragged = true + } + } + + private fun enableZoomButton() { + if (zoomButton != null) { + zoomButton!!.isEnabled = true + } + } + + private fun zoomToMarker(animate: Boolean) { + map!!.zoomToPoint(map!!.getMarkerPoint(featureId), animate) + } + + private fun clear() { + map!!.clearFeatures(listOf(featureId)) + featureId = -1 + clearButton!!.isEnabled = false + placeMarkerButton!!.isEnabled = true + + isPointLocked = false + isDragged = false + captureLocation = false + setClear = true + } + + /** + * Places the marker and enables the button to remove it. + */ + private fun placeMarker(point: MapPoint) { + this.location = point + + if (featureId != -1) { + map!!.clearFeatures(listOf(featureId)) + } + + val iconDescription = MarkerIconDescription.DrawableResource( + org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big, + MARKER_COLOR.toColorInt() + ) + + featureId = map!!.addMarker( + MarkerDescription( + point, + draggable && !readOnly && !isPointLocked, + MapFragment.IconAnchor.BOTTOM, + iconDescription + ) + ) + if (!readOnly) { + clearButton!!.isEnabled = true + } + captureLocation = true + setClear = false + } + + companion object { + const val POINT_KEY: String = "point" + + const val IS_DRAGGED_KEY: String = "is_dragged" + const val CAPTURE_LOCATION_KEY: String = "capture_location" + const val SET_CLEAR_KEY: String = "set_clear" + const val POINT_FROM_INTENT_KEY: String = "point_from_intent" + const val IS_POINT_LOCKED_KEY: String = "is_point_locked" + + const val PLACE_MARKER_BUTTON_ENABLED_KEY: String = "place_marker_button_enabled" + const val ZOOM_BUTTON_ENABLED_KEY: String = "zoom_button_enabled" + const val CLEAR_BUTTON_ENABLED_KEY: String = "clear_button_enabled" + const val LOCATION_STATUS_VISIBILITY_KEY: String = "location_status_visibility" + + const val MARKER_COLOR: String = "#52C268" + const val REQUEST_GEOPOINT: String = "geopoint" + const val RESULT_GEOPOINT: String = "geopoint" + } +} \ No newline at end of file diff --git a/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.kt b/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapFragmentTest.kt similarity index 52% rename from geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.kt rename to geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapFragmentTest.kt index 26b38e93510..b8a63f10ef0 100644 --- a/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapFragmentTest.kt @@ -1,32 +1,28 @@ package org.odk.collect.geo.geopoint -import android.app.Activity import android.app.Application -import android.content.Intent -import android.view.View -import android.widget.TextView import androidx.test.core.app.ApplicationProvider -import androidx.test.espresso.Espresso -import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.not import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito -import org.odk.collect.androidtest.ActivityScenarioLauncherRule +import org.mockito.kotlin.mock +import org.odk.collect.androidtest.FragmentScenarioExtensions.setFragmentResultListener import org.odk.collect.async.Scheduler -import org.odk.collect.externalapp.ExternalAppUtils.getReturnedSingleValue -import org.odk.collect.geo.Constants.EXTRA_RETAIN_MOCK_ACCURACY +import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.geo.DaggerGeoDependencyComponent import org.odk.collect.geo.GeoDependencyModule -import org.odk.collect.geo.GeoUtils import org.odk.collect.geo.GeoUtils.toMapPoint +import org.odk.collect.geo.R import org.odk.collect.geo.support.FakeLocationTracker import org.odk.collect.geo.support.FakeMapFragment import org.odk.collect.geo.support.MapFragmentAssertions.hasZoomedToCurrentLocation @@ -34,6 +30,7 @@ import org.odk.collect.geo.support.MapFragmentAssertions.showsCurrentLocation import org.odk.collect.geo.support.RobolectricApplication import org.odk.collect.location.Location import org.odk.collect.location.tracker.LocationTracker +import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapFragmentFactory import org.odk.collect.maps.MapPoint import org.odk.collect.maps.circles.CurrentLocationDelegate @@ -43,109 +40,82 @@ import org.odk.collect.settings.SettingsProvider import org.odk.collect.strings.R.string import org.odk.collect.testshared.EspressoAssertions import org.odk.collect.testshared.EspressoInteractions +import org.odk.collect.testshared.FragmentResultRecorder import org.odk.collect.webpage.WebPageService import org.robolectric.Shadows @RunWith(AndroidJUnit4::class) -class GeoPointMapActivityTest { - - private val mapFragment = FakeMapFragment() +class GeoPointMapFragmentTest { + private val map = FakeMapFragment(ready = true) private val locationTracker = FakeLocationTracker() @get:Rule - val launcherRule: ActivityScenarioLauncherRule = ActivityScenarioLauncherRule() + val launcherRule = FragmentScenarioLauncherRule() + + private val application = ApplicationProvider.getApplicationContext() @Before fun setUp() { val shadowApplication = - Shadows.shadowOf(ApplicationProvider.getApplicationContext()) + Shadows.shadowOf(application) shadowApplication.grantPermissions("android.permission.ACCESS_FINE_LOCATION") shadowApplication.grantPermissions("android.permission.ACCESS_COARSE_LOCATION") - - val application = ApplicationProvider.getApplicationContext() - application.geoDependencyComponent = DaggerGeoDependencyComponent.builder() - .application(application) - .geoDependencyModule(object : GeoDependencyModule() { - override fun providesMapFragmentFactory(): MapFragmentFactory { - return MapFragmentFactory { mapFragment } - } - - override fun providesReferenceLayerRepository(): ReferenceLayerRepository { - return Mockito.mock() - } - - override fun providesScheduler(): Scheduler { - return Mockito.mock() - } - - override fun providesSettingsProvider(): SettingsProvider { - return InMemSettingsProvider() - } - - override fun providesWebPageService(): WebPageService { - return Mockito.mock() - } - - override fun providesLocationTracker(application: Application): LocationTracker { - return locationTracker - } - }) - .build() + overrideDependencies(map) } @Test - fun whenLocationNotSetShouldDisplayPleaseWaitMessage() { - val scenario = launcherRule.launchForResult(GeoPointMapActivity::class.java) - mapFragment.ready() - - scenario.onActivity { activity: GeoPointMapActivity? -> - assertThat( - activity!!.getString(string.please_wait_long), - equalTo(getLocationStatus(activity)) - ) + fun `displays please wait message when location not set`() { + launcherRule.launchInContainer { + GeoPointMapFragment(null, false, false, false) } + + EspressoAssertions.assertVisible(withText(string.please_wait_long)) } @Test - fun whenLocationSetShouldDisplayStatusMessage() { - val scenario = launcherRule.launchForResult(GeoPointMapActivity::class.java) - mapFragment.ready() - locationTracker.currentLocation = Location(1.0, 2.0, 3.0, 4.0f) - - scenario.onActivity { activity: GeoPointMapActivity? -> - assertThat("Accuracy: 4 m", equalTo(getLocationStatus(activity!!))) + fun `displays status message when location set`() { + launcherRule.launchInContainer { + GeoPointMapFragment(null, false, false, false) } + + locationTracker.currentLocation = Location(1.0, 2.0, 3.0, 4.0f) + EspressoAssertions.assertVisible(withText("Accuracy: 4 m")) } @Test fun `returns point from first location fix`() { - val scenario = launcherRule.launchForResult(GeoPointMapActivity::class.java) - mapFragment.ready() + val scenario = launcherRule.launchInContainer { + GeoPointMapFragment(null, false, false, false) + } + + val resultListener = FragmentResultRecorder() + scenario.setFragmentResultListener(GeoPointMapFragment.REQUEST_GEOPOINT, resultListener) val firstLocation = Location(1.0, 2.0, 3.0, 4.0f) locationTracker.currentLocation = firstLocation locationTracker.currentLocation = Location(5.0, 6.0, 7.0, 8.0f) EspressoInteractions.clickOn(withContentDescription(string.save)) - assertThat(scenario.result.resultCode, equalTo(Activity.RESULT_OK)) - val resultData = scenario.result.resultData + val result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPointMapFragment.REQUEST_GEOPOINT)) assertThat( - getReturnedSingleValue(resultData), - equalTo(GeoUtils.formatLocationResultString(firstLocation)) + result.second.getString(GeoPointMapFragment.RESULT_GEOPOINT), + equalTo("1.0 2.0 3.0 4.0") ) } @Test fun `shows marker at first location fix`() { - launcherRule.launchForResult(GeoPointMapActivity::class.java) - mapFragment.ready() + launcherRule.launchInContainer { + GeoPointMapFragment(null, false, false, false) + } val firstLocation = Location(1.0, 2.0, 3.0, 4.0f) locationTracker.currentLocation = firstLocation locationTracker.currentLocation = Location(5.0, 6.0, 7.0, 8.0f) - val markers = mapFragment.getMarkers() + val markers = map.getMarkers() .filter { it.iconDescription != CurrentLocationDelegate.ICON_DESCRIPTION } assertThat(markers.size, equalTo(1)) assertThat(markers[0].point, equalTo(firstLocation.toMapPoint())) @@ -153,8 +123,9 @@ class GeoPointMapActivityTest { @Test fun `clicking add marker moves marker to the current location`() { - launcherRule.launchForResult(GeoPointMapActivity::class.java) - mapFragment.ready() + launcherRule.launchInContainer { + GeoPointMapFragment(null, false, false, false) + } locationTracker.currentLocation = Location(1.0, 2.0, 3.0, 4.0f) val secondLocation = Location(5.0, 6.0, 7.0, 8.0f) @@ -162,129 +133,138 @@ class GeoPointMapActivityTest { EspressoInteractions.clickOn(withContentDescription(string.record_geopoint)) - val markers = mapFragment.getMarkers() + val markers = map.getMarkers() .filter { it.iconDescription != CurrentLocationDelegate.ICON_DESCRIPTION } assertThat(markers.size, equalTo(1)) assertThat(markers[0].point, equalTo(secondLocation.toMapPoint())) } @Test - fun whenLocationExtraIncluded_showsMarker() { - val intent = Intent( - ApplicationProvider.getApplicationContext(), - GeoPointMapActivity::class.java - ) - intent.putExtra(GeoPointMapActivity.EXTRA_LOCATION, MapPoint(1.0, 2.0)) - launcherRule.launch(intent) - mapFragment.ready() + fun `shows marker when input point provided`() { + val inputPoint = MapPoint(1.0, 2.0) + launcherRule.launchInContainer { + GeoPointMapFragment(inputPoint, false, false, false) + } - val markers = mapFragment.getMarkers() + val markers = map.getMarkers() assertThat(markers.size, equalTo(1)) assertThat(markers[0].point.latitude, equalTo(1.0)) - assertThat( - markers[0].point.longitude, - equalTo(2.0) - ) - } - - @Test - fun mapFragmentRetainMockAccuracy_isFalse() { - launcherRule.launch(GeoPointMapActivity::class.java) - mapFragment.ready() - - assertThat(mapFragment.isRetainMockAccuracy(), equalTo(false)) + assertThat(markers[0].point.longitude, equalTo(2.0)) } @Test - fun passingRetainMockAccuracyExtra_updatesLocationTracker() { - val intent = Intent( - ApplicationProvider.getApplicationContext(), - GeoPointMapActivity::class.java - ) - intent.putExtra(EXTRA_RETAIN_MOCK_ACCURACY, true) - launcherRule.launch(intent) - mapFragment.ready() + fun `passing retain mock accuracy extra updates location tracker`() { + launcherRule.launchInContainer { + GeoPointMapFragment(null, false, false, true) + } assertThat(locationTracker.retainMockAccuracy, equalTo(true)) - intent.putExtra(EXTRA_RETAIN_MOCK_ACCURACY, false) - launcherRule.launch(intent) - mapFragment.ready() + launcherRule.launchInContainer { + GeoPointMapFragment(null, false, false, false) + } assertThat(locationTracker.retainMockAccuracy, equalTo(false)) } @Test - fun recreatingTheActivityWithTheLayersDialogDisplayedDoesNotCrashTheApp() { - val scenario = launcherRule.launch(GeoPointMapActivity::class.java) - mapFragment.ready() + fun `recreating the fragment with the layers dialog displayed does not crash the app`() { + val scenario = + launcherRule.launchInContainer { GeoPointMapFragment(null, false, false, false) } - Espresso.onView(ViewMatchers.withId(org.odk.collect.geo.R.id.layer_menu)).perform( - ViewActions.click() - ) + onView(withId(R.id.layer_menu)).perform(click()) scenario.recreate() } @Test fun `clicking zoom zooms to the current location`() { - launcherRule.launch(GeoPointMapActivity::class.java) - mapFragment.ready() + launcherRule.launchInContainer { + GeoPointMapFragment(null, false, false, false) + } locationTracker.currentLocation = Location(5.0, 5.0) locationTracker.currentLocation = Location(6.0, 6.0) EspressoInteractions.clickOn(withContentDescription(string.show_my_location)) - assertThat(mapFragment, hasZoomedToCurrentLocation(MapPoint(6.0, 6.0))) + assertThat(map, hasZoomedToCurrentLocation(MapPoint(6.0, 6.0))) } @Test fun `shows current location`() { - launcherRule.launch(GeoPointMapActivity::class.java) - mapFragment.ready() + launcherRule.launchInContainer { + GeoPointMapFragment(null, false, false, false) + } val firstLocation = Location(2.0, 2.0, accuracy = 5.2f) locationTracker.currentLocation = firstLocation - assertThat(mapFragment, showsCurrentLocation(firstLocation.toMapPoint())) + assertThat(map, showsCurrentLocation(firstLocation.toMapPoint())) val secondLocation = Location(3.0, 2.0, accuracy = 2.1f) locationTracker.currentLocation = secondLocation - assertThat(mapFragment, showsCurrentLocation(secondLocation.toMapPoint())) - assertThat(mapFragment, not(showsCurrentLocation(firstLocation.toMapPoint()))) + assertThat(map, showsCurrentLocation(secondLocation.toMapPoint())) + assertThat(map, not(showsCurrentLocation(firstLocation.toMapPoint()))) } @Test fun `clicking clear clears marker`() { - launcherRule.launch(GeoPointMapActivity::class.java) - mapFragment.ready() + launcherRule.launchInContainer { + GeoPointMapFragment(null, false, false, false) + } val location = Location(2.0, 2.0, accuracy = 5.2f) locationTracker.currentLocation = location EspressoInteractions.clickOn(withContentDescription(string.clear)) - assertThat(mapFragment.getMarkers().size, equalTo(1)) - assertThat(mapFragment, showsCurrentLocation(location.toMapPoint())) + assertThat(map.getMarkers().size, equalTo(1)) + assertThat(map, showsCurrentLocation(location.toMapPoint())) } @Test - fun `clearing an existing location enables the place marker button`() { - val intent = Intent( - ApplicationProvider.getApplicationContext(), - GeoPointMapActivity::class.java - ) - intent.putExtra(GeoPointMapActivity.EXTRA_LOCATION, MapPoint(1.0, 2.0)) - launcherRule.launch(intent) - mapFragment.ready() + fun `enables place marker button when existing location cleared`() { + val inputPoint = MapPoint(1.0, 2.0) + launcherRule.launchInContainer { + GeoPointMapFragment(inputPoint, false, false, false) + } EspressoAssertions.assertDisabled(withContentDescription(string.record_geopoint)) EspressoInteractions.clickOn(withContentDescription(string.clear)) EspressoAssertions.assertEnabled(withContentDescription(string.record_geopoint)) } - private fun getLocationStatus(activity: Activity): String { - return activity - .findViewById(org.odk.collect.geo.R.id.status_section) - .findViewById(org.odk.collect.geo.R.id.location_status) - .text.toString() + private fun overrideDependencies(mapFragment: MapFragment) { + val application = ApplicationProvider.getApplicationContext() + application.geoDependencyComponent = DaggerGeoDependencyComponent.builder() + .application(application) + .geoDependencyModule(object : GeoDependencyModule() { + override fun providesMapFragmentFactory(): MapFragmentFactory { + return object : MapFragmentFactory { + override fun createMapFragment(): MapFragment { + return mapFragment + } + } + } + + override fun providesLocationTracker(application: Application): LocationTracker { + return locationTracker + } + + override fun providesReferenceLayerRepository(): ReferenceLayerRepository { + return mock() + } + + override fun providesScheduler(): Scheduler { + return mock() + } + + override fun providesSettingsProvider(): SettingsProvider { + return InMemSettingsProvider() + } + + override fun providesWebPageService(): WebPageService { + return mock() + } + }) + .build() } } diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index ca2f82ca866..3fd9f7674f2 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -217,10 +217,6 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm return hasCenter } - fun isRetainMockAccuracy(): Boolean { - return retainMockAccuracy - } - fun clickOnFeature(index: Int) { featureClickListener!!.onFeature(featureIds[index]) }