diff --git a/buildSrc/src/main/java/dependencies/Dependencies.kt b/buildSrc/src/main/java/dependencies/Dependencies.kt index 0d80927d27a..9c27c077b9a 100644 --- a/buildSrc/src/main/java/dependencies/Dependencies.kt +++ b/buildSrc/src/main/java/dependencies/Dependencies.kt @@ -21,7 +21,7 @@ object Dependencies { const val android_material = "com.google.android.material:material:1.9.0" const val android_flexbox = "com.google.android.flexbox:flexbox:3.0.0" const val play_services_maps = "com.google.android.gms:play-services-maps:18.2.0" - const val play_services_location = "com.google.android.gms:play-services-location:21.0.1" + const val play_services_location = "com.google.android.gms:play-services-location:20.0.0" // Check if map screens still work when upgrading and location works as expected https://github.com/getodk/collect/issues/6027, especially after moving to FusedLocationProviderClient. const val play_services_oss_licenses = "com.google.android.gms:play-services-oss-licenses:17.0.1" const val mapbox_android_sdk = "com.mapbox.maps:android:10.16.1" const val osmdroid = "org.osmdroid:osmdroid-android:6.1.17" diff --git a/location/src/main/java/org/odk/collect/location/BaseLocationClient.kt b/location/src/main/java/org/odk/collect/location/BaseLocationClient.kt index 77323ebb37f..792e39ece27 100644 --- a/location/src/main/java/org/odk/collect/location/BaseLocationClient.kt +++ b/location/src/main/java/org/odk/collect/location/BaseLocationClient.kt @@ -12,7 +12,7 @@ import org.odk.collect.location.LocationClient.LocationClientListener * * @param locationManager The LocationManager to retrieve locations from. */ -abstract class BaseLocationClient(protected val locationManager: LocationManager?) : +internal abstract class BaseLocationClient(protected val locationManager: LocationManager?) : LocationClient { private var listener: LocationClientListener? = null @@ -84,7 +84,7 @@ abstract class BaseLocationClient(protected val locationManager: LocationManager listener = locationClientListener } - fun getListener(): LocationClientListener? { + protected fun getListener(): LocationClientListener? { return listener } } diff --git a/location/src/main/java/org/odk/collect/location/GoogleFusedLocationClient.java b/location/src/main/java/org/odk/collect/location/GoogleFusedLocationClient.java new file mode 100644 index 00000000000..d4b64fc11e7 --- /dev/null +++ b/location/src/main/java/org/odk/collect/location/GoogleFusedLocationClient.java @@ -0,0 +1,231 @@ +package org.odk.collect.location; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.content.Context; +import android.location.Location; +import android.location.LocationManager; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; +import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; +import com.google.android.gms.location.FusedLocationProviderApi; +import com.google.android.gms.location.LocationListener; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationServices; + +import org.odk.collect.analytics.Analytics; + +import timber.log.Timber; + +/** + * An implementation of {@link LocationClient} that uses Google Play Services to retrieve the + * User's location. + *

+ * Should be used whenever there Google Play Services is present. In general, use + * {@link LocationClientProvider} to retrieve a configured {@link LocationClient}. + */ +@SuppressLint("MissingPermission") // Permission checks for location services handled in components that use this class +public class GoogleFusedLocationClient + extends BaseLocationClient + implements ConnectionCallbacks, OnConnectionFailedListener, LocationListener { + + /** + * The default requested time between location updates, in milliseconds. + */ + private static final long DEFAULT_UPDATE_INTERVAL = 5000; + + /** + * The default maximum rate at which location updates can arrive (other updates will be throttled), + * in milliseconds. + */ + private static final long DEFAULT_FASTEST_UPDATE_INTERVAL = 2500; + + /** + * Although FusedLocationProviderApi is deprecated, FusedLocationProviderClient which is + * supposed to replace it doesn't work until Google Play Services 11.6.0, released Nov 2017. + * Some of our users have really slow connections and old versions of Play Services so we should + * wait to switch APIs. + */ + @NonNull + private final FusedLocationProviderApi fusedLocationProviderApi; + + @NonNull + private final GoogleApiClient googleApiClient; + + @Nullable + private LocationListener locationListener; + + private long updateInterval = DEFAULT_UPDATE_INTERVAL; + private long fastestUpdateInterval = DEFAULT_FASTEST_UPDATE_INTERVAL; + private boolean retainMockAccuracy; + + /** + * Constructs a new GoogleFusedLocationClient with the provided Context. + *

+ * This Constructor should be used normally. + * + * @param application The application. Used as the Context for building the GoogleApiClient because + * it doesn't release context. + */ + public GoogleFusedLocationClient(@NonNull Application application) { + this(locationServicesClientForContext(application), LocationServices.FusedLocationApi, + (LocationManager) application.getSystemService(Context.LOCATION_SERVICE)); + } + + /** + * Constructs a new AndroidLocationClient with the provided GoogleApiClient + * and FusedLocationProviderApi. + *

+ * This Constructor should only be used for testing. + * + * @param googleApiClient The GoogleApiClient for managing the LocationClient's connection + * to Play Services. + * @param fusedLocationProviderApi The FusedLocationProviderApi for fetching the User's + * location. + */ + public GoogleFusedLocationClient(@NonNull GoogleApiClient googleApiClient, + @NonNull FusedLocationProviderApi fusedLocationProviderApi, + @NonNull LocationManager locationManager) { + super(locationManager); + + this.googleApiClient = googleApiClient; + this.fusedLocationProviderApi = fusedLocationProviderApi; + } + + // LocationClient: + + public void start(LocationClientListener listener) { + setListener(listener); + googleApiClient.registerConnectionCallbacks(this); + googleApiClient.registerConnectionFailedListener(this); + + googleApiClient.connect(); + } + + public void stop() { + stopLocationUpdates(); + + googleApiClient.unregisterConnectionCallbacks(this); + googleApiClient.unregisterConnectionFailedListener(this); + + if (googleApiClient.isConnected()) { + googleApiClient.disconnect(); + } + + if (getListener() != null) { + getListener().onClientStop(); + } + setListener(null); + } + + public void requestLocationUpdates(@NonNull LocationListener locationListener) { + if (!isMonitoringLocation() && googleApiClient.isConnected()) { + fusedLocationProviderApi.requestLocationUpdates(googleApiClient, createLocationRequest(), this); + } + + this.locationListener = locationListener; + } + + public void stopLocationUpdates() { + if (!isMonitoringLocation()) { + return; + } + + if (googleApiClient.isConnected()) { + fusedLocationProviderApi.removeLocationUpdates(googleApiClient, locationListener); + } + locationListener = null; + } + + @Override + public void setRetainMockAccuracy(boolean retainMockAccuracy) { + this.retainMockAccuracy = retainMockAccuracy; + } + + @Override + public Location getLastLocation() { + // We need to block if the Client isn't already connected: + if (!googleApiClient.isConnected()) { + googleApiClient.blockingConnect(); + } + + return LocationUtils.sanitizeAccuracy(fusedLocationProviderApi.getLastLocation(googleApiClient), retainMockAccuracy); + } + + @Override + public boolean isMonitoringLocation() { + return locationListener != null; + } + + @Override + public void setUpdateIntervals(long updateInterval, long fastestUpdateInterval) { + Timber.i("GoogleFusedLocationClient setting update intervals: %d, %d", updateInterval, fastestUpdateInterval); + + this.updateInterval = updateInterval; + this.fastestUpdateInterval = fastestUpdateInterval; + } + + // GoogleFusedLocationClient: + + private LocationRequest createLocationRequest() { + LocationRequest locationRequest = new LocationRequest(); + locationRequest.setPriority(getPriority().getValue()); + + locationRequest.setInterval(updateInterval); + locationRequest.setFastestInterval(fastestUpdateInterval); + + return locationRequest; + } + + // ConnectionCallbacks: + + @Override + public void onConnected(@Nullable Bundle bundle) { + if (getListener() != null) { + getListener().onClientStart(); + } + } + + @Override + public void onConnectionSuspended(int cause) { + } + + // OnConnectionFailedListener: + + @Override + public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { + Analytics.log("GoogleFusedLocationClientConnectionFailed"); + if (getListener() != null) { + getListener().onClientStartFailure(); + } + } + + // LocationListener: + + @Override + public void onLocationChanged(Location location) { + Timber.i("Location changed: %s", location.toString()); + + if (locationListener != null) { + locationListener.onLocationChanged(LocationUtils.sanitizeAccuracy(location, retainMockAccuracy)); + } + } + + /** + * Helper method for building a GoogleApiClient with the LocationServices API. + * + * @param context The Context for building the GoogleApiClient. + * @return A GoogleApiClient with the LocationServices API. + */ + private static GoogleApiClient locationServicesClientForContext(@NonNull Context context) { + return new GoogleApiClient.Builder(context) + .addApi(LocationServices.API) + .build(); + } +} diff --git a/location/src/main/java/org/odk/collect/location/GoogleFusedLocationClient.kt b/location/src/main/java/org/odk/collect/location/GoogleFusedLocationClient.kt deleted file mode 100644 index 42b185f8ab3..00000000000 --- a/location/src/main/java/org/odk/collect/location/GoogleFusedLocationClient.kt +++ /dev/null @@ -1,131 +0,0 @@ -package org.odk.collect.location - -import android.annotation.SuppressLint -import android.app.Application -import android.content.Context -import android.location.Location -import android.location.LocationManager -import android.os.Looper -import com.google.android.gms.location.FusedLocationProviderClient -import com.google.android.gms.location.LocationCallback -import com.google.android.gms.location.LocationListener -import com.google.android.gms.location.LocationRequest -import com.google.android.gms.location.LocationResult -import com.google.android.gms.location.LocationServices -import org.odk.collect.location.LocationClient.LocationClientListener -import org.odk.collect.location.LocationUtils.sanitizeAccuracy - -/** - * An implementation of [LocationClient] that uses Google Play Services to retrieve the User's location. - * - * Should be used whenever Google Play Services is present. In general, use - * [LocationClientProvider] to retrieve a configured [LocationClient]. - */ -class GoogleFusedLocationClient( - private val fusedLocationProviderClientWrapper: FusedLocationProviderClientWrapper, - locationManager: LocationManager -) : BaseLocationClient(locationManager), LocationListener { - - constructor(application: Application) : this( - FusedLocationProviderClientWrapper(LocationServices.getFusedLocationProviderClient(application)), - (application.getSystemService(Context.LOCATION_SERVICE) as LocationManager) - ) - - private var lastKnownLocation: Location? = null - set(value) { - field = sanitizeAccuracy(value, retainMockAccuracy) - } - - private var locationListener: LocationListener? = null - private var updateInterval: Long = 5000 - private var fastestUpdateInterval: Long = 2500 - private var retainMockAccuracy = false - - override fun start(listener: LocationClientListener) { - fusedLocationProviderClientWrapper.start(this) - setListener(listener) - listener.onClientStart() - } - - override fun stop() { - stopLocationUpdates() - getListener()?.onClientStop() - setListener(null) - } - - override fun requestLocationUpdates(locationListener: LocationListener) { - if (!isMonitoringLocation) { - fusedLocationProviderClientWrapper.requestLocationUpdates(createLocationRequest()) - } - this.locationListener = locationListener - } - - override fun stopLocationUpdates() { - if (isMonitoringLocation) { - fusedLocationProviderClientWrapper.removeLocationUpdates() - locationListener = null - } - } - - override fun setRetainMockAccuracy(retainMockAccuracy: Boolean) { - this.retainMockAccuracy = retainMockAccuracy - } - - override fun getLastLocation(): Location? { - return lastKnownLocation - } - - override fun isMonitoringLocation(): Boolean { - return locationListener != null - } - - override fun setUpdateIntervals(updateInterval: Long, fastestUpdateInterval: Long) { - this.updateInterval = updateInterval - this.fastestUpdateInterval = fastestUpdateInterval - } - - override fun onLocationChanged(location: Location) { - lastKnownLocation = location - lastKnownLocation?.let { - locationListener?.onLocationChanged(it) - } - } - - private fun createLocationRequest(): LocationRequest { - return LocationRequest.create().apply { - priority = this@GoogleFusedLocationClient.getPriority().value - interval = updateInterval - fastestInterval = fastestUpdateInterval - } - } -} - -@SuppressLint("MissingPermission") // Permission checks for location services handled in components that use this class -class FusedLocationProviderClientWrapper(private val fusedLocationProviderClient: FusedLocationProviderClient) { - private lateinit var listener: LocationListener - - private var locationCallback: LocationCallback = object : LocationCallback() { - override fun onLocationResult(locationResult: LocationResult) { - for (location in locationResult.locations) { - listener.onLocationChanged(location) - } - } - } - - fun start(listener: LocationListener) { - this.listener = listener - fusedLocationProviderClient.lastLocation.addOnSuccessListener { location: Location? -> - location?.let { - listener.onLocationChanged(it) - } - } - } - - fun requestLocationUpdates(locationRequest: LocationRequest) { - fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()) - } - - fun removeLocationUpdates() { - fusedLocationProviderClient.removeLocationUpdates(locationCallback) - } -} diff --git a/location/src/test/java/org/odk/collect/location/GoogleFusedLocationClientTest.java b/location/src/test/java/org/odk/collect/location/GoogleFusedLocationClientTest.java new file mode 100644 index 00000000000..4b219db45ae --- /dev/null +++ b/location/src/test/java/org/odk/collect/location/GoogleFusedLocationClientTest.java @@ -0,0 +1,282 @@ +package org.odk.collect.location; + +import static android.location.LocationManager.GPS_PROVIDER; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.location.Location; +import android.location.LocationManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.location.FusedLocationProviderApi; +import com.google.android.gms.location.LocationListener; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.odk.collect.testshared.LocationTestUtils; + +@RunWith(AndroidJUnit4.class) +public class GoogleFusedLocationClientTest { + + private GoogleApiClient googleApiClient; + private GoogleFusedLocationClient client; + private FusedLocationProviderApi fusedLocationProviderApi; + + @Before + public void setUp() { + fusedLocationProviderApi = mock(FusedLocationProviderApi.class); + googleApiClient = mock(GoogleApiClient.class); + LocationManager locationManager = mock(LocationManager.class); + client = new GoogleFusedLocationClient(googleApiClient, fusedLocationProviderApi, locationManager); + } + + @Test + public void startShouldSetListenerAndStopShouldRemoveIt() { + LocationClient.LocationClientListener listener = mock(LocationClient.LocationClientListener.class); + client.start(listener); + assertThat(client.getListener(), is(notNullValue())); + + client.stop(); + assertThat(client.getListener(), is(nullValue())); + } + + @Test + public void startShouldCallLocationClientOnConnected() { + doAnswer(new OnConnectedAnswer()).when(googleApiClient).connect(); + + LocationClient.LocationClientListener listener = mock(LocationClient.LocationClientListener.class); + + client.start(listener); + verify(listener).onClientStart(); + verify(listener, never()).onClientStartFailure(); + verify(listener, never()).onClientStop(); + + client.stop(); + + reset(listener); + doAnswer(new OnConnectionFailedAnswer()).when(googleApiClient).connect(); + + client.start(listener); + verify(listener, never()).onClientStart(); + verify(listener).onClientStartFailure(); + verify(listener, never()).onClientStop(); + + client.stop(); + verify(listener, never()).onClientStart(); + verify(listener).onClientStartFailure(); + verify(listener).onClientStop(); + } + + @Test + public void stopShouldDisconnectFromGoogleApiIfConnected_andAlwaysCallOnClientStopIfListenerSet() { + LocationClient.LocationClientListener listener = mock(LocationClient.LocationClientListener.class); + client.start(listener); + + // Previously connected, disconnection succeeds + when(googleApiClient.isConnected()).thenReturn(true); + client.stop(); + verify(googleApiClient).disconnect(); + verify(listener).onClientStop(); + + reset(listener); + + client.start(listener); + // Previously connected, disconnection calls onConnectionSuspended + doAnswer(new OnDisconnectedAnswer()).when(googleApiClient).disconnect(); + client.stop(); + verify(listener).onClientStop(); + + reset(listener); + + client.start(listener); + // Not previously connected + when(googleApiClient.isConnected()).thenReturn(false); + client.stop(); + verify(listener).onClientStop(); + } + + @Test + public void whenGoogleApiClientNotConnected_shouldNotRemoveLocationUpdatesBeCalled() { + when(googleApiClient.isConnected()).thenReturn(false); + + client.start(null); + + TestLocationListener listener = new TestLocationListener(); + client.requestLocationUpdates(listener); + + client.stop(); + verify(fusedLocationProviderApi, never()).removeLocationUpdates(googleApiClient, listener); + } + + @Test + public void whenGoogleApiClientNotConnected_shouldNotRequestLocationUpdatesBeCalled() { + when(googleApiClient.isConnected()).thenReturn(false); + + client.start(null); + client.requestLocationUpdates(new TestLocationListener()); + + verify(fusedLocationProviderApi, never()).requestLocationUpdates(any(), any(), (LocationListener) any()); + } + + @Test + public void requestingLocationUpdatesShouldUpdateCorrectListener() { + client.start(null); + + TestLocationListener firstListener = new TestLocationListener(); + client.requestLocationUpdates(firstListener); + + Location firstLocation = newMockLocation(); + client.onLocationChanged(firstLocation); + + assertSame(firstLocation, firstListener.getLastLocation()); + + Location secondLocation = newMockLocation(); + client.onLocationChanged(secondLocation); + + assertSame(secondLocation, firstListener.getLastLocation()); + + // Now stop updates: + client.stopLocationUpdates(); + + Location thirdLocation = newMockLocation(); + client.onLocationChanged(thirdLocation); + + // Should still be second: + assertSame(secondLocation, firstListener.getLastLocation()); + + // Call requestLocationUpdates again with new listener: + TestLocationListener secondListener = new TestLocationListener(); + client.requestLocationUpdates(secondListener); + + Location fourthLocation = newMockLocation(); + client.onLocationChanged(fourthLocation); + + // First listener should still have second location: + assertSame(secondLocation, firstListener.getLastLocation()); + assertSame(fourthLocation, secondListener.getLastLocation()); + + // Call stop() and make sure it called stopLocationUpdates(): + client.stop(); + + Location fifthLocation = newMockLocation(); + client.onLocationChanged(fifthLocation); + + // Listener should still have fourth location: + assertSame(fourthLocation, secondListener.getLastLocation()); + } + + @Test + public void getLastLocationShouldCallBlockingConnectIfNotConnected() { + client.getLastLocation(); + verify(googleApiClient).blockingConnect(); + + when(googleApiClient.isConnected()).thenReturn(true); + client.start(null); + + client.getLastLocation(); + verify(googleApiClient).blockingConnect(); // 'verify' checks if called *once*. + } + + @Test + public void whenNewlyReceivedLocationAccuracyIsNegative_shouldBeSetToZero() { + TestLocationListener listener = new TestLocationListener(); + client.requestLocationUpdates(listener); + + Location location = LocationTestUtils.createLocation("GPS", 7, 2, 3, -1); + client.onLocationChanged(location); + + assertThat(listener.getLastLocation().getAccuracy(), is(0.0f)); + } + + @Test + public void whenNewlyReceivedLocationIsMocked_shouldAccuracyBeSetToZero() { + TestLocationListener listener = new TestLocationListener(); + client.requestLocationUpdates(listener); + + Location location = LocationTestUtils.createLocation("GPS", 7, 2, 3, 5.0f, true); + client.onLocationChanged(location); + + assertThat(listener.getLastLocation().getAccuracy(), is(0.0f)); + } + + @Test + public void whenNewlyReceivedLocationIsMocked_andRetainMockAccuracyIsTrue_doesNotChangeAccuracy() { + TestLocationListener listener = new TestLocationListener(); + client.setRetainMockAccuracy(true); + client.requestLocationUpdates(listener); + + Location location = LocationTestUtils.createLocation("GPS", 7, 2, 3, 5.0f, true); + client.onLocationChanged(location); + + assertThat(listener.getLastLocation().getAccuracy(), is(5.0f)); + } + + @Test + public void whenLastKnownLocationAccuracyIsNegative_shouldBeSetToZero() { + Location location = LocationTestUtils.createLocation(GPS_PROVIDER, 7, 2, 3, -1.0f); + when(fusedLocationProviderApi.getLastLocation(googleApiClient)).thenReturn(location); + + assertThat(client.getLastLocation().getAccuracy(), is(0.0f)); + } + + @Test + public void whenLastKnownLocationIsMocked_shouldAccuracyBeSetToZero() { + Location location = LocationTestUtils.createLocation(GPS_PROVIDER, 7, 2, 3, 5.0f, true); + when(fusedLocationProviderApi.getLastLocation(googleApiClient)).thenReturn(location); + + assertThat(client.getLastLocation().getAccuracy(), is(0.0f)); + } + + @Test + public void whenLastKnownLocationIsMocked_andRetainMockAccuracyIsTrue_doesNotChangeAccuracy() { + Location location = LocationTestUtils.createLocation(GPS_PROVIDER, 7, 2, 3, 5.0f, true); + client.setRetainMockAccuracy(true); + when(fusedLocationProviderApi.getLastLocation(googleApiClient)).thenReturn(location); + + assertThat(client.getLastLocation().getAccuracy(), is(5.0f)); + } + + private static Location newMockLocation() { + return mock(Location.class); + } + + private class OnConnectedAnswer implements Answer { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + client.onConnected(null); + return null; + } + } + + private class OnConnectionFailedAnswer implements Answer { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + client.onConnectionFailed(new ConnectionResult(0)); + return null; + } + } + + private class OnDisconnectedAnswer implements Answer { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + client.onConnectionSuspended(0); + return null; + } + } +} diff --git a/location/src/test/java/org/odk/collect/location/GoogleFusedLocationClientTest.kt b/location/src/test/java/org/odk/collect/location/GoogleFusedLocationClientTest.kt deleted file mode 100644 index 4449d9fb18a..00000000000 --- a/location/src/test/java/org/odk/collect/location/GoogleFusedLocationClientTest.kt +++ /dev/null @@ -1,145 +0,0 @@ -package org.odk.collect.location - -import android.location.Location -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.android.gms.location.LocationRequest -import org.hamcrest.CoreMatchers.equalTo -import org.hamcrest.CoreMatchers.notNullValue -import org.hamcrest.CoreMatchers.nullValue -import org.hamcrest.MatcherAssert.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.verify -import org.mockito.kotlin.any -import org.mockito.kotlin.argThat -import org.mockito.kotlin.isA -import org.mockito.kotlin.mock -import org.odk.collect.location.LocationClient.LocationClientListener -import org.odk.collect.testshared.LocationTestUtils.createLocation - -@RunWith(AndroidJUnit4::class) -class GoogleFusedLocationClientTest { - private val fusedLocationProviderClientWrapper = mock() - private val locationClientListener = mock() - private val locationListener = TestLocationListener() - private lateinit var client: GoogleFusedLocationClient - - @Before - fun setUp() { - client = GoogleFusedLocationClient(fusedLocationProviderClientWrapper, mock()) - } - - @Test - fun startShouldSetListenerAndCallOnClientStart() { - client.start(locationClientListener) - - assertThat(client.getListener(), notNullValue()) - verify(locationClientListener).onClientStart() - } - - @Test - fun startShouldStartFusedLocationProviderClientWrapper() { - client.start(locationClientListener) - - verify(fusedLocationProviderClientWrapper).start(isA()) - } - - @Test - fun stopShouldRemoveListenerAndCallOnClientStop() { - client.start(locationClientListener) - client.stop() - - assertThat(client.getListener(), nullValue()) - verify(locationClientListener).onClientStop() - } - - @Test - fun stopShouldStopLocationUpdates() { - val listener = TestLocationListener() - client.requestLocationUpdates(listener) - client.stop() - - verify(fusedLocationProviderClientWrapper).removeLocationUpdates() - } - - @Test - fun requestLocationUpdatesShouldStartLocationUpdates() { - val listener = TestLocationListener() - client.requestLocationUpdates(listener) - - verify(fusedLocationProviderClientWrapper).requestLocationUpdates(any()) - } - - @Test - fun requestLocationUpdatesBuildsLocationRequestBasedOnPassedValues() { - client.setPriority(LocationClient.Priority.PRIORITY_NO_POWER) - client.setUpdateIntervals(10L, 5L) - val listener = TestLocationListener() - client.requestLocationUpdates(listener) - - verify(fusedLocationProviderClientWrapper).requestLocationUpdates( - argThat { locationRequest: LocationRequest -> - assertThat(locationRequest.priority, equalTo(LocationClient.Priority.PRIORITY_NO_POWER.value)) - assertThat(locationRequest.interval, equalTo(10L)) - assertThat(locationRequest.fastestInterval, equalTo(5L)) - true - } - ) - } - - @Test - fun isMonitoringLocationReturnsFalseIfListenerIsNotSet() { - client.stopLocationUpdates() - - assertThat(client.isMonitoringLocation, equalTo(false)) - } - - @Test - fun isMonitoringLocationReturnsTrueIfListenerIsSet() { - val listener = TestLocationListener() - client.requestLocationUpdates(listener) - - assertThat(client.isMonitoringLocation, equalTo(true)) - } - - @Test - fun stopLocationUpdatesShouldStopLocationUpdates() { - val listener = TestLocationListener() - client.requestLocationUpdates(listener) - client.stopLocationUpdates() - - verify(fusedLocationProviderClientWrapper).removeLocationUpdates() - } - - @Test - fun whenReceivedLocationAccuracyIsNegative_shouldBeSetToZero() { - client.requestLocationUpdates(locationListener) - val location: Location = createLocation("GPS", 7.0, 2.0, 3.0, -1f) - client.onLocationChanged(location) - - assertThat(client.lastLocation!!.accuracy, equalTo(0.0f)) - assertThat(locationListener.lastLocation!!.accuracy, equalTo(0.0f)) - } - - @Test - fun whenReceivedLocationIsMocked_shouldAccuracyBeSetToZero() { - client.requestLocationUpdates(locationListener) - val location: Location = createLocation("GPS", 7.0, 2.0, 3.0, 5.0f, true) - client.onLocationChanged(location) - - assertThat(client.lastLocation!!.accuracy, equalTo(0.0f)) - assertThat(locationListener.lastLocation!!.accuracy, equalTo(0.0f)) - } - - @Test - fun whenReceivedLocationIsMocked_andRetainMockAccuracyIsTrue_doesNotChangeAccuracy() { - client.setRetainMockAccuracy(true) - client.requestLocationUpdates(locationListener) - val location: Location = createLocation("GPS", 7.0, 2.0, 3.0, 5.0f, true) - client.onLocationChanged(location) - - assertThat(client.lastLocation!!.accuracy, equalTo(5.0f)) - assertThat(locationListener.lastLocation!!.accuracy, equalTo(5.0f)) - } -}