diff --git a/androidshared/src/main/res/color/color_primary_low_emphasis.xml b/androidshared/src/main/res/color/color_primary_low_emphasis.xml deleted file mode 100644 index 6d09caa48c1..00000000000 --- a/androidshared/src/main/res/color/color_primary_low_emphasis.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt index 1e7711f0355..b0f5e0a34c0 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt @@ -3,13 +3,13 @@ package org.odk.collect.android.support import android.os.Handler import android.os.Looper import androidx.fragment.app.Fragment -import org.odk.collect.maps.traces.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint import org.odk.collect.maps.circles.CircleDescription -import org.odk.collect.maps.traces.PolygonDescription import org.odk.collect.maps.markers.MarkerDescription import org.odk.collect.maps.markers.MarkerIconDescription +import org.odk.collect.maps.traces.LineDescription +import org.odk.collect.maps.traces.PolygonDescription class FakeClickableMapFragment : Fragment(), MapFragment { @@ -46,11 +46,6 @@ class FakeClickableMapFragment : Fragment(), MapFragment { animate: Boolean ) {} - override fun addMarker(markerDescription: MarkerDescription): Int { - val id = idCounter++ - return id - } - override fun updateMarker( featureId: Int, markerDescription: MarkerDescription @@ -60,7 +55,7 @@ class FakeClickableMapFragment : Fragment(), MapFragment { override fun addMarkers(markers: List): List { return markers.map { - addMarker(it) + idCounter++ } } @@ -97,6 +92,7 @@ class FakeClickableMapFragment : Fragment(), MapFragment { } override fun clearFeatures() {} + override fun clearFeatures(ids: List) {} override fun setClickListener(listener: MapFragment.PointListener?) {} @@ -108,16 +104,6 @@ class FakeClickableMapFragment : Fragment(), MapFragment { override fun setDragEndListener(listener: MapFragment.FeatureListener?) {} - override fun setGpsLocationEnabled(enabled: Boolean) {} - - override fun getGpsLocation(): MapPoint? { - return null - } - - override fun setGpsLocationListener(listener: MapFragment.PointListener?) {} - - override fun setRetainMockAccuracy(retainMockAccuracy: Boolean) {} - override fun hasCenter(): Boolean { return false } diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGoogleMapsDependencyModule.kt b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGoogleMapsDependencyModule.kt index ff322a6bbb3..7a3d32cdd12 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGoogleMapsDependencyModule.kt +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGoogleMapsDependencyModule.kt @@ -12,10 +12,6 @@ class CollectGoogleMapsDependencyModule( return appDependencyComponent.referenceLayerRepository() } - override fun providesLocationClient(): LocationClient { - return appDependencyComponent.locationClient() - } - override fun providesSettingsProvider(): SettingsProvider { return appDependencyComponent.settingsProvider() } diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectOsmDroidDependencyModule.kt b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectOsmDroidDependencyModule.kt index 77630904a55..2887a64f1f5 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectOsmDroidDependencyModule.kt +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectOsmDroidDependencyModule.kt @@ -1,7 +1,6 @@ package org.odk.collect.android.injection.config import org.odk.collect.android.geo.MapConfiguratorProvider -import org.odk.collect.location.LocationClient import org.odk.collect.maps.MapConfigurator import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.osmdroid.OsmDroidDependencyModule @@ -15,10 +14,6 @@ class CollectOsmDroidDependencyModule( return appDependencyComponent.referenceLayerRepository() } - override fun providesLocationClient(): LocationClient { - return appDependencyComponent.locationClient() - } - override fun providesMapConfigurator(): MapConfigurator { return MapConfiguratorProvider.getConfigurator( appDependencyComponent.settingsProvider().getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE) diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt index 78d3d9c5bec..9c58b194c5f 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt @@ -44,10 +44,6 @@ class NoOpMapFragment : Fragment(), MapFragment { ) { } - override fun addMarker(markerDescription: MarkerDescription): Int { - TODO("Not yet implemented") - } - override fun updateMarker( featureId: Int, markerDescription: MarkerDescription @@ -106,6 +102,9 @@ class NoOpMapFragment : Fragment(), MapFragment { override fun clearFeatures() { } + override fun clearFeatures(ids: List) { + } + override fun setClickListener(listener: MapFragment.PointListener?) { } @@ -118,19 +117,6 @@ class NoOpMapFragment : Fragment(), MapFragment { override fun setDragEndListener(listener: MapFragment.FeatureListener?) { } - override fun setGpsLocationEnabled(enabled: Boolean) { - } - - override fun getGpsLocation(): MapPoint? { - TODO("Not yet implemented") - } - - override fun setGpsLocationListener(listener: MapFragment.PointListener?) { - } - - override fun setRetainMockAccuracy(retainMockAccuracy: Boolean) { - } - override fun hasCenter(): Boolean { return false } diff --git a/geo/src/main/java/org/odk/collect/geo/GeoUtils.kt b/geo/src/main/java/org/odk/collect/geo/GeoUtils.kt index 4dca90e210d..e53801e7661 100644 --- a/geo/src/main/java/org/odk/collect/geo/GeoUtils.kt +++ b/geo/src/main/java/org/odk/collect/geo/GeoUtils.kt @@ -2,8 +2,14 @@ package org.odk.collect.geo import android.content.Context import android.location.Location +import androidx.fragment.app.Fragment +import androidx.lifecycle.asLiveData import org.javarosa.core.model.data.GeoPointData +import org.odk.collect.location.tracker.LocationTracker +import org.odk.collect.location.tracker.bindToLifecycle +import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.circles.CurrentLocationDelegate import org.odk.collect.shared.strings.StringUtils.removeEnd import org.odk.collect.strings.R import java.text.DecimalFormat @@ -93,7 +99,27 @@ object GeoUtils { return MapPoint(this.getPart(0), this.getPart(1), this.getPart(2), this.getPart(3)) } + @JvmStatic fun org.odk.collect.location.Location.toMapPoint(): MapPoint { return MapPoint(this.latitude, this.longitude, this.altitude, this.accuracy.toDouble()) } + + @JvmStatic + @JvmOverloads + fun MapFragment.showCurrentLocation( + locationTracker: LocationTracker, + currentLocationDelegate: CurrentLocationDelegate, + retainMockAccuracy: Boolean = false, + afterUpdate: (MapPoint) -> Unit = {} + ) { + val lifecycleOwner = this as Fragment + locationTracker.bindToLifecycle(lifecycleOwner, retainMockAccuracy) + locationTracker.getLocation().asLiveData().observe(lifecycleOwner) { + if (it != null) { + val mapPoint = it.toMapPoint() + currentLocationDelegate.update(this, mapPoint) + afterUpdate(mapPoint) + } + } + } } 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 index 686a8b427d6..62c81b6614f 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java @@ -19,9 +19,14 @@ 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; @@ -38,9 +43,12 @@ 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; @@ -49,8 +57,11 @@ 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; /** @@ -96,6 +107,9 @@ public class GeoPointMapActivity extends LocalizedActivity { @Inject WebPageService webPageService; + @Inject + LocationTracker locationTracker; + private MapFragment map; private int featureId = -1; // will be a positive featureId once map is ready @@ -110,7 +124,6 @@ public class GeoPointMapActivity extends LocalizedActivity { private ImageButton clearButton; private boolean captureLocation; - private boolean foundFirstLocation; /** * True if a tap on the clear button removed an existing marker and @@ -118,18 +131,28 @@ public class GeoPointMapActivity extends LocalizedActivity { */ private boolean setClear; - /** True if the current point came from the intent. */ + /** + * True if the current point came from the intent. + */ private boolean pointFromIntent; - /** True if the intent requested for the point to be read-only. */ + /** + * 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. */ + /** + * 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. */ + /** + * 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); @@ -162,7 +185,8 @@ public void onCreate(Bundle savedInstanceState) { mapFragment.init(this::initMap, this::finish); } - @Override protected void onSaveInstanceState(Bundle state) { + @Override + protected void onSaveInstanceState(Bundle state) { super.onSaveInstanceState(state); if (map == null) { // initMap() is called asynchronously, so map can be null if the activity @@ -179,7 +203,6 @@ public void onCreate(Bundle savedInstanceState) { // Flags state.putBoolean(IS_DRAGGED_KEY, isDragged); state.putBoolean(CAPTURE_LOCATION_KEY, captureLocation); - state.putBoolean(FOUND_FIRST_LOCATION_KEY, foundFirstLocation); state.putBoolean(SET_CLEAR_KEY, setClear); state.putBoolean(POINT_FROM_INTENT_KEY, pointFromIntent); state.putBoolean(INTENT_READ_ONLY_KEY, intentReadOnly); @@ -222,8 +245,10 @@ public void initMap(MapFragment newMapFragment) { placeMarkerButton.setEnabled(false); placeMarkerButton.setOnClickListener(v -> { - MapPoint mapPoint = map.getGpsLocation(); - if (mapPoint != null) { + Location currentLocation = getCurrentLocation(locationTracker); + + if (currentLocation != null) { + MapPoint mapPoint = toMapPoint(currentLocation); placeMarker(mapPoint); zoomToMarker(true); } @@ -231,7 +256,7 @@ public void initMap(MapFragment newMapFragment) { // Focuses on marked location zoomButton.setEnabled(false); - zoomButton.setOnClickListener(v -> map.zoomToCurrentLocation(map.getGpsLocation())); + zoomButton.setOnClickListener(v -> currentLocationDelegate.zoomToCurrentLocation(map)); // Menu Layer Toggle findViewById(R.id.layer_menu).setOnClickListener(v -> { @@ -242,11 +267,6 @@ public void initMap(MapFragment newMapFragment) { clearButton.setEnabled(false); clearButton.setOnClickListener(v -> { clear(); - if (map.getGpsLocation() != null) { - placeMarkerButton.setEnabled(true); - // locationStatus.setVisibility(View.VISIBLE); - } - // placeMarkerButton.setEnabled(true); locationStatus.setVisibility(View.VISIBLE); pointFromIntent = false; }); @@ -279,24 +299,24 @@ public void initMap(MapFragment newMapFragment) { pointFromIntent = true; locationStatus.setVisibility(View.GONE); zoomButton.setEnabled(true); - foundFirstLocation = true; zoomToMarker(false); } } - map.setRetainMockAccuracy(intent.getBooleanExtra(EXTRA_RETAIN_MOCK_ACCURACY, false)); - map.setGpsLocationListener(this::onLocationChanged); - map.setGpsLocationEnabled(true); - 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); - foundFirstLocation = state.getBoolean(FOUND_FIRST_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); @@ -307,14 +327,11 @@ protected void restoreFromInstanceState(Bundle state) { MapPoint point = state.getParcelable(POINT_KEY); if (point != null) { placeMarker(point); - } else { - clear(); } // 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); - foundFirstLocation = state.getBoolean(FOUND_FIRST_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); @@ -333,8 +350,6 @@ public void onLocationChanged(MapPoint point) { placeMarkerButton.setEnabled(true); } - this.location = point; - if (point != null) { enableZoomButton(true); @@ -343,11 +358,6 @@ public void onLocationChanged(MapPoint point) { placeMarkerButton.setEnabled(true); } - if (!foundFirstLocation) { - map.zoomToCurrentLocation(map.getGpsLocation()); - foundFirstLocation = true; - } - locationStatus.setAccuracy(new LocationAccuracy.Improving((float) point.accuracy)); } } @@ -384,9 +394,10 @@ public void zoomToMarker(boolean animate) { } private void clear() { - map.clearFeatures(); + map.clearFeatures(Arrays.asList(featureId)); featureId = -1; clearButton.setEnabled(false); + placeMarkerButton.setEnabled(true); isPointLocked = false; isDragged = false; @@ -394,10 +405,18 @@ private void clear() { setClear = true; } - /** Places the marker and enables the button to remove it. */ + /** + * Places the marker and enables the button to remove it. + */ private void placeMarker(@NonNull MapPoint point) { - map.clearFeatures(); - featureId = map.addMarker(new MarkerDescription(point, intentDraggable && !intentReadOnly && !isPointLocked, MapFragment.IconAnchor.CENTER, new MarkerIconDescription.DrawableResource(org.odk.collect.icons.R.drawable.ic_map_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("#52C268")); + featureId = addMarker(map, new MarkerDescription(point, intentDraggable && !intentReadOnly && !isPointLocked, MapFragment.IconAnchor.BOTTOM, iconDescription)); if (!intentReadOnly) { clearButton.setEnabled(true); } diff --git a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt index eb0566f359a..872be3669fc 100644 --- a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt @@ -25,16 +25,19 @@ import org.odk.collect.androidshared.ui.multiclicksafe.setMultiClickSafeOnClickL import org.odk.collect.androidshared.utils.sanitizeToColorInt import org.odk.collect.async.Scheduler import org.odk.collect.geo.GeoDependencyComponentProvider +import org.odk.collect.geo.GeoUtils.showCurrentLocation import org.odk.collect.geo.databinding.SelectionMapLayoutBinding -import org.odk.collect.maps.traces.LineDescription +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.traces.PolygonDescription +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.maps.traces.LineDescription +import org.odk.collect.maps.traces.PolygonDescription import org.odk.collect.material.BottomSheetBehavior import org.odk.collect.material.MaterialProgressDialogFragment import org.odk.collect.permissions.PermissionsChecker @@ -72,6 +75,9 @@ class SelectionMapFragment( @Inject lateinit var webPageService: WebPageService + @Inject + lateinit var locationTracker: LocationTracker + private val selectedItemViewModel by viewModels() private lateinit var map: MapFragment @@ -91,6 +97,7 @@ class SelectionMapFragment( private var featureCount: Int = 0 private var previousState: Bundle? = null + private val currentLocationDelegate = CurrentLocationDelegate() override fun onCreate(savedInstanceState: Bundle?) { childFragmentManager.fragmentFactory = FragmentFactoryBuilder() @@ -190,7 +197,7 @@ class SelectionMapFragment( map = newMapFragment binding.zoomToLocation.setMultiClickSafeOnClickListener { - map.zoomToCurrentLocation(map.getGpsLocation()) + currentLocationDelegate.zoomToCurrentLocation(map) } binding.zoomToBounds.setMultiClickSafeOnClickListener { @@ -217,8 +224,6 @@ class SelectionMapFragment( binding.newItem.visibility = View.GONE } - map.setGpsLocationEnabled(true) - map.setFeatureClickListener(::onFeatureSelected) map.setClickListener { onClick() } @@ -228,6 +233,8 @@ class SelectionMapFragment( updateCounts(binding) } } + + map.showCurrentLocation(locationTracker, currentLocationDelegate) } private fun updateCounts(binding: SelectionMapLayoutBinding) { @@ -367,11 +374,6 @@ class SelectionMapFragment( } else if (!map.hasCenter()) { if (zoomToFitItems && points.isNotEmpty()) { map.zoomToBoundingBox(points, 0.8, false) - } else { - map.setGpsLocationListener { point -> - map.zoomToCurrentLocation(point) - map.setGpsLocationListener(null) - } } } } @@ -391,7 +393,7 @@ class SelectionMapFragment( */ private fun updateFeatures(items: List) { points.clear() - map.clearFeatures() + map.clearFeatures(featureIdsByItemId.values.toList()) itemsByFeatureId.clear() val singlePoints = items.filterIsInstance() diff --git a/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java b/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java deleted file mode 100644 index 39fe1763819..00000000000 --- a/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.odk.collect.geo.geopoint; - -import static android.app.Activity.RESULT_OK; -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.odk.collect.geo.Constants.EXTRA_RETAIN_MOCK_ACCURACY; -import static org.robolectric.Shadows.shadowOf; - -import android.app.Activity; -import android.app.Application; -import android.content.Intent; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.test.core.app.ActivityScenario; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.odk.collect.androidtest.ActivityScenarioLauncherRule; -import org.odk.collect.async.Scheduler; -import org.odk.collect.externalapp.ExternalAppUtils; -import org.odk.collect.geo.DaggerGeoDependencyComponent; -import org.odk.collect.geo.GeoDependencyModule; -import org.odk.collect.geo.R; -import org.odk.collect.geo.support.FakeMapFragment; -import org.odk.collect.geo.support.RobolectricApplication; -import org.odk.collect.maps.MapFragmentFactory; -import org.odk.collect.maps.MapPoint; -import org.odk.collect.maps.layers.ReferenceLayerRepository; -import org.odk.collect.settings.InMemSettingsProvider; -import org.odk.collect.settings.SettingsProvider; -import org.odk.collect.webpage.WebPageService; -import org.robolectric.shadows.ShadowApplication; - -import java.util.List; - -@RunWith(AndroidJUnit4.class) -public class GeoPointMapActivityTest { - - private final FakeMapFragment mapFragment = new FakeMapFragment(); - - @Rule - public ActivityScenarioLauncherRule launcherRule = new ActivityScenarioLauncherRule(); - - @Before - public void setUp() { - ShadowApplication shadowApplication = shadowOf(ApplicationProvider.getApplicationContext()); - shadowApplication.grantPermissions("android.permission.ACCESS_FINE_LOCATION"); - shadowApplication.grantPermissions("android.permission.ACCESS_COARSE_LOCATION"); - - RobolectricApplication application = ApplicationProvider.getApplicationContext(); - application.geoDependencyComponent = DaggerGeoDependencyComponent.builder() - .application(application) - .geoDependencyModule(new GeoDependencyModule() { - @NonNull - @Override - public MapFragmentFactory providesMapFragmentFactory() { - return () -> mapFragment; - } - - @NonNull - @Override - public ReferenceLayerRepository providesReferenceLayerRepository() { - return mock(); - } - - @NonNull - @Override - public Scheduler providesScheduler() { - return mock(); - } - - @NonNull - @Override - public SettingsProvider providesSettingsProvider() { - return new InMemSettingsProvider(); - } - - @NonNull - @Override - public WebPageService providesWebPageService() { - return mock(); - } - }) - .build(); - } - - @Test - public void whenLocationNotSetShouldDisplayPleaseWaitMessage() { - ActivityScenario scenario = launcherRule.launchForResult(GeoPointMapActivity.class); - mapFragment.ready(); - - scenario.onActivity(activity -> assertEquals(activity.getString(org.odk.collect.strings.R.string.please_wait_long), getLocationStatus(activity))); - } - - @Test - public void whenLocationSetShouldDisplayStatusMessage() { - ActivityScenario scenario = launcherRule.launchForResult(GeoPointMapActivity.class); - mapFragment.ready(); - mapFragment.setLocationProvider("GPS"); - mapFragment.setLocation(new MapPoint(1, 2, 3, 4f)); - - scenario.onActivity(activity -> assertEquals("Accuracy: 4 m", getLocationStatus(activity))); - } - - @Test - public void shouldReturnPointFromLastLocationFix() { - ActivityScenario scenario = launcherRule.launchForResult(GeoPointMapActivity.class); - mapFragment.ready(); - mapFragment.setLocationProvider("GPS"); - - // First location - mapFragment.setLocation(new MapPoint(1, 2, 3, 4f)); - - // Second location - mapFragment.setLocation(new MapPoint(5, 6, 7, 8f)); - - // When the user clicks the "Save" button, the fix location should be returned. - scenario.onActivity(activity -> activity.findViewById(R.id.accept_location).performClick()); - - assertThat(scenario.getResult().getResultCode(), is(RESULT_OK)); - scenario.onActivity(activity -> { - Intent resultData = scenario.getResult().getResultData(); - assertThat(ExternalAppUtils.getReturnedSingleValue(resultData), is(activity.formatResult(new MapPoint(5, 6, 7, 8)))); - }); - } - - @Test - public void whenLocationExtraIncluded_showsMarker() { - Intent intent = new Intent(ApplicationProvider.getApplicationContext(), GeoPointMapActivity.class); - intent.putExtra(GeoPointMapActivity.EXTRA_LOCATION, new MapPoint(1.0, 2.0)); - launcherRule.launch(intent); - mapFragment.ready(); - - List markers = mapFragment.getMarkers(); - assertThat(markers.size(), equalTo(1)); - assertThat(markers.get(0).latitude, equalTo(1.0)); - assertThat(markers.get(0).longitude, equalTo(2.0)); - } - - @Test - public void mapFragmentRetainMockAccuracy_isFalse() { - launcherRule.launch(GeoPointMapActivity.class); - mapFragment.ready(); - - assertThat(mapFragment.isRetainMockAccuracy(), is(false)); - } - - @Test - public void passingRetainMockAccuracyExtra_updatesMapFragmentState() { - Intent intent = new Intent(ApplicationProvider.getApplicationContext(), GeoPointMapActivity.class); - intent.putExtra(EXTRA_RETAIN_MOCK_ACCURACY, true); - launcherRule.launch(intent); - mapFragment.ready(); - - assertThat(mapFragment.isRetainMockAccuracy(), is(true)); - - intent.putExtra(EXTRA_RETAIN_MOCK_ACCURACY, false); - launcherRule.launch(intent); - mapFragment.ready(); - - assertThat(mapFragment.isRetainMockAccuracy(), is(false)); - } - - @Test - public void recreatingTheActivityWithTheLayersDialogDisplayedDoesNotCrashTheApp() { - ActivityScenario scenario = launcherRule.launch(GeoPointMapActivity.class); - mapFragment.ready(); - - onView(withId(R.id.layer_menu)).perform(click()); - - scenario.recreate(); - } - - private String getLocationStatus(Activity activity) { - return activity - .findViewById(R.id.status_section) - .findViewById(R.id.location_status) - .getText().toString(); - } -} diff --git a/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.kt b/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.kt new file mode 100644 index 00000000000..955f2cc0bb7 --- /dev/null +++ b/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.kt @@ -0,0 +1,295 @@ +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.matcher.ViewMatchers.withContentDescription +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.not +import org.junit.Assert +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.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.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.support.FakeLocationTracker +import org.odk.collect.geo.support.FakeMapFragment +import org.odk.collect.geo.support.MapFragmentAssertions.hasZoomedToCurrentLocation +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.MapFragmentFactory +import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.circles.CurrentLocationDelegate +import org.odk.collect.maps.layers.ReferenceLayerRepository +import org.odk.collect.settings.InMemSettingsProvider +import org.odk.collect.settings.SettingsProvider +import org.odk.collect.strings.R +import org.odk.collect.strings.R.string +import org.odk.collect.testshared.EspressoAssertions +import org.odk.collect.testshared.EspressoInteractions +import org.odk.collect.webpage.WebPageService +import org.robolectric.Shadows + +@RunWith(AndroidJUnit4::class) +class GeoPointMapActivityTest { + + private val mapFragment = FakeMapFragment() + + private val locationTracker = FakeLocationTracker() + + @get:Rule + val launcherRule: ActivityScenarioLauncherRule = ActivityScenarioLauncherRule() + + @Before + fun setUp() { + val shadowApplication = + Shadows.shadowOf(ApplicationProvider.getApplicationContext()) + 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() + } + + @Test + fun whenLocationNotSetShouldDisplayPleaseWaitMessage() { + val scenario = launcherRule.launchForResult(GeoPointMapActivity::class.java) + mapFragment.ready() + + scenario.onActivity { activity: GeoPointMapActivity? -> + Assert.assertEquals( + activity!!.getString( + R.string.please_wait_long + ), getLocationStatus(activity) + ) + } + } + + @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? -> + Assert.assertEquals( + "Accuracy: 4 m", + getLocationStatus(activity!!) + ) + } + } + + @Test + fun `returns point from first location fix`() { + val scenario = launcherRule.launchForResult(GeoPointMapActivity::class.java) + mapFragment.ready() + + 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 + assertThat( + getReturnedSingleValue(resultData), + equalTo(GeoUtils.formatLocationResultString(firstLocation)) + ) + } + + @Test + fun `shows marker at first location fix`() { + launcherRule.launchForResult(GeoPointMapActivity::class.java) + mapFragment.ready() + + 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() + .filter { it.iconDescription != CurrentLocationDelegate.ICON_DESCRIPTION } + assertThat(markers.size, equalTo(1)) + assertThat(markers[0].point, equalTo(firstLocation.toMapPoint())) + } + + @Test + fun `clicking add marker moves marker to the current location`() { + launcherRule.launchForResult(GeoPointMapActivity::class.java) + mapFragment.ready() + + locationTracker.currentLocation = Location(1.0, 2.0, 3.0, 4.0f) + val secondLocation = Location(5.0, 6.0, 7.0, 8.0f) + locationTracker.currentLocation = secondLocation + + EspressoInteractions.clickOn(withContentDescription(string.record_geopoint)) + + val markers = mapFragment.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() + + val markers = mapFragment.getMarkersPoints() + assertThat(markers.size, equalTo(1)) + assertThat(markers[0].latitude, equalTo(1.0)) + assertThat( + markers[0].longitude, + equalTo(2.0) + ) + } + + @Test + fun mapFragmentRetainMockAccuracy_isFalse() { + launcherRule.launch(GeoPointMapActivity::class.java) + mapFragment.ready() + + assertThat(mapFragment.isRetainMockAccuracy(), equalTo(false)) + } + + @Test + fun passingRetainMockAccuracyExtra_updatesLocationTracker() { + val intent = Intent( + ApplicationProvider.getApplicationContext(), + GeoPointMapActivity::class.java + ) + intent.putExtra(EXTRA_RETAIN_MOCK_ACCURACY, true) + launcherRule.launch(intent) + mapFragment.ready() + + assertThat(locationTracker.retainMockAccuracy, equalTo(true)) + + intent.putExtra(EXTRA_RETAIN_MOCK_ACCURACY, false) + launcherRule.launch(intent) + mapFragment.ready() + + assertThat(locationTracker.retainMockAccuracy, equalTo(false)) + } + + @Test + fun recreatingTheActivityWithTheLayersDialogDisplayedDoesNotCrashTheApp() { + val scenario = launcherRule.launch(GeoPointMapActivity::class.java) + mapFragment.ready() + + Espresso.onView(ViewMatchers.withId(org.odk.collect.geo.R.id.layer_menu)).perform( + ViewActions.click() + ) + + scenario.recreate() + } + + @Test + fun `clicking zoom zooms to the current location`() { + launcherRule.launch(GeoPointMapActivity::class.java) + mapFragment.ready() + + 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))) + } + + @Test + fun `shows current location`() { + launcherRule.launch(GeoPointMapActivity::class.java) + mapFragment.ready() + + val firstLocation = Location(2.0, 2.0, accuracy = 5.2f) + locationTracker.currentLocation = firstLocation + assertThat(mapFragment, 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()))) + } + + @Test + fun `clicking clear clears marker`() { + launcherRule.launch(GeoPointMapActivity::class.java) + mapFragment.ready() + + 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())) + } + + @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() + + 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() + } +} diff --git a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt index af7586549f0..2e99b0290ee 100644 --- a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt @@ -40,6 +40,8 @@ import org.odk.collect.geo.geopoly.GeoPolyFragment.OutputMode import org.odk.collect.geo.support.AccuracyStatusViewMatcher.Companion.hasAccuracy import org.odk.collect.geo.support.FakeLocationTracker import org.odk.collect.geo.support.FakeMapFragment +import org.odk.collect.geo.support.MapFragmentAssertions.hasZoomedToCurrentLocation +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 @@ -54,9 +56,9 @@ import org.odk.collect.strings.R.string import org.odk.collect.testshared.EspressoAssertions import org.odk.collect.testshared.EspressoAssertions.assertNotVisible import org.odk.collect.testshared.EspressoAssertions.assertVisible +import org.odk.collect.testshared.EspressoInteractions import org.odk.collect.testshared.FakeScheduler import org.odk.collect.testshared.FragmentResultRecorder -import org.odk.collect.testshared.EspressoInteractions import org.odk.collect.webpage.WebPageService import org.robolectric.Shadows @@ -95,8 +97,7 @@ class GeoPolyFragmentTest { } locationTracker.currentLocation = Location(2.0, 2.0) - assertThat(mapFragment.getCenter(), equalTo(MapPoint(2.0, 2.0))) - assertThat(mapFragment.getZoom(), not(equalTo(0.0))) + assertThat(mapFragment, hasZoomedToCurrentLocation(MapPoint(2.0, 2.0))) } @Test @@ -105,27 +106,14 @@ class GeoPolyFragmentTest { GeoPolyFragment({ OnBackPressedDispatcher() }) } - locationTracker.currentLocation = Location(2.0, 2.0, accuracy = 5.2f) - assertThat( - mapFragment.getMarkers(), - equalTo(listOf(locationTracker.currentLocation!!.toMapPoint())) - ) - mapFragment.getCircles().let { - assertThat(it.size, equalTo(1)) - assertThat(it[0].center, equalTo(locationTracker.currentLocation!!.toMapPoint())) - assertThat(it[0].radius, equalTo(5.2f)) - } + val firstLocation = Location(2.0, 2.0, accuracy = 5.2f) + locationTracker.currentLocation = firstLocation + assertThat(mapFragment, showsCurrentLocation(firstLocation.toMapPoint())) - locationTracker.currentLocation = Location(3.0, 2.0, accuracy = 2.1f) - assertThat( - mapFragment.getMarkers(), - equalTo(listOf(locationTracker.currentLocation!!.toMapPoint())) - ) - mapFragment.getCircles().let { - assertThat(it.size, equalTo(1)) - assertThat(it[0].center, equalTo(locationTracker.currentLocation!!.toMapPoint())) - assertThat(it[0].radius, equalTo(2.1f)) - } + val secondLocation = Location(3.0, 2.0, accuracy = 2.1f) + locationTracker.currentLocation = secondLocation + assertThat(mapFragment, showsCurrentLocation(secondLocation.toMapPoint())) + assertThat(mapFragment, not(showsCurrentLocation(firstLocation.toMapPoint()))) } @Test @@ -977,8 +965,10 @@ class GeoPolyFragmentTest { } locationTracker.currentLocation = Location(5.0, 5.0) + locationTracker.currentLocation = Location(6.0, 6.0) + EspressoInteractions.clickOn(withContentDescription(string.show_my_location)) - assertThat(mapFragment.getCenter(), equalTo(MapPoint(5.0, 5.0))) + assertThat(mapFragment, hasZoomedToCurrentLocation(MapPoint(6.0, 6.0))) } @Test diff --git a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt index 82942d656ae..4697b181375 100644 --- a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt @@ -1,5 +1,6 @@ package org.odk.collect.geo.selection +import android.app.Application import android.content.Context import android.graphics.Color import android.os.Bundle @@ -39,21 +40,31 @@ import org.odk.collect.async.Scheduler import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.geo.DaggerGeoDependencyComponent import org.odk.collect.geo.GeoDependencyModule +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.Fixtures +import org.odk.collect.geo.support.MapFragmentAssertions.hasZoomedToCurrentLocation +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.addMarker 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.material.BottomSheetBehavior import org.odk.collect.material.MaterialProgressDialogFragment import org.odk.collect.permissions.PermissionsChecker import org.odk.collect.settings.InMemSettingsProvider import org.odk.collect.settings.SettingsProvider +import org.odk.collect.strings.R.string +import org.odk.collect.testshared.EspressoInteractions import org.odk.collect.testshared.RobolectricHelpers.getFragmentByClass import org.odk.collect.webpage.WebPageService @@ -72,6 +83,7 @@ class SelectionMapFragmentTest { } private val onBackPressedDispatcher = OnBackPressedDispatcher() + private val locationTracker = FakeLocationTracker() @get:Rule val launcherRule = FragmentScenarioLauncherRule( @@ -120,6 +132,10 @@ class SelectionMapFragmentTest { override fun providesWebPageService(): WebPageService { return mock() } + + override fun providesLocationTracker(application: Application): LocationTracker { + return locationTracker + } }).build() BottomSheetBehavior.DRAGGING_ENABLED = false @@ -155,10 +171,34 @@ class SelectionMapFragmentTest { launcherRule.launchInContainer(SelectionMapFragment::class.java) map.ready() - assertThat(map.getMarkers(), equalTo(itemsLiveData.value?.map { (it as MappableSelectItem.MappableSelectPoint).toMapPoint() })) + assertThat(map.getMarkersPoints(), equalTo(itemsLiveData.value?.map { (it as MappableSelectItem.MappableSelectPoint).toMapPoint() })) + + itemsLiveData.value = emptyList() + assertThat(map.getMarkersPoints(), equalTo(emptyList())) + } + + @Test + fun `doesn't clear other markers when items update`() { + val items: List = listOf( + Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)) + ) + val itemsLiveData = MutableLiveData(items) + whenever(data.getMappableItems()).thenReturn(itemsLiveData) + + launcherRule.launchInContainer(SelectionMapFragment::class.java) + map.ready() + map.addMarker( + MarkerDescription( + MapPoint(0.0, 0.0), + false, + iconDescription = MarkerIconDescription.DrawableResource(R.drawable.ic_info) + ) + ) + + assertThat(map.getMarkersPoints().size, equalTo(2)) itemsLiveData.value = emptyList() - assertThat(map.getMarkers(), equalTo(emptyList())) + assertThat(map.getMarkersPoints().size, equalTo(1)) } @Test @@ -324,7 +364,7 @@ class SelectionMapFragmentTest { assertThat(map.hasCenter(), equalTo(false)) - map.setLocation(MapPoint(1.0, 2.0)) + locationTracker.currentLocation = Location(1.0, 2.0) assertThat(map.getCenter(), equalTo(MapPoint(1.0, 2.0))) } @@ -337,7 +377,7 @@ class SelectionMapFragmentTest { assertThat(map.hasCenter(), equalTo(false)) - map.setLocation(MapPoint(1.0, 2.0)) + locationTracker.currentLocation = Location(1.0, 2.0) assertThat(map.getCenter(), equalTo(MapPoint(1.0, 2.0))) assertThat(map.getZoom(), equalTo(MapFragment.POINT_ZOOM.toDouble())) } @@ -352,7 +392,7 @@ class SelectionMapFragmentTest { assertThat(map.hasCenter(), equalTo(false)) - map.setLocation(MapPoint(1.0, 2.0)) + locationTracker.currentLocation = Location(1.0, 2.0) assertThat(map.getCenter(), equalTo(MapPoint(1.0, 2.0))) assertThat(map.getZoom(), equalTo(10.0)) } @@ -366,10 +406,10 @@ class SelectionMapFragmentTest { assertThat(map.hasCenter(), equalTo(false)) - map.setLocation(MapPoint(1.0, 2.0)) + locationTracker.currentLocation = Location(1.0, 2.0) assertThat(map.getCenter(), equalTo(MapPoint(1.0, 2.0))) - map.setLocation(MapPoint(3.0, 4.0)) + locationTracker.currentLocation = Location(3.0, 4.0) assertThat(map.getCenter(), equalTo(MapPoint(1.0, 2.0))) } @@ -383,7 +423,7 @@ class SelectionMapFragmentTest { launcherRule.launchInContainer(SelectionMapFragment::class.java) map.ready() - map.setLocation(MapPoint(67.0, 48.0)) + locationTracker.currentLocation = Location(67.0, 48.0) itemsLiveData.value = listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(52.0, 0.0))) @@ -397,7 +437,7 @@ class SelectionMapFragmentTest { launcherRule.launchInContainer(SelectionMapFragment::class.java) map.ready() - map.setLocation(MapPoint(40.181389, 44.514444)) + locationTracker.currentLocation = Location(40.181389, 44.514444) onView(withId(R.id.zoom_to_location)).perform(click()) assertThat(map.getCenter(), equalTo(MapPoint(40.181389, 44.514444))) @@ -742,14 +782,15 @@ class SelectionMapFragmentTest { fun `does not move when location changes when centered on already selected item`() { val items = listOf( Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)), - Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0), selected = true) + Fixtures.actionMappableSelectPoint() + .copy(id = 1, point = MapPoint(41.0, 0.0), selected = true) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) launcherRule.launchInContainer(SelectionMapFragment::class.java) map.ready() - map.setLocation(MapPoint(1.0, 2.0)) + locationTracker.currentLocation = Location(1.0, 2.0) assertThat(map.getCenter(), equalTo(items[1].toMapPoint())) } @@ -917,6 +958,33 @@ class SelectionMapFragmentTest { assertThat(actualResult, equalTo(null)) } + @Test + fun `clicking zoom zooms to the current location`() { + launcherRule.launchInContainer(SelectionMapFragment::class.java) + map.ready() + + locationTracker.currentLocation = Location(5.0, 5.0) + locationTracker.currentLocation = Location(6.0, 6.0) + + EspressoInteractions.clickOn(withContentDescription(string.show_my_location)) + assertThat(map, hasZoomedToCurrentLocation(MapPoint(6.0, 6.0))) + } + + @Test + fun `shows current location`() { + launcherRule.launchInContainer(SelectionMapFragment::class.java) + map.ready() + + val firstLocation = Location(2.0, 2.0, accuracy = 5.2f) + locationTracker.currentLocation = firstLocation + assertThat(map, showsCurrentLocation(firstLocation.toMapPoint())) + + val secondLocation = Location(3.0, 2.0, accuracy = 2.1f) + locationTracker.currentLocation = secondLocation + assertThat(map, showsCurrentLocation(secondLocation.toMapPoint())) + assertThat(map, not(showsCurrentLocation(firstLocation.toMapPoint()))) + } + private fun MappableSelectItem.MappableSelectPoint.toMapPoint(): MapPoint { return MapPoint(this.point.latitude, this.point.longitude) } diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeLocationTracker.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeLocationTracker.kt index 7d444d851c0..b2cbf0bf698 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeLocationTracker.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeLocationTracker.kt @@ -9,8 +9,10 @@ class FakeLocationTracker : LocationTracker { var currentLocation: Location? = null set(value) { - _currentLocation.value = value - field = value + if (isStarted) { + _currentLocation.value = value + field = value + } } var retainMockAccuracy: Boolean = false 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 64653df53ed..a07f4aac725 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 @@ -14,9 +14,9 @@ import org.odk.collect.maps.traces.PolygonDescription import kotlin.random.Random class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragment { + private var gpsLocationEnabled: Boolean = false private var clickListener: PointListener? = null private var gpsLocationListener: PointListener? = null - private var locationProvider: String? = null private var retainMockAccuracy = false private var center: MapPoint? = null private var zoom = 0.0 @@ -25,8 +25,7 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm private var gpsLocation: MapPoint? = null private var featureClickListener: FeatureListener? = null private var dragListener: FeatureListener? = null - private val markers = mutableMapOf() - private val markerIcons = mutableMapOf() + private val markers = mutableMapOf() private val polyLines = mutableMapOf() private val polygons = mutableMapOf() @@ -103,22 +102,11 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm } } - override fun addMarker(markerDescription: MarkerDescription): Int { - val featureId = generateFeatureId() - - markers[featureId] = markerDescription.point - markerIcons[featureId] = markerDescription.iconDescription - - featureIds.add(featureId) - return featureId - } - override fun updateMarker( featureId: Int, markerDescription: MarkerDescription ) { - markers[featureId] = markerDescription.point - markerIcons[featureId] = markerDescription.iconDescription + markers[featureId] = markerDescription } override fun addMarkers(markers: List): List { @@ -127,12 +115,20 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm } } + private fun addMarker(markerDescription: MarkerDescription): Int { + val featureId = generateFeatureId() + + markers[featureId] = markerDescription + featureIds.add(featureId) + return featureId + } + override fun setMarkerIcon(featureId: Int, markerIconDescription: MarkerIconDescription) { - markerIcons[featureId] = markerIconDescription + markers[featureId] = markers[featureId]!!.copy(iconDescription = markerIconDescription) } override fun getMarkerPoint(featureId: Int): MapPoint? { - return markers[featureId] + return markers[featureId]?.point } override fun addPolyLine(lineDescription: LineDescription): Int { @@ -187,11 +183,18 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm override fun clearFeatures() { markers.clear() - markerIcons.clear() polyLines.clear() polygons.clear() } + override fun clearFeatures(ids: List) { + listOf(markers, polyLines, polygons).forEach { + ids.forEach { id -> + it.remove(id) + } + } + } + override fun setClickListener(listener: PointListener?) { this.clickListener = listener } @@ -209,38 +212,10 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm dragListener = listener } - override fun setGpsLocationEnabled(enabled: Boolean) {} - override fun getGpsLocation(): MapPoint? { - return gpsLocation - } - - override fun setGpsLocationListener(listener: PointListener?) { - gpsLocationListener = listener - - gpsLocation?.let { - listener?.onPoint(it) - } - } - - override fun setRetainMockAccuracy(retainMockAccuracy: Boolean) { - this.retainMockAccuracy = retainMockAccuracy - } - override fun hasCenter(): Boolean { return hasCenter } - fun setLocation(mapPoint: MapPoint?) { - gpsLocation = mapPoint - if (gpsLocationListener != null) { - gpsLocationListener!!.onPoint(mapPoint!!) - } - } - - fun setLocationProvider(locationProvider: String?) { - this.locationProvider = locationProvider - } - fun isRetainMockAccuracy(): Boolean { return retainMockAccuracy } @@ -253,12 +228,16 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm featureClickListener!!.onFeature(featureId) } - fun getMarkers(): List { + fun getMarkers(): List { return markers.values.toList() } + fun getMarkersPoints(): List { + return markers.values.map { it.point } + } + fun getMarkerIcons(): List { - return markerIcons.values.toList() + return markers.values.map { it.iconDescription } } fun getZoomBoundingBox(): Pair, Double>? { @@ -276,7 +255,7 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm fun getFeatureId(points: List): Int { return if (points.size == 1) { markers.entries.find { - it.value == points[0] + it.value.point == points[0] }!!.key } else { polyLines.entries.find { diff --git a/geo/src/test/java/org/odk/collect/geo/support/MapFragmentAssertions.kt b/geo/src/test/java/org/odk/collect/geo/support/MapFragmentAssertions.kt new file mode 100644 index 00000000000..d2df752ac4e --- /dev/null +++ b/geo/src/test/java/org/odk/collect/geo/support/MapFragmentAssertions.kt @@ -0,0 +1,54 @@ +package org.odk.collect.geo.support + +import org.hamcrest.Description +import org.hamcrest.TypeSafeMatcher +import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.circles.CurrentLocationDelegate + +object MapFragmentAssertions { + fun hasZoomedToCurrentLocation(location: MapPoint): TypeSafeMatcher { + return object : TypeSafeMatcher() { + override fun matchesSafely(mapFragment: FakeMapFragment): Boolean { + return mapFragment.getCenter() == location && mapFragment.getZoom() != 0.0 + } + + override fun describeTo(description: Description) { + description.appendText("is zoomed to $location") + } + + override fun describeMismatchSafely( + mapFragment: FakeMapFragment, + mismatchDescription: Description + ) { + mismatchDescription.appendText("was ${mapFragment.getCenter()}") + } + } + } + + fun showsCurrentLocation(location: MapPoint): TypeSafeMatcher { + return object : TypeSafeMatcher() { + override fun matchesSafely(mapFragment: FakeMapFragment): Boolean { + val hasLocationMarker = mapFragment.getMarkers().any { + it.point == location && it.iconDescription == CurrentLocationDelegate.ICON_DESCRIPTION + } + + val hasAccuracyCircle = mapFragment.getCircles().any { + it.center == location && it.radius == location.accuracy.toFloat() + } + + return hasLocationMarker && hasAccuracyCircle + } + + override fun describeTo(description: Description) { + description.appendText("shows current location as $location") + } + + override fun describeMismatchSafely( + mapFragment: FakeMapFragment, + mismatchDescription: Description + ) { + mismatchDescription.appendText("was ${mapFragment.getCenter()}") + } + } + } +} \ No newline at end of file diff --git a/google-maps/build.gradle.kts b/google-maps/build.gradle.kts index d4a6ec7df11..6c8239005a7 100644 --- a/google-maps/build.gradle.kts +++ b/google-maps/build.gradle.kts @@ -46,7 +46,6 @@ dependencies { implementation(project(":androidshared")) implementation(project(":maps")) implementation(project(":settings")) - implementation(project(":location")) implementation(project(":strings")) implementation(project(":icons")) diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/DaggerSetup.kt b/google-maps/src/main/java/org/odk/collect/googlemaps/DaggerSetup.kt index 94cdd92dae8..6820b6de8bc 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/DaggerSetup.kt +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/DaggerSetup.kt @@ -3,7 +3,6 @@ package org.odk.collect.googlemaps import dagger.Component import dagger.Module import dagger.Provides -import org.odk.collect.location.LocationClient import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.settings.SettingsProvider import javax.inject.Singleton @@ -26,11 +25,6 @@ open class GoogleMapsDependencyModule { throw UnsupportedOperationException("This should be overridden by dependent application") } - @Provides - open fun providesLocationClient(): LocationClient { - throw UnsupportedOperationException("This should be overridden by dependent application") - } - @Provides open fun providesSettingsProvider(): SettingsProvider { throw UnsupportedOperationException("This should be overridden by dependent application") diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java index c56006f2c53..b7309ea54e1 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java @@ -31,15 +31,12 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; -import com.google.android.gms.location.LocationListener; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.BitmapDescriptor; import com.google.android.gms.maps.model.CameraPosition; -import com.google.android.gms.maps.model.Circle; -import com.google.android.gms.maps.model.CircleOptions; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.MapStyleOptions; @@ -53,12 +50,10 @@ import com.google.android.gms.maps.model.TileOverlayOptions; import org.jetbrains.annotations.NotNull; -import org.odk.collect.androidshared.system.ContextExt; import org.odk.collect.androidshared.ui.ToastUtils; import org.odk.collect.googlemaps.GoogleMapConfigurator.GoogleMapTypeOption; import org.odk.collect.googlemaps.circles.CircleFeature; import org.odk.collect.googlemaps.scaleview.MapScaleView; -import org.odk.collect.location.LocationClient; import org.odk.collect.maps.MapConfigurator; import org.odk.collect.maps.MapFragment; import org.odk.collect.maps.MapPoint; @@ -89,7 +84,6 @@ import timber.log.Timber; public class GoogleMapFragment extends MapViewModelMapFragment implements - LocationListener, LocationClient.LocationClientListener, GoogleMap.OnMapClickListener, GoogleMap.OnMapLongClickListener, GoogleMap.OnMarkerClickListener, GoogleMap.OnMarkerDragListener, GoogleMap.OnPolylineClickListener, GoogleMap.OnPolygonClickListener { @@ -100,9 +94,6 @@ public class GoogleMapFragment extends MapViewModelMapFragment implements @Inject ReferenceLayerRepository referenceLayerRepository; - @Inject - LocationClient locationClient; - @Inject SettingsProvider settingsProvider; @@ -110,18 +101,11 @@ public class GoogleMapFragment extends MapViewModelMapFragment implements private MapScaleView scaleView; private ReadyListener readyListener; private ErrorListener errorListener; - private Marker locationCrosshairs; - private Circle accuracyCircle; - private final List gpsLocationReadyListeners = new ArrayList<>(); private PointListener clickListener; private PointListener longPressListener; - private PointListener gpsLocationListener; private FeatureListener featureClickListener; private FeatureListener dragEndListener; - private boolean clientWantsLocationUpdates; - private MapPoint lastLocationFix; - private int nextFeatureId = 1; private final Map features = new HashMap<>(); private int mapType; @@ -250,16 +234,6 @@ public void onZoomToBox(@NonNull Zoom.Box zoom) { component.inject(this); } - @Override public void onResume() { - super.onResume(); - enableLocationUpdates(clientWantsLocationUpdates); - } - - @Override public void onPause() { - super.onPause(); - enableLocationUpdates(false); - } - @Override public void onDestroy() { BitmapDescriptorCache.clearCache(); super.onDestroy(); @@ -280,21 +254,16 @@ public void onZoomToBox(@NonNull Zoom.Box zoom) { return map.getCameraPosition().zoom; } - @Override public int addMarker(MarkerDescription markerDescription) { - int featureId = nextFeatureId++; - return addMarker(featureId, markerDescription); - } - - private int addMarker(int featureId, MarkerDescription markerDescription) { + private void addMarker(int featureId, MarkerDescription markerDescription) { features.put(featureId, new MarkerFeature(getActivity(), markerDescription, map)); - return featureId; } @Override public List addMarkers(List markers) { List featureIds = new ArrayList<>(); for (MarkerDescription markerDescription : markers) { - int featureId = addMarker(markerDescription); + int featureId = nextFeatureId++; + addMarker(featureId, markerDescription); featureIds.add(featureId); } @@ -373,6 +342,13 @@ public void updatePolygon(int featureId, @NotNull PolygonDescription polygonDesc nextFeatureId = 1; } + @Override + public void clearFeatures(@NotNull List<@NotNull Integer> ids) { + for (Integer id : ids) { + features.remove(id).dispose(); + } + } + @Override public void setClickListener(@Nullable PointListener listener) { clickListener = listener; } @@ -389,42 +365,6 @@ public void updatePolygon(int featureId, @NotNull PolygonDescription polygonDesc dragEndListener = listener; } - @Override public void setGpsLocationListener(@Nullable PointListener listener) { - gpsLocationListener = listener; - } - - @Override - public void setRetainMockAccuracy(boolean retainMockAccuracy) { - locationClient.setRetainMockAccuracy(retainMockAccuracy); - } - - @Override public void setGpsLocationEnabled(boolean enable) { - if (enable != clientWantsLocationUpdates) { - clientWantsLocationUpdates = enable; - enableLocationUpdates(clientWantsLocationUpdates); - } - } - - @Override public void onLocationChanged(Location location) { - Timber.i("onLocationChanged: location = %s", location); - lastLocationFix = fromLocation(location); - for (ReadyListener listener : gpsLocationReadyListeners) { - listener.onReady(this); - } - gpsLocationReadyListeners.clear(); - if (gpsLocationListener != null) { - gpsLocationListener.onPoint(lastLocationFix); - } - - if (getActivity() != null) { - updateLocationIndicator(toLatLng(lastLocationFix), location.getAccuracy()); - } - } - - @Override public @Nullable MapPoint getGpsLocation() { - return lastLocationFix; - } - @Override public void onMapClick(LatLng latLng) { if (clickListener != null) { clickListener.onPoint(fromLatLng(latLng)); @@ -438,11 +378,6 @@ public void setRetainMockAccuracy(boolean retainMockAccuracy) { } @Override public boolean onMarkerClick(Marker marker) { - // Avoid calling listeners if location crosshair is clicked on. - if (marker == locationCrosshairs) { - return true; - } - if (featureClickListener != null) { // FormMapActivity featureClickListener.onFeature(findFeature(marker)); } else { // GeoWidget @@ -486,18 +421,6 @@ public void setRetainMockAccuracy(boolean retainMockAccuracy) { } } - @Override public void onClientStart() { - lastLocationFix = fromLocation(locationClient.getLastLocation()); - Timber.i("Requesting location updates (to %s)", this); - locationClient.requestLocationUpdates(this); - } - - @Override public void onClientStartFailure() { - } - - @Override public void onClientStop() { - } - private static @NonNull MapPoint fromLatLng(@NonNull LatLng latLng) { return new MapPoint(latLng.latitude, latLng.longitude); } @@ -581,44 +504,6 @@ private void moveOrAnimateCamera(CameraUpdate movement, boolean animate) { } } - private void enableLocationUpdates(boolean enable) { - if (enable) { - Timber.i("Starting LocationClient %s (for MapFragment %s)", locationClient, this); - locationClient.start(this); - } else { - Timber.i("Stopping LocationClient %s (for MapFragment %s)", locationClient, this); - locationClient.stop(); - } - } - - private void updateLocationIndicator(LatLng loc, double radius) { - if (map == null) { - return; - } - if (locationCrosshairs == null) { - locationCrosshairs = map.addMarker(new MarkerOptions() - .position(loc) - .icon(getBitmapDescriptor(getContext(), new MarkerIconDescription.DrawableResource(org.odk.collect.maps.R.drawable.ic_crosshairs))) - .anchor(0.5f, 0.5f) // center the crosshairs on the position - ); - } - if (accuracyCircle == null) { - int stroke = ContextExt.getThemeAttributeValue(requireContext(), androidx.appcompat.R.attr.colorPrimary); - int fill = getResources().getColor(org.odk.collect.androidshared.R.color.color_primary_low_emphasis); - accuracyCircle = map.addCircle(new CircleOptions() - .center(loc) - .radius(radius) - .strokeWidth(1) - .strokeColor(stroke) - .fillColor(fill) - ); - } - - locationCrosshairs.setPosition(loc); - accuracyCircle.setCenter(loc); - accuracyCircle.setRadius(radius); - } - /** Finds the feature to which the given marker belongs. */ private int findFeature(Marker marker) { for (int featureId : features.keySet()) { @@ -659,6 +544,14 @@ private static Marker createMarker(Context context, MarkerDescription markerDesc if (map == null || context == null) { // during Robolectric tests, map will be null return null; } + + int index; + if (markerDescription.getIconDescription().getBackground()) { + index = 1; + } else { + index = 2; + } + // A Marker's position is a LatLng with just latitude and longitude // fields. We need to store the point's altitude and standard // deviation values somewhere, so they go in the marker's snippet. @@ -668,6 +561,7 @@ private static Marker createMarker(Context context, MarkerDescription markerDesc .draggable(markerDescription.isDraggable()) .icon(getBitmapDescriptor(context, markerDescription.getIconDescription())) .anchor(getIconAnchorValueX(markerDescription.getIconAnchor()), getIconAnchorValueY(markerDescription.getIconAnchor())) // center the icon on the position + .zIndex(index) ); } diff --git a/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt b/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt index 364fad892e1..bd6da8e2cb0 100644 --- a/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt +++ b/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt @@ -79,9 +79,7 @@ class LocationTrackerService : Service(), LocationClient.LocationClientListener if (intent?.hasExtra(EXTRA_UPDATE_INTERVAL) == true) { val interval = intent.getLongExtra(EXTRA_UPDATE_INTERVAL, -1) - locationClient.setUpdateInterval( - interval - ) + locationClient.setUpdateInterval(interval) } locationClient.start(this) @@ -90,7 +88,7 @@ class LocationTrackerService : Service(), LocationClient.LocationClientListener override fun onDestroy() { locationClient.stop() - application.getState().clear(LOCATION_KEY) + application.getState().setFlow(LOCATION_KEY, null) } override fun onClientStart() { diff --git a/location/src/main/java/org/odk/collect/location/tracker/LocationTracker.kt b/location/src/main/java/org/odk/collect/location/tracker/LocationTracker.kt index 267dbd4877e..5e8fdcb61bb 100644 --- a/location/src/main/java/org/odk/collect/location/tracker/LocationTracker.kt +++ b/location/src/main/java/org/odk/collect/location/tracker/LocationTracker.kt @@ -1,5 +1,7 @@ package org.odk.collect.location.tracker +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import kotlinx.coroutines.flow.StateFlow import org.odk.collect.location.Location @@ -31,3 +33,15 @@ interface LocationTracker { fun LocationTracker.getCurrentLocation(): Location? { return this.getLocation().value } + +fun LocationTracker.bindToLifecycle(lifecycleOwner: LifecycleOwner, retainMockAccuracy: Boolean = false) { + lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + start(retainMockAccuracy) + } + + override fun onPause(owner: LifecycleOwner) { + stop() + } + }) +} diff --git a/location/src/test/java/org/odk/collect/location/tracker/LocationTrackerTest.kt b/location/src/test/java/org/odk/collect/location/tracker/LocationTrackerTest.kt index 8f117845794..d1db2b669df 100644 --- a/location/src/test/java/org/odk/collect/location/tracker/LocationTrackerTest.kt +++ b/location/src/test/java/org/odk/collect/location/tracker/LocationTrackerTest.kt @@ -51,4 +51,25 @@ abstract class LocationTrackerTest { runBackground() assertThat(locationTracker.getCurrentLocation(), equalTo(null)) } + + @Test + fun canBeRestarted() { + val location = locationTracker.getLocation() + + locationTracker.start() + runBackground() + + setDeviceLocation(Location(1.0, 1.0)) + runBackground() + + locationTracker.stop() + runBackground() + + locationTracker.start() + runBackground() + + setDeviceLocation(Location(2.0, 2.0)) + runBackground() + assertThat(location.value, equalTo(Location(2.0, 2.0))) + } } diff --git a/mapbox/build.gradle.kts b/mapbox/build.gradle.kts index 0a2e58b80bd..bf1f505702d 100644 --- a/mapbox/build.gradle.kts +++ b/mapbox/build.gradle.kts @@ -34,7 +34,6 @@ dependencies { implementation(project(":androidshared")) implementation(project(":icons")) - implementation(project(":location")) implementation(project(":maps")) implementation(project(":settings")) implementation(project(":shared")) diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt index 62ba6a09fde..fea8c9a3939 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt @@ -1,18 +1,15 @@ package org.odk.collect.mapbox import android.graphics.Color -import android.location.Location import android.os.Bundle import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.viewModelFactory import androidx.startup.AppInitializer -import com.google.android.gms.location.LocationListener import com.mapbox.android.gestures.MoveGestureDetector import com.mapbox.android.gestures.StandardScaleGestureDetector import com.mapbox.geojson.Point @@ -32,7 +29,6 @@ import com.mapbox.maps.extension.style.sources.generated.RasterSource import com.mapbox.maps.extension.style.sources.generated.VectorSource import com.mapbox.maps.extension.style.sources.getSource import com.mapbox.maps.loader.MapboxMapsInitializer -import com.mapbox.maps.plugin.LocationPuck2D import com.mapbox.maps.plugin.animation.MapAnimationOptions.Companion.mapAnimationOptions import com.mapbox.maps.plugin.animation.flyTo import com.mapbox.maps.plugin.annotation.annotations @@ -52,13 +48,9 @@ import com.mapbox.maps.plugin.gestures.addOnMapClickListener import com.mapbox.maps.plugin.gestures.addOnMapLongClickListener import com.mapbox.maps.plugin.gestures.addOnMoveListener import com.mapbox.maps.plugin.gestures.addOnScaleListener -import com.mapbox.maps.plugin.locationcomponent.location import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.odk.collect.androidshared.utils.ScreenUtils -import org.odk.collect.location.LocationClient -import org.odk.collect.location.LocationClient.LocationClientListener -import org.odk.collect.maps.traces.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapFragment.ErrorListener import org.odk.collect.maps.MapFragment.FeatureListener @@ -67,7 +59,6 @@ import org.odk.collect.maps.MapFragment.ReadyListener import org.odk.collect.maps.MapPoint import org.odk.collect.maps.MapViewModel import org.odk.collect.maps.MapViewModelMapFragment -import org.odk.collect.maps.traces.PolygonDescription import org.odk.collect.maps.Zoom import org.odk.collect.maps.ZoomObserver import org.odk.collect.maps.circles.CircleDescription @@ -77,6 +68,8 @@ import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.maps.markers.MarkerDescription import org.odk.collect.maps.markers.MarkerIconCreator import org.odk.collect.maps.markers.MarkerIconDescription +import org.odk.collect.maps.traces.LineDescription +import org.odk.collect.maps.traces.PolygonDescription import org.odk.collect.settings.SettingsProvider import org.odk.collect.shared.injection.ObjectProviderHost import timber.log.Timber @@ -86,9 +79,7 @@ import java.io.IOException class MapboxMapFragment : MapViewModelMapFragment(), OnMapClickListener, - OnMapLongClickListener, - LocationListener, - LocationClientListener { + OnMapLongClickListener { private lateinit var mapView: MapView private lateinit var mapboxMap: MapboxMap @@ -97,23 +88,16 @@ class MapboxMapFragment : private lateinit var polylineAnnotationManager: PolylineAnnotationManager private lateinit var polygonAnnotationManager: PolygonAnnotationManager private var mapReadyListener: ReadyListener? = null - private val gpsLocationReadyListeners = mutableListOf() private var nextFeatureId = 1 private val features = mutableMapOf() - - private var gpsLocationListener: PointListener? = null private var clickListener: PointListener? = null private var longPressListener: PointListener? = null private var featureClickListener: FeatureListener? = null private var featureDragEndListener: FeatureListener? = null - - private var lastLocationProvider: String? = null - private var lastLocationFix: MapPoint? = null private var tileServer: TileHttpServer? = null private var referenceLayerFile: File? = null - private var clientWantsLocationUpdates = false private var topStyleLayerId: String? = null private val _mapViewModel by viewModels { @@ -135,10 +119,6 @@ class MapboxMapFragment : (requireActivity().applicationContext as ObjectProviderHost).getObjectProvider().provide(ReferenceLayerRepository::class.java) } - private val locationClient: LocationClient by lazy { - (requireActivity().applicationContext as ObjectProviderHost).getObjectProvider().provide(LocationClient::class.java) - } - override fun init(readyListener: ReadyListener?, errorListener: ErrorListener?) { mapReadyListener = readyListener @@ -209,7 +189,6 @@ class MapboxMapFragment : .annotations .createPointAnnotationManager() - initLocationComponent() moveOrAnimateCamera(MapFragment.INITIAL_CENTER, false, MapFragment.INITIAL_ZOOM.toDouble()) // If the screen is rotated before the map is ready, this fragment could already be detached, @@ -257,16 +236,6 @@ class MapboxMapFragment : return mapView } - override fun onResume() { - super.onResume() - enableLocationUpdates(clientWantsLocationUpdates) - } - - override fun onPause() { - super.onPause() - enableLocationUpdates(false) - } - override fun onDestroy() { tileServer?.destroy() MarkerIconCreator.clearCache() @@ -298,10 +267,6 @@ class MapboxMapFragment : return _mapViewModel } - override fun addMarker(markerDescription: MarkerDescription): Int { - return addMarkers(listOf(markerDescription)).first() - } - override fun updateMarker( featureId: Int, markerDescription: MarkerDescription @@ -472,6 +437,10 @@ class MapboxMapFragment : nextFeatureId = 1 } + override fun clearFeatures(ids: List) { + ids.forEach { features.remove(it)?.dispose() } + } + override fun setClickListener(listener: PointListener?) { clickListener = listener } @@ -488,25 +457,6 @@ class MapboxMapFragment : featureDragEndListener = listener } - override fun setGpsLocationEnabled(enabled: Boolean) { - if (enabled != clientWantsLocationUpdates) { - clientWantsLocationUpdates = enabled - enableLocationUpdates(clientWantsLocationUpdates) - } - } - - override fun getGpsLocation(): MapPoint? { - return lastLocationFix - } - - override fun setGpsLocationListener(listener: PointListener?) { - gpsLocationListener = listener - } - - override fun setRetainMockAccuracy(retainMockAccuracy: Boolean) { - locationClient.setRetainMockAccuracy(retainMockAccuracy) - } - override fun onMapClick(point: Point): Boolean { clickListener?.onPoint(MapPoint(point.latitude(), point.longitude())) @@ -522,51 +472,6 @@ class MapboxMapFragment : return true } - override fun onLocationChanged(location: Location) { - lastLocationFix = MapPoint( - location.latitude, - location.longitude, - location.altitude, - location.accuracy.toDouble() - ) - lastLocationProvider = location.provider - Timber.i( - "Received location update: %s (%s)", - lastLocationFix, - lastLocationProvider - ) - for (listener in gpsLocationReadyListeners) { - listener.onReady(this) - } - gpsLocationReadyListeners.clear() - gpsLocationListener?.onPoint(lastLocationFix!!) - } - - @SuppressWarnings("MissingPermission") // permission checks for location services are handled in widgets - private fun enableLocationUpdates(enabled: Boolean) { - if (enabled) { - Timber.i("Starting LocationClient %s (for MapFragment %s)", locationClient, this) - locationClient.start(this) - } else { - Timber.i("Stopping LocationClient %s (for MapFragment %s)", locationClient, this) - locationClient.stop() - } - - mapView.location.enabled = enabled - } - - private fun initLocationComponent() { - mapView.location.updateSettings { - this.enabled = true - this.locationPuck = LocationPuck2D( - AppCompatResources.getDrawable( - requireContext(), - org.odk.collect.maps.R.drawable.ic_crosshairs - ) - ) - } - } - private fun moveOrAnimateCamera(point: MapPoint, animate: Boolean, zoom: Double? = getZoom()) { mapboxMap.flyTo( cameraOptions { @@ -681,17 +586,6 @@ class MapboxMapFragment : } } - override fun onClientStart() { - Timber.i("Requesting location updates (to %s)", this) - locationClient.requestLocationUpdates(this) - } - - override fun onClientStartFailure() { - } - - override fun onClientStop() { - } - companion object { const val KEY_STYLE_URL = "STYLE_URL" } diff --git a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt index b0111cc1cad..e1b710f6f06 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt +++ b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt @@ -1,9 +1,9 @@ package org.odk.collect.maps import org.odk.collect.maps.circles.CircleDescription -import org.odk.collect.maps.traces.LineDescription import org.odk.collect.maps.markers.MarkerDescription import org.odk.collect.maps.markers.MarkerIconDescription +import org.odk.collect.maps.traces.LineDescription import org.odk.collect.maps.traces.PolygonDescription /** @@ -79,10 +79,8 @@ interface MapFragment { * the user will be able to drag the marker to change its location. * Returns a positive integer, the featureId for the newly added shape. */ - fun addMarker(markerDescription: MarkerDescription): Int - fun updateMarker(featureId: Int, markerDescription: MarkerDescription) - fun addMarkers(markers: List): List + fun updateMarker(featureId: Int, markerDescription: MarkerDescription) /** Sets the icon for a marker. */ @Deprecated(message = "Use #updateMarker instead") @@ -117,6 +115,7 @@ interface MapFragment { /** Removes all map features from the map. */ fun clearFeatures() + fun clearFeatures(ids: List) /** Sets or clears the callback for a click on the map. */ fun setClickListener(listener: PointListener?) @@ -130,29 +129,6 @@ interface MapFragment { /** Sets or clears the callback for when a drag is completed. */ fun setDragEndListener(listener: FeatureListener?) - /** - * Enables/disables GPS tracking. While enabled, the GPS location is shown - * on the map, the first GPS fix will trigger any pending callbacks set by - * runOnGpsLocationReady(), and every GPS fix will invoke the callback set - * by setGpsLocationListener(). - */ - @Deprecated(message = "Location should be handled outside of MapFragment") - fun setGpsLocationEnabled(enabled: Boolean) - - /** Gets the last GPS location fix, or null if there hasn't been one. */ - @Deprecated(message = "Location should be handled outside of MapFragment") - fun getGpsLocation(): MapPoint? - - /** - * Sets or clears the callback for GPS location updates. This callback - * will only be invoked while GPS is enabled with setGpsLocationEnabled(). - */ - @Deprecated(message = "Location should be handled outside of MapFragment") - fun setGpsLocationListener(listener: PointListener?) - - @Deprecated(message = "Location should be handled outside of MapFragment") - fun setRetainMockAccuracy(retainMockAccuracy: Boolean) - /** * @return true if the [MapFragment] center has already been set (by [MapFragment.zoomToPoint] for instance). */ @@ -184,3 +160,7 @@ interface MapFragment { enum class IconAnchor { CENTER, BOTTOM } } + +fun MapFragment.addMarker(marker: MarkerDescription): Int { + return addMarkers(listOf(marker))[0] +} diff --git a/maps/src/main/java/org/odk/collect/maps/MapFragmentFactory.kt b/maps/src/main/java/org/odk/collect/maps/MapFragmentFactory.kt index ce0c551d953..33b15c7cdc5 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapFragmentFactory.kt +++ b/maps/src/main/java/org/odk/collect/maps/MapFragmentFactory.kt @@ -1,5 +1,5 @@ package org.odk.collect.maps -interface MapFragmentFactory { +fun interface MapFragmentFactory { fun createMapFragment(): MapFragment } diff --git a/maps/src/main/java/org/odk/collect/maps/circles/CurrentLocationDelegate.kt b/maps/src/main/java/org/odk/collect/maps/circles/CurrentLocationDelegate.kt index 89a51baba9e..d90a8b0aa46 100644 --- a/maps/src/main/java/org/odk/collect/maps/circles/CurrentLocationDelegate.kt +++ b/maps/src/main/java/org/odk/collect/maps/circles/CurrentLocationDelegate.kt @@ -1,8 +1,9 @@ package org.odk.collect.maps.circles -import org.odk.collect.maps.MapConsts +import androidx.core.graphics.toColorInt import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.addMarker import org.odk.collect.maps.markers.MarkerDescription import org.odk.collect.maps.markers.MarkerIconDescription @@ -16,14 +17,14 @@ class CurrentLocationDelegate { private var locationMarkerId: Int? = null private var accuracyHaloId: Int? = null - fun update(map: MapFragment, location: MapPoint, follow: Boolean) { + fun update(map: MapFragment, location: MapPoint, follow: Boolean = false) { currentLocation = location val markerDescription = MarkerDescription( location, false, MapFragment.IconAnchor.CENTER, - MarkerIconDescription.DrawableResource(org.odk.collect.maps.R.drawable.ic_crosshairs) + ICON_DESCRIPTION ) locationMarkerId.let { @@ -56,4 +57,12 @@ class CurrentLocationDelegate { fun zoomToCurrentLocation(map: MapFragment) { map.zoomToCurrentLocation(currentLocation) } + + companion object { + val ICON_DESCRIPTION = MarkerIconDescription.DrawableResource( + drawable = org.odk.collect.maps.R.drawable.ic_current_location, + color = "#6393F2".toColorInt(), + background = true + ) + } } \ No newline at end of file diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerDescription.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerDescription.kt index 77d2b3d63a8..fc75ce4d721 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerDescription.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerDescription.kt @@ -6,6 +6,6 @@ import org.odk.collect.maps.MapPoint data class MarkerDescription( val point: MapPoint, val isDraggable: Boolean, - val iconAnchor: MapFragment.IconAnchor, + val iconAnchor: MapFragment.IconAnchor = MapFragment.IconAnchor.CENTER, val iconDescription: MarkerIconDescription ) diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt index aec3bd88f97..64f15169ea7 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt @@ -7,6 +7,7 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Typeface +import android.graphics.drawable.GradientDrawable import android.util.LruCache import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils @@ -93,7 +94,16 @@ object MarkerIconCreator { drawable.mutate() val isBackgroundDark = color?.let { - drawable.setTint(it) + /** + * Tint icon for normal Drawables, but only change solid color for + * GradientDrawables (shapes). + */ + if (drawable is GradientDrawable) { + drawable.setColor(it) + } else { + drawable.setTint(it) + } + ColorUtils.calculateLuminance(color) < 0.5 } ?: true diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt index c55a09696c2..099a198a26b 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt @@ -5,12 +5,19 @@ import org.odk.collect.shared.strings.StringUtils import java.util.Locale sealed interface MarkerIconDescription { + + val background: Boolean + data class DrawableResource @JvmOverloads constructor( val drawable: Int, - private val color: String? = null, - private val symbol: String? = null + private val color: Int? = null, + private val symbol: String? = null, + override val background: Boolean = false ) : MarkerIconDescription { - fun getColor(): Int? = color?.sanitizeToColorInt() + + constructor(drawable: Int, color: String?, symbol: String?) : this(drawable, color?.sanitizeToColorInt(), symbol) + + fun getColor(): Int? = color fun getSymbol(): String? = symbol?.let { if (it.isBlank()) { @@ -21,5 +28,7 @@ sealed interface MarkerIconDescription { } } - data class TracePoint(val lineSize: Float, val color: Int) : MarkerIconDescription + data class TracePoint(val lineSize: Float, val color: Int) : MarkerIconDescription { + override val background = true + } } diff --git a/maps/src/main/res/drawable/ic_crosshairs.xml b/maps/src/main/res/drawable/ic_crosshairs.xml deleted file mode 100644 index da69a92a862..00000000000 --- a/maps/src/main/res/drawable/ic_crosshairs.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/maps/src/main/res/drawable/ic_current_location.xml b/maps/src/main/res/drawable/ic_current_location.xml new file mode 100644 index 00000000000..be0deea42eb --- /dev/null +++ b/maps/src/main/res/drawable/ic_current_location.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/osmdroid/build.gradle.kts b/osmdroid/build.gradle.kts index 43f8cd6015b..da1c296553c 100644 --- a/osmdroid/build.gradle.kts +++ b/osmdroid/build.gradle.kts @@ -36,7 +36,6 @@ dependencies { implementation(project(":androidshared")) implementation(project(":icons")) implementation(project(":maps")) - implementation(project(":location")) implementation(project(":settings")) implementation(project(":strings")) diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/DaggerSetup.kt b/osmdroid/src/main/java/org/odk/collect/osmdroid/DaggerSetup.kt index a1b61e44c42..93bd4000511 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/DaggerSetup.kt +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/DaggerSetup.kt @@ -3,7 +3,6 @@ package org.odk.collect.osmdroid import dagger.Component import dagger.Module import dagger.Provides -import org.odk.collect.location.LocationClient import org.odk.collect.maps.MapConfigurator import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.settings.SettingsProvider @@ -27,11 +26,6 @@ open class OsmDroidDependencyModule { throw UnsupportedOperationException("This should be overridden by dependent application") } - @Provides - open fun providesLocationClient(): LocationClient { - throw UnsupportedOperationException("This should be overridden by dependent application") - } - @Provides open fun providesMapConfigurator(): MapConfigurator { throw UnsupportedOperationException("This should be overridden by dependent application") diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index ac3347b1300..685d929ec24 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -15,9 +15,8 @@ package org.odk.collect.osmdroid; import static androidx.core.graphics.drawable.BitmapDrawableKt.toDrawable; -import static androidx.core.graphics.drawable.DrawableKt.toBitmap; -import static org.odk.collect.maps.traces.TraceDescriptionKt.getMarkersForPoints; import static org.odk.collect.maps.markers.MarkerIconCreator.toBitmap; +import static org.odk.collect.maps.traces.TraceDescriptionKt.getMarkersForPoints; import android.content.BroadcastReceiver; import android.content.Context; @@ -28,7 +27,6 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.drawable.Drawable; -import android.location.Location; import android.os.Bundle; import android.os.Handler; import android.view.LayoutInflater; @@ -37,30 +35,26 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; -import com.google.android.gms.location.LocationListener; - import org.jetbrains.annotations.NotNull; import org.odk.collect.androidshared.system.ContextExt; -import org.odk.collect.location.LocationClient; -import org.odk.collect.maps.circles.CircleDescription; -import org.odk.collect.maps.traces.LineDescription; import org.odk.collect.maps.MapConfigurator; import org.odk.collect.maps.MapFragment; import org.odk.collect.maps.MapPoint; import org.odk.collect.maps.MapViewModel; import org.odk.collect.maps.MapViewModelMapFragment; -import org.odk.collect.maps.traces.PolygonDescription; import org.odk.collect.maps.Zoom; import org.odk.collect.maps.ZoomObserver; +import org.odk.collect.maps.circles.CircleDescription; import org.odk.collect.maps.layers.MapFragmentReferenceLayerUtils; import org.odk.collect.maps.layers.ReferenceLayerRepository; import org.odk.collect.maps.markers.MarkerDescription; import org.odk.collect.maps.markers.MarkerIconCreator; import org.odk.collect.maps.markers.MarkerIconDescription; +import org.odk.collect.maps.traces.LineDescription; +import org.odk.collect.maps.traces.PolygonDescription; import org.odk.collect.settings.SettingsProvider; import org.osmdroid.api.IGeoPoint; import org.osmdroid.events.MapListener; @@ -78,9 +72,6 @@ import org.osmdroid.views.overlay.Polyline; import org.osmdroid.views.overlay.ScaleBarOverlay; import org.osmdroid.views.overlay.TilesOverlay; -import org.osmdroid.views.overlay.mylocation.IMyLocationConsumer; -import org.osmdroid.views.overlay.mylocation.IMyLocationProvider; -import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay; import java.io.File; import java.util.ArrayList; @@ -97,8 +88,7 @@ /** * A MapFragment drawn by OSMDroid. */ -public class OsmDroidMapFragment extends MapViewModelMapFragment implements - LocationListener, LocationClient.LocationClientListener { +public class OsmDroidMapFragment extends MapViewModelMapFragment { // Bundle keys understood by applyConfig(). public static final String KEY_WEB_MAP_SERVICE = "WEB_MAP_SERVICE"; @@ -106,9 +96,6 @@ public class OsmDroidMapFragment extends MapViewModelMapFragment implements @Inject ReferenceLayerRepository referenceLayerRepository; - @Inject - LocationClient locationClient; - @Inject MapConfigurator mapConfigurator; @@ -119,14 +106,10 @@ public class OsmDroidMapFragment extends MapViewModelMapFragment implements private ReadyListener readyListener; private PointListener clickListener; private PointListener longPressListener; - private PointListener gpsLocationListener; private FeatureListener featureClickListener; private FeatureListener dragEndListener; - private MyLocationNewOverlay myLocationOverlay; - private OsmLocationClientWrapper osmLocationClientWrapper; private int nextFeatureId = 1; private final Map features = new HashMap<>(); - private boolean clientWantsLocationUpdates; private IGeoPoint lastMapCenter; private WebMapService webMapService; private File referenceLayerFile; @@ -159,18 +142,6 @@ public T create(@NonNull Class modelClass) { mapViewModel = new ViewModelProvider(this, viewModelFactory).get(MapViewModel.class); } - @Override - public void onResume() { - super.onResume(); - enableLocationUpdates(clientWantsLocationUpdates); - } - - @Override - public void onPause() { - super.onPause(); - enableLocationUpdates(false); - } - @Override public void onDestroy() { clearFeatures(); // prevent a memory leak due to refs held by markers @@ -221,13 +192,6 @@ public boolean onScroll(ScrollEvent event) { addAttributionAndMapEventsOverlays(); loadReferenceOverlay(); addMapLayoutChangeListener(map); - osmLocationClientWrapper = new OsmLocationClientWrapper(locationClient); - myLocationOverlay = new MyLocationNewOverlay(osmLocationClientWrapper, map); - myLocationOverlay.setDrawAccuracyEnabled(true); - Drawable drawable = ContextCompat.getDrawable(requireActivity(), org.odk.collect.maps.R.drawable.ic_crosshairs); - Bitmap crosshairs = toBitmap(drawable, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), null); - myLocationOverlay.setDirectionArrow(crosshairs, crosshairs); - myLocationOverlay.setPersonHotspot(crosshairs.getWidth() / 2.0f, crosshairs.getHeight() / 2.0f); new Handler().postDelayed(() -> { // If the screen is rotated before the map is ready, this fragment @@ -301,25 +265,16 @@ public double getZoom() { return map.getZoomLevel(); } - @Override - public int addMarker(MarkerDescription markerDescription) { - int featureId = nextFeatureId++; - return addMarker(featureId, markerDescription); - } - - private int addMarker(int featureId, MarkerDescription markerDescription) { - features.put(featureId, new MarkerFeature(map, markerDescription)); - return featureId; - } - @Override public List addMarkers(List markers) { List featureIds = new ArrayList<>(); for (MarkerDescription markerDescription : markers) { - int featureId = addMarker(markerDescription); + int featureId = nextFeatureId++; + features.put(featureId, new MarkerFeature(map, markerDescription)); featureIds.add(featureId); } + map.invalidate(); return featureIds; } @@ -404,6 +359,14 @@ public void clearFeatures() { nextFeatureId = 1; } + @Override + public void clearFeatures(@NotNull List<@NotNull Integer> ids) { + for (Integer id : ids) { + features.remove(id).dispose(); + } + + map.invalidate(); + } @Override public void setClickListener(@Nullable PointListener listener) { @@ -425,98 +388,7 @@ public void setDragEndListener(@Nullable FeatureListener listener) { dragEndListener = listener; } - @Override - public void setGpsLocationListener(@Nullable PointListener listener) { - gpsLocationListener = listener; - } - - @Override - public void setRetainMockAccuracy(boolean retainMockAccuracy) { - locationClient.setRetainMockAccuracy(retainMockAccuracy); - } - - @Override - public void setGpsLocationEnabled(boolean enable) { - if (enable != clientWantsLocationUpdates) { - clientWantsLocationUpdates = enable; - enableLocationUpdates(clientWantsLocationUpdates); - } - } - - @Override - public @Nullable - MapPoint getGpsLocation() { - return fromLocation(myLocationOverlay); - } - - @Override - public void onLocationChanged(Location location) { - Timber.i("onLocationChanged: location = %s", location); - if (gpsLocationListener != null) { - MapPoint point = fromLocation(myLocationOverlay); - if (point != null) { - gpsLocationListener.onPoint(point); - } - } - - if (myLocationOverlay != null) { - myLocationOverlay.onLocationChanged(location, osmLocationClientWrapper); - } - } - - @Override - public void onClientStart() { - map.getOverlays().add(myLocationOverlay); - myLocationOverlay.setEnabled(true); - myLocationOverlay.enableMyLocation(); - - Timber.i("Requesting location updates (to %s)", this); - locationClient.requestLocationUpdates(this); - } - - @Override - public void onClientStartFailure() { - } - - @Override - public void onClientStop() { - } - - private void enableLocationUpdates(boolean enable) { - if (enable) { - Timber.i("Starting LocationClient %s (for MapFragment %s)", locationClient, this); - locationClient.start(this); - } else { - Timber.i("Stopping LocationClient %s (for MapFragment %s)", locationClient, this); - locationClient.stop(); - myLocationOverlay.setEnabled(false); - safelyDisableOverlayLocationFollowing(); - } - } - - /** - * - * https://github.com/osmdroid/osmdroid/issues/1783 - * - **/ - private void safelyDisableOverlayLocationFollowing() { - if (map.isAttachedToWindow()) { - myLocationOverlay.disableFollowLocation(); - myLocationOverlay.disableMyLocation(); - } - } - private static @Nullable - MapPoint fromLocation(@NonNull MyLocationNewOverlay overlay) { - GeoPoint geoPoint = overlay.getMyLocation(); - if (geoPoint == null) { - return null; - } - return new MapPoint( - geoPoint.getLatitude(), geoPoint.getLongitude(), - geoPoint.getAltitude(), overlay.getLastFix().getAccuracy() - ); - } private static @NonNull MapPoint fromGeoPoint(@NonNull IGeoPoint geoPoint) { @@ -734,7 +606,8 @@ public MapViewModel getMapViewModel() { @Override public void updateMarker(int featureId, @NotNull MarkerDescription markerDescription) { features.get(featureId).dispose(); - addMarker(featureId, markerDescription); + features.put(featureId, new MarkerFeature(map, markerDescription)); + map.invalidate(); } @Override @@ -1129,39 +1002,6 @@ public void draw(Canvas canvas, MapView map, boolean shadow) { } } - private static class OsmLocationClientWrapper implements IMyLocationProvider { - private LocationClient locationClient; - - OsmLocationClientWrapper(LocationClient locationClient) { - this.locationClient = locationClient; - } - - @Override - public boolean startLocationProvider(IMyLocationConsumer myLocationConsumer) { - // locationClient.start launches async work and we need to be confident that - // getLastKnownLocation is never called before onClientStart so we don't let the OSM - // location overlay start the provider. We also ignore the location consumer passed in - // and instead explicitly forward location updates to the overlay from onLocationChanged - return true; - } - - @Override - public void stopLocationProvider() { - locationClient.stop(); - } - - @Override - public Location getLastKnownLocation() { - return locationClient.getLastLocation(); - } - - @Override - public void destroy() { - locationClient.stop(); - locationClient = null; - } - } - private static class RegisterReceiver implements IRegisterReceiver { private final Context context; diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 325d07e7822..b536a8fac06 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -587,7 +587,7 @@ Are you sure you want to delete the %1$s offline layer? - Record a point + Add marker Accuracy: %1$s ⚠️ Accuracy: %1$s (unacceptable) Sorry, Location providers are disabled!