I am working on a project where we are trying to track the position of a device and keep the data for later use. Before I talk about the issue I would like to provide some background.
By searching around StackExchange and Google and everywhere else, I have come to the conclusion that it is virtually impossible to get information about the satellites using the Fused Location API (good job there Google).
The method that most people are using is to actually use a LocationManager along side the Fused location to get the GPS Status. My first question comes here: How can we be 100% sure that the numbers provided by the LocationManager are in sync with what the Fused Location has given us? Does the Fused Location use the Manager internally?
And now the issue. The app is using an "always on" sticky service to pick up the positions no matter what. When there are no satellites everything works as intended. Placing the device to a position where it can see satellites it does not seem to have a lock. Using the debugger the GpsStatus.getSatellites() brings an empty list. Now, without moving the device I start the app Compass (by Catch.com as there are many) that has a GPS type compass scheme. That one locks the satellites, and quite fast, and from that moment on my app also reports the satellites. If the compass is closed then the app gets stuck on the last number the Compass was providing!!! The device I am personally using for testing is a Nexus 7 2013 with its latest official updates (Android 6.0.1).
Here is some code:
public class BackgroundLocationService extends Service implements
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
GpsStatus.Listener,
LocationListener {
// Constants here....
private GoogleApiClient mGoogleApiClient;
private LocationRequest mLocationRequest;
private LocationManager locationManager;
// Flag that indicates if a request is underway.
private boolean mInProgress;
private NotificationManagement myNotificationManager;
private Boolean servicesAvailable = false;
//And other variables here...
@Override
public void onCreate()
{
super.onCreate();
myNotificationManager = new NotificationManagement(getApplicationContext());
myNotificationManager.displayMainNotification();
mInProgress = false;
// Create the LocationRequest object
mLocationRequest = LocationRequest.create();
// Use high accuracy
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
// Set the update interval
mLocationRequest.setInterval(PREFERRED_INTERVAL);
// Set the fastest update interval
mLocationRequest.setFastestInterval(FASTEST_INTERVAL);
servicesAvailable = servicesConnected();
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
locationManager.addGpsStatusListener(this);
setUpLocationClientIfNeeded();
}
/**
* Create a new location client, using the enclosing class to
* handle callbacks.
*/
protected synchronized void buildGoogleApiClient()
{
this.mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(LocationServices.API)
.build();
}
private boolean servicesConnected()
{
// Check that Google Play services is available
int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
// If Google Play services is available
if (ConnectionResult.SUCCESS == resultCode)
{
return true;
}
else
{
return false;
}
}
public int onStartCommand(Intent intent, int flags, int startId)
{
super.onStartCommand(intent, flags, startId);
if (!servicesAvailable || mGoogleApiClient.isConnected() || mInProgress)
return START_STICKY;
setUpLocationClientIfNeeded();
if (!mGoogleApiClient.isConnected() || !mGoogleApiClient.isConnecting() && !mInProgress)
{
mInProgress = true;
mGoogleApiClient.connect();
}
return START_STICKY;
}
private void setUpLocationClientIfNeeded()
{
if (mGoogleApiClient == null)
buildGoogleApiClient();
}
public void onGpsStatusChanged(int event)
{
}
// Define the callback method that receives location updates
@Override
public void onLocationChanged(Location location)
{
simpleGPSFilter(location);
}
// Other fancy and needed stuff here...
/**
* "Stupid" filter that utilizes experience data to filter out location noise.
* @param location Location object carrying all the needed information
*/
private void simpleGPSFilter(Location location)
{
//Loading all the required variables
int signalPower = 0;
satellites = 0;
// Getting the satellites
mGpsStatus = locationManager.getGpsStatus(mGpsStatus);
Iterable<GpsSatellite> sats = mGpsStatus.getSatellites();
if (sats != null)
{
for (GpsSatellite sat : sats)
{
if (sat.usedInFix())
{
satellites++;
signalPower += sat.getSnr();
}
}
}
if (satellites != 0)
signalPower = signalPower/satellites;
mySpeed = (location.getSpeed() * 3600) / 1000;
myAccuracy = location.getAccuracy();
myBearing = location.getBearing();
latitude = location.getLatitude();
longitude = location.getLongitude();
Log.i("START OF CYCLE", "START OF CYCLE");
Log.i("Sat Strength", Integer.toString(signalPower));
Log.i("Locked Sats", Integer.toString(satellites));
// Do the math for the coordinates distance
/*
* Earth's radius at given Latitude.
* Formula: Radius = sqrt( ((equatorR^2 * cos(latitude))^2 + (poleR^2 * sin(latitude))^2 ) / ((equatorR * cos(latitude))^2 + (poleR * sin(latitude))^2)
* IMPORTANT: Math lib uses radians for the trigonometry equations so do not forget to use toRadians()
*/
Log.i("Lat for Radius", Double.toString(latitude));
double earthRadius = Math.sqrt((Math.pow((EARTH_RADIUS_EQUATOR * EARTH_RADIUS_EQUATOR * Math.cos(Math.toRadians(latitude))), 2)
+ Math.pow((EARTH_RADIUS_POLES * EARTH_RADIUS_POLES * Math.cos(Math.toRadians(latitude))), 2))
/ (Math.pow((EARTH_RADIUS_EQUATOR * Math.cos(Math.toRadians(latitude))), 2)
+ Math.pow((EARTH_RADIUS_POLES * Math.cos(Math.toRadians(latitude))), 2)));
Log.i("Earth Radius", Double.toString(earthRadius));
/*
* Calculating distance between 2 points on map using the Haversine formula (arctangent writing) with the following algorithm
* latDifference = latitude - lastLatitude;
* lngDifference = longitude - lastLongitude;
* a = (sin(latDifference/2))^2 + cos(lastLatitude) * cos(latitude) * (sin(lngDifference/2))^2
* c = 2 * atan2( sqrt(a), sqrt(1-a) )
* distance = earthRadius * c
*/
double latDifference = latitude - lastLatitude;
double lngDifference = longitude - lastLongitude;
double a = Math.pow((Math.sin(Math.toRadians(latDifference / 2))), 2) + (Math.cos(Math.toRadians(lastLatitude))
* Math.cos(Math.toRadians(latitude))
* Math.pow((Math.sin(Math.toRadians(lngDifference / 2))), 2));
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
double distance = earthRadius * c;
Log.i("New point distance", Double.toString(distance));
// Filter logic
// Make an initial location log
if ((!isInit) && (myAccuracy < ACCEPTED_ACCURACY))
{
isInit = true;
lastLatitude = latitude;
lastLongitude = longitude;
logLocations(location);
}
else
{
// Satellite lock (use of GPS) on the higher level
if (satellites == 0)
{
// Accuracy filtering at the second level
if (myAccuracy < ACCEPTED_ACCURACY)
{
if ((distance > ACCEPTED_DISTANCE))
{
lastLatitude = latitude;
lastLongitude = longitude;
logLocations(location);
Log.i("Location Logged", "No Sats");
/*
// Calculate speed in correlation to perceived movement
double speed = distance / (PREFERRED_INTERVAL / 1000); // TODO: Need to make actual time dynamic as the fused location does not have fixed timing
if (speed < ACCEPTED_SPEED)
{
lastLatitude = latitude;
lastLongitude = longitude;
logLocations(location);
} */
}
}
}
else if ((satellites < 4) && (signalPower > ACCEPTED_SIGNAL))
{
if (myAccuracy < (ACCEPTED_ACCURACY + 50))
{
logLocations(location);
Log.i("Location Logged", "With Sats");
}
}
else
{
if (myAccuracy < (ACCEPTED_ACCURACY + 100))
{
lastSpeed = mySpeed;
lastBearing = myBearing;
lastLatitude = latitude;
lastLongitude = longitude;
logLocations(location);
Log.i("Location Logged", "With Good Sats");
}
}
}
Log.i("END OF CYCLE", "END OF CYCLE");
}
private void logLocations(Location location)
{
String myprovider = "false";
String temp = timestampFormat.format(location.getTime());
MySQLiteHelper dbHelper = new MySQLiteHelper(getApplicationContext());
try
{
dbHelper.createEntry(latitude, longitude, allschemes, temp, mySpeed, myAccuracy, myBearing, myprovider, satellites);
}
catch (Exception e)
{
e.printStackTrace();
}
CheckAutoArrive(String.valueOf(latitude), String.valueOf(longitude));
}
This is the part of the code I think might be needed. I am leaving all the filtering code there along with the math to compute Earth's radius given the latitude and the distance between 2 points on the map. Feel free to use that if you need it.
In connection to the fact that the Compass app can actually make the system get satellites while my app cannot. Is there a way to actually force read the location services? Is it possible that the Fused Location actually uses GPS but the Location Manager does not know it?
Finally I would like to mention that the application has been tested in other devices (phones, not tablets) with different versions of Android and seems to be working properly.
Any ideas would be more than welcome. And of course go ahead and ask anything I might have forgotten to mention.
EDIT : My actual questions were hidden in the text so to lay them out:
1) Are the Location data we get from Fused Location and the rest of the GPS data we can, seemingly, only get from the Location Manager in sync or is there the possibility to get a Location but wrong number of locked satellites for the particular point?
2) What could be the reason behind the weird behavior where the application cannot get a lock to satellites but if the lock comes from another application it seems to be used properly by the application? To make this even weirder this happens to a Nexus 7 (Android 6.0.1) but not to other devices tested with different Android versions.