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.
答案 0 :(得分:13)
根据我的理解:
每次客户端设备上的任何相关提供商(WiFi,CellTower,GPS,蓝牙)都有新的读数时,FusedLocationApi会返回一个新位置。该读数与先前的位置估计(可能使用扩展卡尔曼滤波器或类似物)融合。生成的位置更新是多个来源的融合估算,这就是为什么没有来自各个提供商的元数据的原因。
因此,您从API获取的位置数据可能与从 LocationManager 获得的纯GPS读数一致(如果GPS是最新且相关的位置源),但它并不是必须的。因此,从上次纯GPS读数获得的卫星数量可能适用于FusedLocationApi返回的最新位置,也可能不适用。
简而言之:
无法保证从LocationManager获取的位置读数与FusedLocationApi 中的位置同步。
首先:要确定此问题的根本原因,您需要在多个位置使用多个设备进行测试。既然你问了
可能成为奇怪行为背后的原因?
我会抛出一个理论:假设LocationManager和FusedLocationApi完全分开工作,LocationManager可能很难获得修复,因为你只依赖于GPS。 除了GPS之外,请尝试使用NETWORK_PROVIDER来加快首次修复的时间(从而使LocationManager能够使用Assisted GPS)。其他应用程序(如指南针应用程序)几乎肯定会这样做,这可以解释为什么他们得到更快的修复。 注意:开始接收GPS数据后,您当然可以取消注册网络提供商。或者你继续保持它,但只是忽略它的更新。
这是对奇怪行为的一种可能解释。您可能知道位置行为取决于您所在的设备,操作系统,GPS芯片组,固件和位置 - 所以你计划去手动(即不使用FusedLocationApi)你将需要进行相当多的实验。
除了答案之外,让我对你的问题提出一个自以为是的看法(采取一些盐;-):我认为你正在尝试将两种不同用途的东西结合起来案件并不意味着要合并。
获取卫星的数量是完全技术性的。没有最终用户会对这种信息感兴趣,除非您的应用程序教他们关于GNSS。如果您想将其记录为内部分析目的,那么您必须能够处理无法获得此信息的情况。
场景1 :由于某种原因,您决定绝对需要GPS读数的详细(技术)细节。在这种情况下,自己构建逻辑,旧学校方式。即通过LocationManager请求GPS读数(并可能通过使用网络提供商加快此过程),然后将这些东西融合在一起。但是,在这种情况下,无法触及FusedLocationApi 。 即使它现在看起来过时而且神秘,但使用具有DIY融合逻辑的LocationManager仍然对少数用例非常有意义。这就是API仍然存在的原因。
场景2 :您只是希望快速准确地更新客户端的位置。在这种情况下,指定所需的更新频率和准确度,让FusedLocationApi完成其工作。 FusedLocationApi在过去几年已经走过了漫长的道路,现在很有可能在确定如何获取位置信息方面比任何DIY逻辑更快更好。这是因为获取位置信息是一个非常异质的问题,取决于客户端设备(芯片组,固件,操作系统,GPS,WiFi,GSM / LTE,蓝牙等)的功能,与物理环境(WiFi / CellTower)相同/蓝牙信号在附近,内部或外部,晴空或城市峡谷等。 在这种情况下,请勿触摸手动提供商。如果您这样做,请不要期望对各个提供者的读数与融合结果之间的关系做出任何有意义的推断。
两个最后的评论:
Android提供Location.distanceTo()和Location.distanceBetween(),因此您无需在代码中实施Haversine公式。
如果您只需要从FusedLocationApi获得快速可靠的更新,我写了a small utility class, called the LocationAssistant,,它简化了设置并为您完成了大部分繁重工作。