Buena forma de obtener la ubicación del usuario en Android.
El problema:
Obtener la ubicación actual del usuario dentro de un umbral lo antes posible y al mismo tiempo conservar la batería.
Por qué el problema es un problema:
En primer lugar, Android tiene dos proveedores; Red y GPS. A veces la red es mejor y otras veces el GPS es mejor.
Por "mejor" me refiero a la relación entre velocidad y precisión.
Estoy dispuesto a sacrificar unos metros de precisión si puedo obtener la ubicación casi al instante y sin encender el GPS.
En segundo lugar, si solicita actualizaciones para cambios de ubicación, no se envía nada si la ubicación actual es estable.
Google tiene un ejemplo de cómo determinar la "mejor" ubicación aquí: http://developer.android.com/guide/topics/location/obtaining-user-location.html#BestEstimate
Pero creo que no es tan bueno como debería /podría ser.
Estoy un poco confundido por qué Google no tiene una API normalizada para la ubicación, al desarrollador no debería importarle de dónde es la ubicación, solo debe especificar lo que desea y el teléfono debe elegir por usted.
Con qué necesito ayuda:
Necesito encontrar una buena manera de determinar la "mejor" ubicación, tal vez mediante alguna heurística o tal vez mediante alguna biblioteca de terceros.
¡Esto no significa determinar cuál es el mejor proveedor!
Probablemente utilizaré todos los proveedores y elegiré los mejores.
Antecedentes de la aplicación:
La aplicación recopilará la ubicación del usuario en un intervalo fijo (digamos cada 10 minutos aproximadamente) y la enviará a un servidor.
La aplicación debe conservar la mayor cantidad de batería posible y la ubicación debe tener una precisión de X (50-100?) metros.
El objetivo es poder trazar la ruta del usuario durante el día en un mapa, por lo que necesito suficiente precisión para ello.
Varios:
¿Cuáles cree que son valores razonables sobre las precisiones deseadas y aceptadas?
He estado usando 100 m según lo aceptado y 30 m según lo deseado, ¿es mucho pedir?
Me gustaría poder trazar la ruta del usuario en un mapa más adelante.
¿Son mejores 100 m para lo deseado y 500 m para lo aceptado?
Además, en este momento tengo el GPS encendido durante un máximo de 60 segundos por actualización de ubicación. ¿Es esto demasiado corto para obtener una ubicación si estás en interiores con una precisión de quizás 200 m?
Este es mi código actual, se agradece cualquier comentario (aparte de la falta de verificación de errores, que es TODO):
protected void runTask() {
final LocationManager locationManager = (LocationManager) context
.getSystemService(Context.LOCATION_SERVICE);
updateBestLocation(locationManager
.getLastKnownLocation(LocationManager.GPS_PROVIDER));
updateBestLocation(locationManager
.getLastKnownLocation(LocationManager.NETWORK_PROVIDER));
if (getLocationQuality(bestLocation) != LocationQuality.GOOD) {
Looper.prepare();
setLooper(Looper.myLooper());
// Define a listener that responds to location updates
LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
updateBestLocation(location);
if (getLocationQuality(bestLocation) != LocationQuality.GOOD)
return;
// We're done
Looper l = getLooper();
if (l != null) l.quit();
}
public void onProviderEnabled(String provider) {}
public void onProviderDisabled(String provider) {}
public void onStatusChanged(String provider, int status,
Bundle extras) {
// TODO Auto-generated method stub
Log.i("LocationCollector", "Fail");
Looper l = getLooper();
if (l != null) l.quit();
}
};
// Register the listener with the Location Manager to receive
// location updates
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000, 1, locationListener,
Looper.myLooper());
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER, 1000, 1,
locationListener, Looper.myLooper());
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
Looper l = getLooper();
if (l != null) l.quit();
// Log.i("LocationCollector",
// "Stopping collector due to timeout");
}
}, MAX_POLLING_TIME);
Looper.loop();
t.cancel();
locationManager.removeUpdates(locationListener);
setLooper(null);
}
if (getLocationQuality(bestLocation) != LocationQuality.BAD)
sendUpdate(locationToString(bestLocation));
else Log.w("LocationCollector", "Failed to get a location");
}
private enum LocationQuality {
BAD, ACCEPTED, GOOD;
public String toString() {
if (this == GOOD) return "Good";
else if (this == ACCEPTED) return "Accepted";
else return "Bad";
}
}
private LocationQuality getLocationQuality(Location location) {
if (location == null) return LocationQuality.BAD;
if (!location.hasAccuracy()) return LocationQuality.BAD;
long currentTime = System.currentTimeMillis();
if (currentTime - location.getTime() < MAX_AGE
&& location.getAccuracy() <= GOOD_ACCURACY)
return LocationQuality.GOOD;
if (location.getAccuracy() <= ACCEPTED_ACCURACY)
return LocationQuality.ACCEPTED;
return LocationQuality.BAD;
}
private synchronized void updateBestLocation(Location location) {
bestLocation = getBestLocation(location, bestLocation);
}
// Pretty much an unmodified version of googles example
protected Location getBestLocation(Location location,
Location currentBestLocation) {
if (currentBestLocation == null) {
// A new location is always better than no location
return location;
}
if (location == null) return currentBestLocation;
// Check whether the new location fix is newer or older
long timeDelta = location.getTime() - currentBestLocation.getTime();
boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
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 location;
// If the new location is more than two minutes older, it must be
// worse
} else if (isSignificantlyOlder) {
return currentBestLocation;
}
// 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 location;
} else if (isNewer && !isLessAccurate) {
return location;
} else if (isNewer && !isSignificantlyLessAccurate
&& isFromSameProvider) {
return location;
}
return bestLocation;
}
/** Checks whether two providers are the same */
private boolean isSameProvider(String provider1, String provider2) {
if (provider1 == null) {
return provider2 == null;
}
return provider1.equals(provider2);
}
Parece que estamos codificando la misma aplicación ;-)
Aquí está mi implementación actual. Todavía estoy en la fase de prueba beta de mi aplicación de carga de GPS, por lo que puede haber muchas mejoras posibles. pero parece funcionar bastante bien hasta ahora.
/**
* try to get the 'best' location selected from all providers
*/
private Location getBestLocation() {
Location gpslocation = getLocationByProvider(LocationManager.GPS_PROVIDER);
Location networkLocation =
getLocationByProvider(LocationManager.NETWORK_PROVIDER);
// if we have only one location available, the choice is easy
if (gpslocation == null) {
Log.d(TAG, "No GPS Location available.");
return networkLocation;
}
if (networkLocation == null) {
Log.d(TAG, "No Network Location available");
return gpslocation;
}
// a locationupdate is considered 'old' if its older than the configured
// update interval. this means, we didn't get a
// update from this provider since the last check
long old = System.currentTimeMillis() - getGPSCheckMilliSecsFromPrefs();
boolean gpsIsOld = (gpslocation.getTime() < old);
boolean networkIsOld = (networkLocation.getTime() < old);
// gps is current and available, gps is better than network
if (!gpsIsOld) {
Log.d(TAG, "Returning current GPS Location");
return gpslocation;
}
// gps is old, we can't trust it. use network location
if (!networkIsOld) {
Log.d(TAG, "GPS is old, Network is current, returning network");
return networkLocation;
}
// both are old return the newer of those two
if (gpslocation.getTime() > networkLocation.getTime()) {
Log.d(TAG, "Both are old, returning gps(newer)");
return gpslocation;
} else {
Log.d(TAG, "Both are old, returning network(newer)");
return networkLocation;
}
}
/**
* get the last known location from a specific provider (network/gps)
*/
private Location getLocationByProvider(String provider) {
Location location = null;
if (!isProviderSupported(provider)) {
return null;
}
LocationManager locationManager = (LocationManager) getApplicationContext()
.getSystemService(Context.LOCATION_SERVICE);
try {
if (locationManager.isProviderEnabled(provider)) {
location = locationManager.getLastKnownLocation(provider);
}
} catch (IllegalArgumentException e) {
Log.d(TAG, "Cannot acces Provider " + provider);
}
return location;
}
Editar: aquí está la parte que solicita las actualizaciones periódicas de los proveedores de ubicación:
public void startRecording() {
gpsTimer.cancel();
gpsTimer = new Timer();
long checkInterval = getGPSCheckMilliSecsFromPrefs();
long minDistance = getMinDistanceFromPrefs();
// receive updates
LocationManager locationManager = (LocationManager) getApplicationContext()
.getSystemService(Context.LOCATION_SERVICE);
for (String s : locationManager.getAllProviders()) {
locationManager.requestLocationUpdates(s, checkInterval,
minDistance, new LocationListener() {
@Override
public void onStatusChanged(String provider,
int status, Bundle extras) {}
@Override
public void onProviderEnabled(String provider) {}
@Override
public void onProviderDisabled(String provider) {}
@Override
public void onLocationChanged(Location location) {
// if this is a gps location, we can use it
if (location.getProvider().equals(
LocationManager.GPS_PROVIDER)) {
doLocationUpdate(location, true);
}
}
});
// //Toast.makeText(this, "GPS Service STARTED",
// Toast.LENGTH_LONG).show();
gps_recorder_running = true;
}
// start the gps receiver thread
gpsTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Location location = getBestLocation();
doLocationUpdate(location, false);
}
}, 0, checkInterval);
}
public void doLocationUpdate(Location l, boolean force) {
long minDistance = getMinDistanceFromPrefs();
Log.d(TAG, "update received:" + l);
if (l == null) {
Log.d(TAG, "Empty location");
if (force)
Toast.makeText(this, "Current location not available",
Toast.LENGTH_SHORT).show();
return;
}
if (lastLocation != null) {
float distance = l.distanceTo(lastLocation);
Log.d(TAG, "Distance to last: " + distance);
if (l.distanceTo(lastLocation) < minDistance && !force) {
Log.d(TAG, "Position didn't change");
return;
}
if (l.getAccuracy() >= lastLocation.getAccuracy()
&& l.distanceTo(lastLocation) < l.getAccuracy() && !force) {
Log.d(TAG,
"Accuracy got worse and we are still "
+ "within the accuracy range.. Not updating");
return;
}
if (l.getTime() <= lastprovidertimestamp && !force) {
Log.d(TAG, "Timestamp not never than last");
return;
}
}
// upload/store your location here
}
Cosas para considerar:
No solicite actualizaciones de GPS con demasiada frecuencia, ya que agota la batería. Actualmente uso 30 minutos como valor predeterminado para mi aplicación.
agregue una verificación de "distancia mínima hasta la última ubicación conocida". sin esto, sus puntos "saltarán" cuando el GPS no esté disponible y la ubicación esté siendo triangulada desde las torres de telefonía celular. o puede verificar si la nueva ubicación está fuera del valor de precisión de la última ubicación conocida.
Para seleccionar el proveedor de ubicación adecuado para su aplicación, puede utilizar objetos Criteria :
Criteria myCriteria = new Criteria();
myCriteria.setAccuracy(Criteria.ACCURACY_HIGH);
myCriteria.setPowerRequirement(Criteria.POWER_LOW);
// let Android select the right location provider for you
String myProvider = locationManager.getBestProvider(myCriteria, true);
// finally require updates at -at least- the desired rate
long minTimeMillis = 600000; // 600,000 milliseconds make 10 minutes
locationManager.requestLocationUpdates(myProvider,minTimeMillis,0,locationListener);
Lea la documentación de requestLocationUpdates para obtener más detalles sobre cómo se tienen en cuenta los argumentos:
La frecuencia de notificación se puede controlar utilizando los parámetros minTime y minDistance. Si minTime es mayor que 0, LocationManager podría descansar durante minTime milisegundos entre actualizaciones de ubicación para ahorrar energía. Si minDistance es mayor que 0, una ubicación solo se transmitirá si el dispositivo se mueve en metros minDistance. Para recibir notificaciones con la mayor frecuencia posible, establezca ambos parámetros en 0.
Más pensamientos
- Puede monitorear la precisión de los objetos de Ubicación con Location.getAccuracy() , que devuelve la precisión estimada de la posición en metros.
- el
Criteria.ACCURACY_HIGH
criterio debería darte errores por debajo de 100 m, lo cual no es tan bueno como puede ser el GPS, pero se adapta a tus necesidades. - También debe monitorear el estado de su proveedor de ubicación y cambiar a otro proveedor si el usuario no está disponible o lo desactiva.
- El proveedor pasivo también puede ser una buena opción para este tipo de aplicaciones: la idea es utilizar actualizaciones de ubicación cada vez que otra aplicación las solicite y transmitirlas a todo el sistema.