Estrategias de Localización en Android

Determinar cuál es el proveedor de localización idóneo para nuestra aplicación puede resultar una tarea compleja. Además esta decisión puede variar con el tiempo según el usuario cambie de posición, o desactivar alguno de los proveedores. A continuación se plantean tres posibles estrategias:
 

Usar siempre el mismo tipo de proveedor

Los dos proveedores de localización disponibles en Android tienen características muy diferentes. Muchas aplicaciones tienen algún tipo de requisito que hace que podamos decantarnos de entrada por un sistema en concreto. Veamos algunos ejemplos.

Usaremos GPS si:

  • La aplicación requiere una precisión inferior a 10 m.
  • Está pensada para su uso al aire libre (ej. senderismo).

Usaremos localización por redes si:

  • El consumo de batería es un problema.
  • Está pensada para su uso en el interior de edificios (visita museo).

Una vez decidido, usaremos las constantes  GPS_PROVIDER  o NETWORK_PROVIDER de la clase LocationManager para indicar el proveedor deseado.

Existe un tercer tipo de proveedor identificado con la constante PASSIVE _PROVIDER. Puedes usarlo si quieres observar pasivamente actualizaciones de ubicación provocadas por otras aplicaciones, pero no quieres que se lancen nuevas lecturas de posición. De esta manera no provocamos consumo de energía adicional.
 

El mejor proveedor según un determinado criterio

Como vimos en el apartado anterior, el API de localización de Android nos proporciona la clase Criteria para seleccionar un proveedor de localización según el criterio indicado. Recordemos el código utilizado:

Criteria criterio = new Criteria();
criterio.setCostAllowed(false);
criterio.setAltitudeRequired(false);
criterio.setAccuracy(Criteria.ACCURACY_FINE);
proveedor = manejadorLoc.getBestProvider(criterio, true);

Los proveedores pueden variar de estado, por lo que podría ser interesante consultar cual es el mejor proveedor cada vez que cambie su estado.

Usar los dos proveedores en paralelo

Otra alternativa podría ser programar actualizaciones de los dos proveedores de localización disponibles. Luego podríamos seleccionar la mejor localización entre las suministradas. Para estudiar esta alternativa realiza el siguiente ejercicio:

video[Tutorial Estrategias de localizacion en Android.

Ejercicio: Añadiendo localización en Mis Lugares.

1.     Añade en AndroidManifest.xml de Mis Lugares los siguientes permisos:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

2.     Crea a la clase CasosUsoLocalizacion con los siguientes atributos y su inicialización:

public class CasosUsoLocalizacion {
   private static final String TAG = "MisLugares";
   private Activity actividad;
   private int codigoPermiso;
   private LocationManager manejadorLoc;
   private Location mejorLoc;
   private GeoPunto posicionActual; 
   private AdaptadorLugares adaptador;

   public CasosUsoLocalizacion(Activity actividad, int codigoPermiso) {
      this.actividad = actividad;
      this.codigoPermiso = codigoPermiso;
      manejadorLoc = (LocationManager) actividad.getSystemService(
      														 LOCATION_SERVICE);
      posicionActual = ((Aplicacion) actividad.getApplication())
                                                             .posicionActual;
      adaptador = ((Aplicacion) actividad.getApplication()).adaptador;
      ultimaLocalizazion();
   }
}  
class CasosUsoLocalizacion(val actividad: Activity, 
                           val codigoPermiso: Int) : LocationListener {
   val TAG = "MisLugares"
   val manejadorLoc = actividad.getSystemService(
                    AppCompatActivity.LOCATION_SERVICE) as LocationManager
   var mejorLoc: Location? = null
   val posicionActual = (actividad.application as Aplicacion).posicionActual
   val adaptador = (actividad.application as Aplicacion).adaptador

   init {
      ultimaLocalizazion()
   }
} 

La variable manejadorLoc nos permite acceder a los servicios de localización de Android. La variable mejorLoc, de tipo Location, almacena la mejor localización actual. La variable posicionActual almacena la misma información, pero en formato GeoPunto. Estará almacenada en Aplicacion para que sea accesible desde cualquier parte de la aplicación. Es necesario tener la información en dos formatos, ya que la primera variable es usada para disponer de la fecha de obtención o proveedor que nos la ha dado y la segunda al ser el formato usado en el resto de la aplicación. Finalmente obtenemos una referencia al adaptador del RecyclerView para poder actualizarlo cuando haya cambios de localización.
 

3.    En la clase Aplicacion crea la variable posicionActual:

public GeoPunto posicionActual = new GeoPunto(0.0, 0.0);  
val posicionActual = GeoPunto.SIN_POSICION.copy() 

Los valores (0, 0) representa que no se dispone de localización.

4.     En MainActivity, añade:

private static final int SOLICITUD_PERMISO_LOCALIZACION = 1;
private CasosUsoLocalizacion usoLocalizacion;


@Override protected void onCreate(Bundle savedInstanceState) {
   …
   usoLocalizacion = new CasosUsoLocalizacion(this,
                                          SOLICITUD_PERMISO_LOCALIZACION);
} 
val SOLICITUD_PERMISO_LOCALIZACION = 1
val usoLocalizacion by lazy { 
   CasosUsoLocalizacion(this, SOLICITUD_PERMISO_LOCALIZACION) }  

5.     Vamos a verificar varias veces si el usuario nos ha dado permiso de localización. Para ello añade en CasosUsoLocalizacion el siguiente código:

public boolean hayPermisoLocalizacion() {
   return (ActivityCompat.checkSelfPermission(
           actividad, Manifest.permission.ACCESS_FINE_LOCATION)
           == PackageManager.PERMISSION_GRANTED);
}  
fun hayPermisoLocalizacion() = (ActivityCompat.checkSelfPermission(
   actividad, Manifest.permission.ACCESS_FINE_LOCATION)
        == PackageManager.PERMISSION_GRANTED)  
6.    Añade el siguiente método:
 
void ultimaLocalizazion() {
  if (hayPermisoLocalizacion()) {
    if (manejadorLoc.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
         actualizaMejorLocaliz(manejadorLoc.getLastKnownLocation(
             LocationManager.GPS_PROVIDER));
    }
    if (manejadorLoc.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){
         actualizaMejorLocaliz(manejadorLoc.getLastKnownLocation(
             LocationManager.NETWORK_PROVIDER));
    } 
  } else  {
         solicitarPermiso(Manifest.permission.ACCESS_FINE_LOCATION,
             "Sin el permiso localización no puedo mostrar la distancia"+
             " a los lugares.", codigoPermiso, actividad);
    
  }
} 
@SuppressLint("MissingPermission")
fun ultimaLocalizazion() {
  if (hayPermisoLocalizacion()) {
    if (manejadorLoc.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
         actualizaMejorLocaliz( manejadorLoc.getLastKnownLocation(
               LocationManager.GPS_PROVIDER))
    }
    if (manejadorLoc.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){
         actualizaMejorLocaliz( manejadorLoc.getLastKnownLocation(
               LocationManager.NETWORK_PROVIDER))
    }
  } else {
         solicitarPermiso(Manifest.permission.ACCESS_FINE_LOCATION,
            "Sin el permiso localización no puedo mostrar la distancia" +
            " a los lugares.", codigoPermiso, actividad)
  }
} 
Antes de obtener una localización se debe verificar que tenemos permiso para hacerlo. Para más información consultar Permisos en Android 6 Marshmallow. En caso de tener permiso buscamos la última localización disponible. Usamos el método getLastKnownLocation() aplicado a los dos proveedores que vamos a utilizar. El método actualizaMejorLocaliz() se explicará más adelante. Si no disponemos del permiso, lo solicitamos al usuario.

7.    La función getLastKnownLocation() estará marcado con el error “Call requiered permision …”, avisándonos que hemos de comprobar que tenemos permiso antes de hacer la llamada. Realmente lo hemos hecho. Para desactivar la advertencia añade @SuppressLint("MissingPermission") antes de la función. 

8.    Copia a esta clase el método solicitarPermiso() del ejercicio Permisos en Android 6 Marshmallow.

9.     Una vez conteste el usuario se llamará a onRequestPermissionsResult de  MainActivity. Añade a la clase el siguiente método:

@Override public void onRequestPermissionsResult(int requestCode,
                               String[] permissions, int[] grantResults) {
   super.onRequestPermissionsResult(requestCode,permissions,grantResults);
   if (requestCode == SOLICITUD_PERMISO_LOCALIZACION 
       && grantResults.length == 1
       && grantResults[0] == PackageManager.PERMISSION_GRANTED) 
      usoLocalizacion.permisoConcedido();
} 
override fun onRequestPermissionsResult(requestCode: Int,
                 permissions: Array<String>, grantResults: IntArray ) {
                 super.onRequestPermissionsResult(requestCode,permissions,grantResults)
   if (requestCode == SOLICITUD_PERMISO_LOCALIZACION
       && grantResults.size == 1 
       && grantResults[0] == PackageManager.PERMISSION_GRANTED) 
     usoLocalizacion.permisoConcedido() 
} 

Si el usuario contesta afirmativamente a la solicitud de permiso llamamos a un caso de uso para que se actúe en consecuencia.

10.    Añade el siguiente caso de uso en CasosUsoLocalizacion:

public void permisoConcedido() {
   ultimaLocalizazion();
   activarProveedores();
   adaptador.notifyDataSetChanged();
}

private void activarProveedores() {
  if (hayPermisoLocalizacion()) {
    if (manejadorLoc.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
         manejadorLoc.requestLocationUpdates(LocationManager.GPS_PROVIDER,
              20 * 1000, 5, this);
    }
    if (manejadorLoc.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){
              manejadorLoc.requestLocationUpdates(LocationManager
              .NETWORK_PROVIDER, 10 * 1000, 10, this);
      }
  } else {
     solicitarPermiso(Manifest.permission.ACCESS_FINE_LOCATION,
              "Sin el permiso localización no puedo mostrar la distancia"+
              " a los lugares.", codigoPermiso, actividad);
  }
} 
fun permisoConcedido() {
   ultimaLocalizazion()
   activarProveedores()
   adaptador.notifyDataSetChanged()
}
  @SuppressLint("MissingPermission")
private fun activarProveedores() {
  if (hayPermisoLocalizacion()) {
    if (manejadorLoc.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
         manejadorLoc.requestLocationUpdates(
            LocationManager.GPS_PROVIDER,20 * 1000, 5F, this )
    }
    if (manejadorLoc.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){
         manejadorLoc.requestLocationUpdates(
            LocationManager.NETWORK_PROVIDER, 10 * 1000, 10F, this )
      }
  } else {
    solicitarPermiso(Manifest.permission.ACCESS_FINE_LOCATION,
         "Sin el permiso localización no puedo mostrar la distancia" +
         " a los lugares.", codigoPermiso, actividad )
  }
}  

La primera función se llama cuando nos conceden permiso de localización. Miramos si ya disponen de una última posición conocida, llamamos a la segunda función que activa los eventos de localización y refrescamos el RecyclerView.
La segunda función hace que nuestra clase (this) sea informada con cada actualización del proveedor de localización. Lo hacemos para el proveedor basado en GPS (cada 10s y si hay un cambio de más de 5m) y con el basado en redes (cada 20s y si hay un cambio de más de 10m).
 

11.    Para recibir los eventos de localización haz que la clase CasosUsoLocalizacion implemente la interfaz  LocationListener y añade las siguientes funciones:

p@Override public void onLocationChanged(Location location) {
   Log.d(TAG, "Nueva localización: "+location);
   actualizaMejorLocaliz(location);
   adaptador.notifyDataSetChanged();
}
@Override public void onProviderDisabled(String proveedor) {
   Log.d(TAG, "Se deshabilita: "+proveedor);
   activarProveedores();
}
@Override public void onProviderEnabled(String proveedor) {
   Log.d(TAG, "Se habilita: "+proveedor);
   activarProveedores();
}
@Override 
public void onStatusChanged(String proveedor, int estado, Bundle extras) {
   Log.d(TAG, "Cambia estado: "+proveedor);
   activarProveedores();
}  
override fun onLocationChanged(location: Location) {
   Log.d(TAG, "Nueva localización: $location")
   actualizaMejorLocaliz(location)
   adaptador.notifyDataSetChanged()
}
override fun onProviderDisabled(proveedor: String) {
   Log.d(TAG, "Se deshabilita: $proveedor")
   activarProveedores()
}
override fun onProviderEnabled(proveedor: String) {
   Log.d(TAG, "Se habilita: $proveedor")
   activarProveedores()
}
override fun onStatusChanged(proveedor:String, estado:Int, extras:Bundle){
   Log.d(TAG, "Cambia estado: $proveedor")
   activarProveedores()
}  

Las acciones a realizar resultan evidentes: cuando la actualizamos cambie la posición y cuando cambie el estado tratamos de activar nuevos proveedores.

12.    Ahora añade la siguiente función:

private static final long DOS_MINUTOS = 2 * 60 * 1000;

private void actualizaMejorLocaliz(Location localiz) {
   if (localiz != null && (mejorLoc == null
         || localiz.getAccuracy() < 2*mejorLoc.getAccuracy()
         || localiz.getTime() - mejorLoc.getTime() > DOS_MINUTOS)) {
      Log.d(TAG, "Nueva mejor localización");
      mejorLoc = localiz;
      posicionActual.setLatitud(localiz.getLatitude());
      posicionActual.setLongitud(localiz.getLongitude());
   }
}  
val DOS_MINUTOS:Long = (2 * 60 * 1000)

private fun actualizaMejorLocaliz(loc: Location?) {
   if (localiz != null && (mejorLoc == null
              || loc.accuracy < 2 * mejorLoc!!.getAccuracy()
              || loc.time - mejorLoc!!.getTime() > DOS_MINUTOS)) {
      Log.d(TAG, "Nueva mejor localización")
      mejorLoc = loc
      posicionActual.latitud = loc.latitude
      posicionActual.longitud = loc.longitude
   }
}  

En la variable mejorLoc almacenamos la mejor localización. Esta solo será actualizada con la nueva propuesta si: todavía no ha sido inicializada; o la nueva localización tiene una precisión aceptable (al menos la mitad que la actual); o la diferencia de tiempo es superior a dos minutos. Una vez comprobado si se cumple alguna de las tres condiciones, actualizamos mejorLocaliz y copiamos la posición en posicionActual.

13.   Si dejáramos activos los escuchadores de eventos mientras la aplicación está en segundo plano, podríamos quedarnos sin batería. Para evitar esta situación añade en MainActivity:

@Override protected void onResume() {
  super.onResume();
  usoLocalizacion.activar();
}

@Override protected void onPause() {
   super.onPause();
   usoLocalizacion.desactivar();
}  
override fun onResume() {
   super.onResume()
   usoLocalizacion.activar()
}

override fun onPause() {
   super.onPause()
   usoLocalizacion.desactivar()
}  

14.   Añade los dos nuevos casos de uso:

public void activar() {
   if (hayPermisoLocalizacion()) activarProveedores();
}

public void desactivar() {
   if (hayPermisoLocalizacion()) manejadorLoc.removeUpdates(this);
} 
fun activar() {
   if (hayPermisoLocalizacion()) activarProveedores()
}

fun desactivar() {
   if (hayPermisoLocalizacion()) manejadorLoc.removeUpdates(this)
} 

15.   Una vez que ya disponemos de la posición actual, vamos a tratar de mostrar la distancia a cada lugar en el RecyclerView de la actividad principal. Abre el layout elemento_lista.xmly añade al final del ConstraintLayout la nueva vista que se indica:

  …
<TextView
    android:id="@+id/distancia"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toRightOf="@id/valoracion"
    android:gravity="right"
    android:text="... Km"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/direccion" />
 
</…ConstraintLayout>
 

16.    Para Java en AdaptadorLugares dentro de la clase ViewHolder la siguiente variable:

public TextView distancia; 

En el constructor de ViewHolder añade al final

distancia = itemView.distancia; 

17.   Dentro del método personaliza()añade el siguiente código al final:

GeoPunto pos=((Aplicacion) itemView.getContext().getApplicationContext())
                                                             .posicionActual;
if (pos.equals(GeoPunto.SIN_POSICION) ||
                      lugar.getPosicion().equals(GeoPunto.SIN_POSICION)) {
   distancia.setText("... Km");
} else {
   int d=(int) pos.distancia(lugar.getPosicion());
   if (d < 2000) distancia.setText(d + " m");
   else          distancia.setText(d / 1000 + " Km");
}  

val pos=(itemView.context.applicationContext as Aplicacion).posicionActual
if (pos.equals(GeoPunto.SIN_POSICION) ||
                           lugar.posicion.equals(GeoPunto.SIN_POSICION)) {
   distancia.text = "... Km"
} else {
   val d = pos.distancia(lugar.posicion).toInt()
   distancia.text = if (d < 2000) "$d m"
                    else          "${(d / 1000)} Km"

} 

Nos aseguramos de que la posición actual y la del lugar existen. Luego calculamos la distancia en la variable d. Si la distancia es inferior a 2000, se muestra en metros; en caso contrario se muestra en Km.

18.   Ejecuta la aplicación y verifica el resultado obtenido:

Nota: En este ejercicio se ha decidido extraer todo el código que nos permite mantener la localización del dispositivo a una nueva clase. Se podría haber integrado dentro de MainActivity, como se hizo en el ejercicio “La API de localización de Android”. Hacerlo de esta forma divide las responsabilidades entre las dos clase, lo que las hace más fáciles de entender y de mantener. Además el código es más reutilizable, si en un futuro queremos que otra actividad acceda a la localización, podremos usar la clase CasosUsoLocalizacion sin tener que cambiarla.