Failing UI test for NearbyActivity
Recently one of the tests started failing. Heres the build stackstrace.

```
fr.free.nrw.commons.NearbyActivityTest > testActivityLaunch[test(AVD) - 5.1.1] FAILED 
	java.lang.RuntimeException: Could not launch intent Intent { act=android.intent.action.MAIN flg=0x14000000 cmp=fr.free.nrw.commons/.nearby.NearbyActivity } within 45 seconds. Perhaps the main thread has not gone idle within a reasonable amount of time? There could be an animation or something constantly repainting the screen. Or the activity is doing network calls on creation? See the threaddump logs. For your reference the last time the event queue was idle before your activity launch request was 1511464601042 and now the last time the queue went idle was: 1511464601042. If these numbers are the same your activity might be hogging the event queue.
	at android.support.test.runner.MonitoringInstrumentation.startActivitySync(MonitoringInstrumentation.java:419)
:app:connectedBetaDebugAndroidTest FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:connectedBetaDebugAndroidTest'.
```

When I run the task `:app:connectedBetaDebugAndroidTest` locally, it succeeds for me. 

Can anyone help with this issue. @whym @psh 
diff --git a/app/build.gradle b/app/build.gradle
index 1076f3e46..02897d4d8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -78,7 +78,6 @@ android {
 
     defaultConfig {
         applicationId 'fr.free.nrw.commons'
-
         versionCode 76
         versionName '2.6.1'
         setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b0cbdd34f..872d02b9e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -16,6 +16,9 @@
     <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS"/>
     <uses-permission android:name="android.permission.READ_LOGS"/>
 
+    <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
+    <uses-feature android:name="android.hardware.location.gps" />
+
     <application
         android:name=".CommonsApplication"
         android:icon="@drawable/ic_launcher"
diff --git a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java b/app/src/main/java/fr/free/nrw/commons/location/LatLng.java
index 787c8c8a5..3ddaab568 100644
--- a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java
+++ b/app/src/main/java/fr/free/nrw/commons/location/LatLng.java
@@ -1,5 +1,8 @@
 package fr.free.nrw.commons.location;
 
+import android.location.Location;
+import android.support.annotation.NonNull;
+
 public class LatLng {
 
     private final double latitude;
@@ -22,6 +25,10 @@ public class LatLng {
         this.accuracy = accuracy;
     }
 
+    public static LatLng from(@NonNull Location location) {
+        return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
+    }
+
     public int hashCode() {
         boolean var1 = true;
         byte var2 = 1;
diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java
index e830d8d06..d4a19c9fc 100644
--- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java
+++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java
@@ -1,11 +1,15 @@
 package fr.free.nrw.commons.location;
 
+import android.Manifest;
+import android.app.Activity;
 import android.content.Context;
-import android.location.Criteria;
+import android.content.pm.PackageManager;
 import android.location.Location;
 import android.location.LocationListener;
 import android.location.LocationManager;
 import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
 
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -17,59 +21,136 @@ import timber.log.Timber;
 
 @Singleton
 public class LocationServiceManager implements LocationListener {
+    public static final int LOCATION_REQUEST = 1;
 
-    private String provider;
+    private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 1000;
+    private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10;
+
+    private Context context;
     private LocationManager locationManager;
-    private LatLng lastLocation;
-    private Float latestLocationAccuracy;
+    private Location lastLocation;
     private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
+    private boolean isLocationManagerRegistered = false;
 
     @Inject
     public LocationServiceManager(Context context) {
+        this.context = context;
         this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
-        provider = locationManager.getBestProvider(new Criteria(), true);
     }
 
     public boolean isProviderEnabled() {
         return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
     }
 
-    public LatLng getLastLocation() {
-        return lastLocation;
+    public boolean isLocationPermissionGranted() {
+        return ContextCompat.checkSelfPermission(context,
+                Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
     }
 
-    /**
-     * Returns the accuracy of the location. The measurement is
-     * given as a radius in meter of 68 % confidence.
-     *
-     * @return Float
-     */
-    public Float getLatestLocationAccuracy() {
-        return latestLocationAccuracy;
+    public void requestPermissions(Activity activity) {
+        if (activity.isFinishing()) {
+            return;
+        }
+        ActivityCompat.requestPermissions(activity,
+                new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
+                LOCATION_REQUEST);
+    }
+
+    public boolean isPermissionExplanationRequired(Activity activity) {
+        if (activity.isFinishing()) {
+            return false;
+        }
+        return ActivityCompat.shouldShowRequestPermissionRationale(activity,
+                Manifest.permission.ACCESS_FINE_LOCATION);
+    }
+
+    public LatLng getLastLocation() {
+        if (lastLocation == null) {
+            return null;
+        }
+        return LatLng.from(lastLocation);
     }
 
     /** Registers a LocationManager to listen for current location.
      */
     public void registerLocationManager() {
+        if (!isLocationManagerRegistered)
+            isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
+                    && requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
+    }
+
+    private boolean requestLocationUpdatesFromProvider(String locationProvider) {
         try {
-            locationManager.requestLocationUpdates(provider, 400, 1, this);
-            Location location = locationManager.getLastKnownLocation(provider);
-            //Location works, just need to 'send' GPS coords
-            // via emulator extended controls if testing on emulator
-            Timber.d("Checking for location...");
-            if (location != null) {
-                this.onLocationChanged(location);
-            }
+            locationManager.requestLocationUpdates(locationProvider,
+                    MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS,
+                    MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS,
+                    this);
+            return true;
         } catch (IllegalArgumentException e) {
             Timber.e(e, "Illegal argument exception");
+            return false;
         } catch (SecurityException e) {
             Timber.e(e, "Security exception");
+            return false;
+        }
+    }
+
+    protected boolean isBetterLocation(Location location, Location currentBestLocation) {
+        if (currentBestLocation == null) {
+            // A new location is always better than no location
+            return true;
+        }
+
+        // Check whether the new location fix is newer or older
+        long timeDelta = location.getTime() - currentBestLocation.getTime();
+        boolean isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
+        boolean isSignificantlyOlder = timeDelta < -MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
+        boolean isNewer = timeDelta > 0;
+
+        // If it's been more than two minutes since the current location, use the new location
+        // because the user has likely moved
+        if (isSignificantlyNewer) {
+            return true;
+            // If the new location is more than two minutes older, it must be worse
+        } else if (isSignificantlyOlder) {
+            return false;
+        }
+
+        // Check whether the new location fix is more or less accurate
+        int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
+        boolean isLessAccurate = accuracyDelta > 0;
+        boolean isMoreAccurate = accuracyDelta < 0;
+        boolean isSignificantlyLessAccurate = accuracyDelta > 200;
+
+        // Check if the old and new location are from the same provider
+        boolean isFromSameProvider = isSameProvider(location.getProvider(),
+                currentBestLocation.getProvider());
+
+        // Determine location quality using a combination of timeliness and accuracy
+        if (isMoreAccurate) {
+            return true;
+        } else if (isNewer && !isLessAccurate) {
+            return true;
+        } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
+            return true;
         }
+        return false;
+    }
+
+    /**
+     * Checks whether two providers are the same
+     */
+    private boolean isSameProvider(String provider1, String provider2) {
+        if (provider1 == null) {
+            return provider2 == null;
+        }
+        return provider1.equals(provider2);
     }
 
     /** Unregisters location manager.
      */
     public void unregisterLocationManager() {
+        isLocationManagerRegistered = false;
         try {
             locationManager.removeUpdates(this);
         } catch (SecurityException e) {
@@ -89,15 +170,11 @@ public class LocationServiceManager implements LocationListener {
 
     @Override
     public void onLocationChanged(Location location) {
-        double currentLatitude = location.getLatitude();
-        double currentLongitude = location.getLongitude();
-        latestLocationAccuracy = location.getAccuracy();
-        Timber.d("Latitude: %f Longitude: %f Accuracy %f",
-                currentLatitude, currentLongitude, latestLocationAccuracy);
-        lastLocation = new LatLng(currentLatitude, currentLongitude, latestLocationAccuracy);
-
-        for (LocationUpdateListener listener : locationListeners) {
-            listener.onLocationChanged(lastLocation);
+        if (isBetterLocation(location, lastLocation)) {
+            lastLocation = location;
+            for (LocationUpdateListener listener : locationListeners) {
+                listener.onLocationChanged(LatLng.from(lastLocation));
+            }
         }
     }
 
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java
index 578b5e91e..f018d1190 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java
@@ -1,6 +1,5 @@
 package fr.free.nrw.commons.nearby;
 
-import android.Manifest;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
@@ -10,10 +9,8 @@ import android.os.Build;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
 import android.support.annotation.NonNull;
-import android.support.v4.app.ActivityCompat;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentTransaction;
-import android.support.v4.content.ContextCompat;
 import android.support.v7.app.AlertDialog;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -46,12 +43,13 @@ import io.reactivex.disposables.Disposable;
 import io.reactivex.schedulers.Schedulers;
 import timber.log.Timber;
 
+import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST;
+
 
 public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
 
     @BindView(R.id.progressBar)
     ProgressBar progressBar;
-    private static final int LOCATION_REQUEST = 1;
     private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed";
 
     @Inject
@@ -70,7 +68,6 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
         sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
         setContentView(R.layout.activity_nearby);
         ButterKnife.bind(this);
-        checkLocationPermission();
         bundle = new Bundle();
         initDrawer();
         initViewState();
@@ -102,7 +99,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
         // Handle item selection
         switch (item.getItemId()) {
             case R.id.action_refresh:
-                lockNearbyView = false;
+                lockNearbyView(false);
                 refreshView(true);
                 return true;
             case R.id.action_toggle_view:
@@ -115,52 +112,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
         }
     }
 
-    private void checkLocationPermission() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            if (ContextCompat.checkSelfPermission(this,
-                    Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
-                refreshView(false);
-            } else {
-                if (ContextCompat.checkSelfPermission(this,
-                        Manifest.permission.ACCESS_FINE_LOCATION)
-                        != PackageManager.PERMISSION_GRANTED) {
-
-                    // Should we show an explanation?
-                    if (ActivityCompat.shouldShowRequestPermissionRationale(this,
-                            Manifest.permission.ACCESS_FINE_LOCATION)) {
-
-                        // Show an explanation to the user *asynchronously* -- don't block
-                        // this thread waiting for the user's response! After the user
-                        // sees the explanation, try again to request the permission.
-
-                        new AlertDialog.Builder(this)
-                                .setMessage(getString(R.string.location_permission_rationale))
-                                .setPositiveButton("OK", (dialog, which) -> {
-                                    ActivityCompat.requestPermissions(NearbyActivity.this,
-                                            new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
-                                            LOCATION_REQUEST);
-                                    dialog.dismiss();
-                                })
-                                .setNegativeButton("Cancel", null)
-                                .create()
-                                .show();
-
-                    } else {
-
-                        // No explanation needed, we can request the permission.
-
-                        ActivityCompat.requestPermissions(this,
-                                new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
-                                LOCATION_REQUEST);
-
-                        // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
-                        // app-defined int constant. The callback method gets the
-                        // result of the request.
-                    }
-                }
-            }
-        } else {
-            refreshView(false);
+    private void requestLocationPermissions() {
+        if (!isFinishing()) {
+            locationManager.requestPermissions(this);
         }
     }
 
@@ -185,7 +139,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
                 .setCancelable(false)
                 .setPositiveButton(R.string.give_permission, (dialog, which) -> {
                     //will ask for the location permission again
-                    checkLocationPermission();
+                    checkGps();
                 })
                 .setNegativeButton(R.string.cancel, (dialog, which) -> {
                     //dismiss dialog and finish activity
@@ -209,11 +163,48 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
                                 Timber.d("Loaded settings page");
                                 startActivityForResult(callGPSSettingIntent, 1);
                             })
-                    .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> dialog.cancel())
+                    .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> {
+                        showLocationPermissionDeniedErrorDialog();
+                        dialog.cancel();
+                    })
                     .create()
                     .show();
         } else {
             Timber.d("GPS is enabled");
+            checkLocationPermission();
+        }
+    }
+
+    private void checkLocationPermission() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            if (locationManager.isLocationPermissionGranted()) {
+                refreshView(false);
+            } else {
+                // Should we show an explanation?
+                if (locationManager.isPermissionExplanationRequired(this)) {
+                    // Show an explanation to the user *asynchronously* -- don't block
+                    // this thread waiting for the user's response! After the user
+                    // sees the explanation, try again to request the permission.
+                    new AlertDialog.Builder(this)
+                            .setMessage(getString(R.string.location_permission_rationale_nearby))
+                            .setPositiveButton("OK", (dialog, which) -> {
+                                requestLocationPermissions();
+                                dialog.dismiss();
+                            })
+                            .setNegativeButton("Cancel", (dialog, id) -> {
+                                showLocationPermissionDeniedErrorDialog();
+                                dialog.cancel();
+                            })
+                            .create()
+                            .show();
+
+                } else {
+                    // No explanation needed, we can request the permission.
+                    requestLocationPermissions();
+                }
+            }
+        } else {
+            refreshView(false);
         }
     }
 
@@ -238,7 +229,6 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
     @Override
     protected void onStart() {
         super.onStart();
-        locationManager.registerLocationManager();
         locationManager.addLocationListener(this);
     }
 
@@ -262,13 +252,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
         super.onResume();
         lockNearbyView = false;
         checkGps();
-        refreshView(false);
     }
 
+    /**
+     * This method should be the single point to load/refresh nearby places
+     *
+     * @param isHardRefresh
+     */
     private void refreshView(boolean isHardRefresh) {
         if (lockNearbyView) {
             return;
         }
+        locationManager.registerLocationManager();
         LatLng lastLocation = locationManager.getLastLocation();
         if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed
             if (isHardRefresh) {
@@ -314,7 +309,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
         bundle.putString("PlaceList", gsonPlaceList);
         bundle.putString("CurLatLng", gsonCurLatLng);
 
-        lockNearbyView = true;
+        lockNearbyView(true);
         // Begin the transaction
         if (viewMode.isMap()) {
             setMapFragment();
@@ -325,6 +320,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
         hideProgressBar();
     }
 
+    private void lockNearbyView(boolean lock) {
+        if (lock) {
+            lockNearbyView = true;
+            locationManager.unregisterLocationManager();
+            locationManager.removeLocationListener(this);
+        } else {
+            lockNearbyView = false;
+            locationManager.registerLocationManager();
+            locationManager.addLocationListener(this);
+        }
+    }
+
     private void hideProgressBar() {
         if (progressBar != null) {
             progressBar.setVisibility(View.GONE);
@@ -353,11 +360,6 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
         fragmentTransaction.commitAllowingStateLoss();
     }
 
-    public static void startYourself(Context context) {
-        Intent settingsIntent = new Intent(context, NearbyActivity.class);
-        context.startActivity(settingsIntent);
-    }
-
     @Override
     public void onLocationChanged(LatLng latLng) {
         refreshView(false);
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 81da9000f..04d49faee 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -213,4 +213,5 @@ Tap this message (or hit back) to skip this step.</string>
 
   <string name="nearby_location_has_not_changed">Location has not changed.</string>
   <string name="nearby_location_not_available">Location not available.</string>
+  <string name="location_permission_rationale_nearby">Permission required to display a list of nearby places</string>
 </resources>
diff --git a/app/src/test/java/fr/free/nrw/commons/nearby/NearbyActivityTest.java b/app/src/test/java/fr/free/nrw/commons/nearby/NearbyActivityTest.java
index c223203b0..7a0f50119 100644
--- a/app/src/test/java/fr/free/nrw/commons/nearby/NearbyActivityTest.java
+++ b/app/src/test/java/fr/free/nrw/commons/nearby/NearbyActivityTest.java
@@ -1,7 +1,6 @@
 package fr.free.nrw.commons.nearby;
 
 import android.app.Activity;
-import android.support.v4.app.Fragment;
 
 import org.junit.Before;
 import org.junit.Test;
['app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java', 'app/src/test/java/fr/free/nrw/commons/nearby/NearbyActivityTest.java', 'app/src/main/AndroidManifest.xml', 'app/build.gradle', 'app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java', 'app/src/main/res/values/strings.xml', 'app/src/main/java/fr/free/nrw/commons/location/LatLng.java']