Tutorial Android #17 – Integrando o GPS

Padrão

Olá pessoal! Depois de fazer nosso aplicativo de lista de restaurantes buscar tweets, vamos agora utilizar outro recurso presente nos smartphones: o GPS. No tutorial de hoje, faremos com que, no momento em que o restaurante for cadastrado, ele recolha a atual localização (contando que o cadastro esteja sendo feito no próprio restaurante) e salve junto ao registro daquele restaurante. Interessante, não?

Pra começar, vamos adicionar os dados de latitude e longitude em nosso modelo de dados. Primeiramente, modifique o método onCreate da classe GerenciadorRestaurantes.

@Override
public void onCreate(SQLiteDatabase db) {
	db.execSQL("CREATE TABLE restaurantes (_id INTEGER PRIMARY KEY AUTOINCREMENT," +
			" nome TEXT, endereco TEXT, tipo TEXT, anotacoes TEXT, twitter TEXT," +
			" latitude REAL, longitude REAL);");
}

Tal modificação exige que atualizemos a versão do schema do banco de dados:

private static final int VERSAO_SCHEMA = 3;

Na hora de modificarmos o método onUpdate() devemos ficar atentos. Precisamos prepará-lo para atualizar bancos tanto com o schema 1 quanto com o schema 2.

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
	if (oldVersion < 2) {
		db.execSQL("ALTER TABLE restaurantes ADD COLUMN twitter TEXT");
	}

	if (oldVersion < 3) {
		db.execSQL("ALTER TABLE restaurantes ADD COLUMN latitude REAL");
		db.execSQL("ALTER TABLE restaurantes ADD COLUMN longitude REAL");
	}
}

Precisaremos atualizar também os métodos obterTodos() e obterPorId() para abranger também os novos campos.

public Cursor obterTodos(String ordenacao) {
	return getReadableDatabase().rawQuery("select _id, nome, endereco, tipo, " +
			"anotacoes, twitter, latitude, longitude FROM restaurantes ORDER BY " +
			ordenacao, null);
}

public Cursor obterPorId(String id) {
	String[] argumentos = {id};

	return getReadableDatabase().rawQuery(
			"SELECT _id, nome, endereco, tipo, anotacoes, twitter, latitude," +
			" longitude FROM restaurantes WHERE _id = ?", argumentos);
}

Como não vamos inserir diretamente as coordenadas de latitude e longitude diretamente (pedir pro usuário digitar é sacanagem, né?), não precisaremos nos preocupar com os métodos inserir() e atualizar(). Criaremos um método atualizarLocalizacao().

public void atualizarLocalizacao(String id, double latitude, double longitude) {
	ContentValues cv = new ContentValues();
	String[] args = {id};

	cv.put("latitude", latitude);
	cv.put("longitude", longitude);

	getWritableDatabase().update("restaurantes", cv, "_ID = ?", args);
}

O método obviamente só funcionará para restaurantes que já existam no banco de dados, restrições que iremos aplicar através da interface. Por fim, vamos criar os dois métodos para obter os valores de latitude e longitude de um Cursor lido do banco.

public double obterLatitude(Cursor c) {
	return c.getDouble(6);
}

public double obterLongitude(Cursor c) {
	return c.getDouble(7);
}

Precisamos agora adicionar um lugar para exibir as coordenadas GPS na tela. Na nossa interface, uma porção considerável da tela é ocupada pelo botão Salvar. Na maioria das interfaces, este botão não está presente. Assim, temos duas abordagens para resolver a situação:

  • Adicionar uma opção no menu para salvar ou;
  • Salvar automaticamente quando a Activity passar para o estado de pausa

No caso, iremos utilizar a segunda abordagem, para salvar sempre que o usuário pressionar a tecla voltar ou home. Para isso, precisaremos nos desfazer de todas as referências ao botão salvar na classe FormularioDetalhes. Para começar, vamos converter o objeto onSave para um método chamado salvar().

private void salvar() {
	String tipo = null;

	switch (tipos.getCheckedRadioButtonId()) {
	case R.id.rodizio:
		tipo = "rodizio";
		break;
	case R.id.fast_food:
		tipo = "fast_food";
		break;
	case R.id.a_domicilio:
		tipo = "a_domicilio";
		break;
	}

	if (idRestaurante == null) {
		gerenciador.inserir(nome.getText().toString(),
				endereco.getText().toString(),
				tipo, anotacoes.getText().toString(),
				twitter.getText().toString());
	} else {
		gerenciador.atualizar(idRestaurante,
				nome.getText().toString(),
				endereco.getText().toString(),
				tipo, anotacoes.getText().toString(),
				twitter.getText().toString());
	}

	finish();
}

Remova as linhas que fazem referência ao botão no método onCreate() e implemente o método onPause() que faz a chamada ao método salvar().

@Override
public void onPause() {
	salvar();

	super.onPause();
}

O próximo passo é modificar o layout do formulário, removendo o botão salvar e adicionando a opção de menu para obter a localização do GPS. Primeiramente, vamos modificar o layout para o modo retrato (res/layout/form_detalhes.xml).

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:stretchColumns="1" >
    <TableRow>
        <TextView android:text="Nome:"/>
        <EditText android:id="@+id/nome"/>
    </TableRow>
    <TableRow>
    	<TextView android:text="Endereço:"/>
    	<EditText android:id="@+id/end"/>
    </TableRow>
    <TableRow>
    	<TextView android:text="Tipo:"/>
    	<RadioGroup android:id="@+id/tipos">
    		<RadioButton android:id="@+id/rodizio"
       			android:text="Rodízio"/>
    		<RadioButton android:id="@+id/fast_food"
        		android:text="Fast Food"/>
        	<RadioButton android:id="@+id/a_domicilio"
            	android:text="A Domicílio"/>
   		</RadioGroup>
	</TableRow>
	<TableRow>
	    <TextView android:text="Localização " />
	    <TextView android:id="@+id/localizacao" android:text="(não atribuída)" />
	</TableRow>
    <EditText android:id="@+id/anotacoes"
    	android:singleLine="false"
    	android:gravity="top"
    	android:lines="2"
    	android:scrollHorizontally="false"
    	android:maxLines="2"
    	android:maxWidth="200sp"
    	android:hint="Anotações"
    	android:layout_marginTop="4dip"/>
    <EditText android:id="@+id/twitter"
        android:layout_span="2"
        android:hint="Conta do Twitter" />    
</TableLayout>

E em seguida o layout para a versão paisagem (res/layout-land/form_detalhes.xml).

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:stretchColumns="2" >
    <TableRow>
        <TextView android:text="Nome:"/>
        <EditText android:id="@+id/nome"
            android:layout_span="2" />
    </TableRow>
    <TableRow>
    	<TextView android:text="Endereço:"/>
    	<EditText android:id="@+id/end"
    	    android:layout_span="2" />
    </TableRow>
    <TableRow>
    	<TextView android:text="Tipo:"/>
    	<RadioGroup android:id="@+id/tipos">
    		<RadioButton android:id="@+id/rodizio"
       			android:text="Rodízio"/>
    		<RadioButton android:id="@+id/fast_food"
        		android:text="Fast Food"/>
        	<RadioButton android:id="@+id/a_domicilio"
            	android:text="A Domicílio"/>
   		</RadioGroup>   		
   		<LinearLayout 
   		    android:layout_width="fill_parent"
   		    android:layout_height="fill_parent"
   		    android:orientation="vertical">
   		    <EditText android:id="@+id/anotacoes"
    			android:singleLine="false"
	    		android:gravity="top"
    			android:lines="4"
    			android:scrollHorizontally="false"
    			android:maxLines="4"
    			android:maxWidth="140sp"
    			android:layout_width="fill_parent"
    			android:layout_height="wrap_content"
    			android:hint="Anotações" />
   		    <EditText android:id="@+id/twitter"
   		        android:layout_width="fill_parent"
   		        android:layout_height="wrap_content"
   		        android:hint="Conta do Twitter"/>
   		    <LinearLayout
   		        android:layout_width="wrap_content"
   		        android:layout_height="wrap_content"
   		        android:orientation="horizontal">
   		        <TextView android:text="Localização "
   		            android:layout_width="wrap_content"
   		            android:layout_height="wrap_content"/>
   		        <TextView android:id="@+id/localizacao"
   		            android:text="(não atribuída)"
   		            android:layout_width="wrap_content"
   		            android:layout_height="wrap_content"/>
   		    </LinearLayout>
   		</LinearLayout>
	</TableRow>    
</TableLayout>

Na classe FormularioDetalhes, adicione o atributo localizacao.

TextView localizacao = null;

No método onCreate(), adicione a linha seguinte para obter o item do formulário.

localizacao = (TextView) findViewById(R.id.localizacao);

Em seguida, no método carregar(), obtemos os valores do GerenciadorRestaurantes e o colocamos no formulário.

private void carregar() {
	Cursor c = gerenciador.obterPorId(idRestaurante);

	c.moveToFirst();
	nome.setText(gerenciador.obterNome(c));
	endereco.setText(gerenciador.obterEndereco(c));
	anotacoes.setText(gerenciador.obterAnotacoes(c));
	twitter.setText(gerenciador.obterTwitter(c));

	if (gerenciador.obterTipo(c).equals("rodizio")) {
		tipos.check(R.id.rodizio);
	} else if (gerenciador.obterTipo(c).equals("fast_food")) {
		tipos.check(R.id.fast_food);
	} else {
		tipos.check(R.id.a_domicilio);
	}

	localizacao.setText(String.valueOf(gerenciador.obterLatitude(c)) +
			", " + String.valueOf(gerenciador.obterLongitude(c)));

	c.close();
}

Também precisamos adicionar no menu a opção para obter a localização. Modifique o arquivo de menu em res/menu/opcao_detalhes.xml.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
	<item android:id="@+id/twitter"
	    android:title="Timeline Twitter"
	    android:icon="@drawable/twitter"/>
	<item android:id="@+id/localizacao"
	    android:title="Salvar Localização"
	    android:icon="@drawable/gps"/>
</menu>

O ícone pode ser encontrado lá na pasta do SDK sob o nome de ic_menu_compass.png (que eu renomeei para gps.png). Em seguida, vamos adicionar a permissão de acessar a localização, através do arquivo AndroidManifest.xml. Adicione a seguinte linha junto às outras permissões:

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

O próximo passo é obter a localização de fato. Primeiramente, adicione o atributo locationManager na classe FormularioDetalhes:

LocationManager locationManager = null;

Em seguida, adicione a sua localização no método onCreate():

locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);

Agora, precisamos fazer a chamada ao método requestLocationUpdates() da classe LocationManager para pedir a localização quando o usuário selecionar o botão no menu. Dessa forma, modifique o método onOptionsItemSelected():

@Override
public boolean onOptionsItemSelected(MenuItem item) {
	if (item.getItemId() == R.id.twitter) {
		if (redeDisponivel()) {
			Intent i = new Intent(this, TwitterActivity.class);
			i.putExtra(TwitterActivity.PERFIL, twitter.getText().toString());
			startActivity(i);
		} else {
			Toast.makeText(this, "Conexão com a Internet indisponível", Toast.LENGTH_LONG).show();
		}

		return true;
	} else if (item.getItemId() == R.id.localizacao) {
		locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
				0, 0, onLocationChange);
	}

	return super.onOptionsItemSelected(item);
}

Como pode ser percebido, adicionamos um listener para obter o resultado a chamada. Por isso, vamos implementar o objeto onLocationChange como um atributo da classe FormularioDetalhes:

LocationListener onLocationChange = new LocationListener() {

	public void onLocationChanged(Location location) {
		gerenciador.atualizarLocalizacao(idRestaurante, location.getLatitude(),
				location.getLongitude());
		localizacao.setText(String.valueOf(location.getLatitude()) + ", " +
				String.valueOf(location.getLongitude()));
		locationManager.removeUpdates(onLocationChange);

		Toast.makeText(FormularioDetalhes.this, "Localização salva", Toast.LENGTH_LONG);
	}

	public void onProviderDisabled(String provider) {
		// Requerido pela interface. Não utilizado
	}

	public void onProviderEnabled(String provider) {
		// Requerido pela interface. Não utilizado
	}

	public void onStatusChanged(String provider, int status, Bundle extras) {
		// Requerido pela interface. Não utilizado
	}
};

No caso, obtemos os dados do GPS, atualizamos o banco e a interface e exibimos uma mensagem para o usuário. Porém, pode ocorrer do usuário sair da tela enquanto a requisição estiver sendo processada. Neste caso, é sensato cancelá-la para evitar problemas. Assim, atualize o método onPause():

@Override
public void onPause() {
	salvar();
	locationManager.removeUpdates(onLocationChange);

	super.onPause();
}

Por fim, precisamos controlar para somente exibir a opção de obter os dados do GPS se o restaurante já tiver sido salvo no banco de dados. Adicione a implementação do método onPrepareOptionsMenu():

@Override
public boolean onPrepareOptionsMenu(Menu menu) {
	if (idRestaurante == null) {
		menu.findItem(R.id.localizacao).setEnabled(false);
	}

	return super.onPrepareOptionsMenu(menu);
}

Prontinho! Vamos testar?

Os dois primeiros screenshots foram feitos no emulador e o último foi feito no usando um celular real (LG Optimus One). Caso você não tenha um celular real para testar, você pode emular coordenadas GPS conectando via telnet no localhost porta 5554. No console que aparecer, digite o comando:

geo fix <latitude> <longitude>

Onde <latitude> e <longitude> devem ser os valores desejados (você consegue pegar os valores de um lugar facilmente usando o Google Maps).

Como de costume, para baixar o projeto, basta clicar aqui.

O projeto possui um pequeno “bug” caso você entre na tela de adicionar novo restaurante, não digite nada e volte à listagem. No próximo post vamos corrigi-lo. 😉

2 comentários sobre “Tutorial Android #17 – Integrando o GPS

  1. daniel

    Chefe como fazer a coleta de dados gps? Tipo gravar a posição em um arquivo txt para depois fazer a media e obter precisão?

Deixe uma resposta