Aufruf eines Webservice

Einleitung

Für die Nutzung von Webservices bietet Android das Paket org.apache.http . In diesem ist unter anderem das Paket org.apache.http.client.methods . Darin befindet sich die Klasse HttpGet . Wie deren Name bereits vermuten lässt, kann damit ein HTTP Get Request gesendet werden. Dazu instanziiert man ein Objekt dieser Klasse und übergibt als Parameter die URL des Webservice. Der folgende Abschnitt zeigt den Aufruf eines Google-Webservice mittels dieser Methode.

Einfache Google-Maps Anfrage

Für dieses Beispiel wird der Geocoding-Service von Google genutzt. Eine Beschreibung des Service kann unter http://code.google.com/intl/de/apis/maps/documentation/geocoding/ gefunden werden. Der Abschnit Geocodierungsanforderungen erklärt das Format, welches die URL für die Anforderung haben muss. Bevor es an die eigentliche Arbeit mit Webservices geht, erstellen wir eine kleine GUI. Diese soll ein Eingabefeld für die zu suchende Adresse, einen Button zum Senden der Anfrage sowie ein Ausgabefeld für die ermittelten Koordinaten besitzen. Die Benutzerschnittstelle könnte wie folgt aussehen:

Die zu diesem Layout gehörige XML-Datei enthält folgenden Inhalt:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:id="@+id/linearLayout1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <EditText
            android:id="@+id/editTextAddress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="0.74"
            android:hint="@string/ADDRESS_HINT" >

            <requestFocus />
        </EditText>

        <Button
            android:id="@+id/buttonGo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/GO" />
    </LinearLayout>

    <TextView
        android:id="@+id/textViewResponse"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0.41" />

</LinearLayout>

In der Activity muss nun die Methode onCreate angepasst werden. Zuerst benötigen wir eine Referenz auf den Button, damit wir einen OnClickListener mit ihm verbinden können:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
        
    Button go = (Button) findViewById(R.id.buttonGo);
        
    go.setOnClickListener(new OnClickListener() {
         
        @Override
        public void onClick(View v) {
        }
    });
}

Nun kann die Methode onClick des Listeners implementiert werden. Diese muss eine URL für die Anfrage generieren. Dazu wird der Inhalt des Eingabefeldes benötigt. Mit der erzeugten URL kann nun das HttpGet -Objekt erzeugt werden. Um den Http GET Request auszuführen, muss noch ein Objekt der Klasse HttpClient erstellt werden. Bis zu diesem Punkt sollte der Code in etwa wie folgt aussehen:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Button go = (Button) findViewById(R.id.buttonGo);
    final EditText input = (EditText) findViewById(R.id.editTextAddress);

    go.setOnClickListener(new OnClickListener() {

        @Override
        public void onClick(View v) {
            String address = input.getText().toString();

            String PROTOCOL = "http";
            String HOST = "maps.google.com";
            String PATH = "/maps/api/geocode/";
            String OUTPUT = "xml";
            String PARAMETERS = "?sensor=false&address=";
            String url = PROTOCOL + "://" + HOST + PATH + OUTPUT + PARAMETERS;

            HttpClient client = new DefaultHttpClient();

            try {
                
                HttpGet request = new HttpGet(url + URLEncoder.encode(address, "UTF-8"));
                HttpResponse response = client.execute(request);
                
            } catch (Exception e) {
                Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show();
            }
        }
    });
}

Wichtig: Der Wert des Adress-Parameters muss kodiert werden, da die URL sonst sehr wahrscheinlich ungültige Zeichen enthält.

Wenn man die Anwendung nun startet und versucht, eine Abfrage zu senden, so wird eine IOException geworfen. Diese hat ihren Ursprung in den Berechtigungen. Eine Anwendung muss explizit angeben, dass sie Zugriff auf das Internet benötigt. Diese Einstellung nimmt man in der Datei AndroidManifest.xml vor. Dort erstellt man innerhalb des manifest -Elementes folgendes Kindelement:

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

Damit erlaubt man der Anwendung, das Internet zu nutzen. Ein erneuter Versuch einer Anfrage sollte nun erfolgreich sein.

Die Daten, welche der Webservice zurück sendet befinden sich in einem Objekt vom Typ Entity . Dieses erhält man mittels response.getEntity() . Die Helferklasse EntityUtils kann den Inhalt des Entity -Objekts extrahieren und als String zurückliefern. Dieser String kann nun in unserem Ausgabefeld angezeigt werden.

@Override
public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.main);
	
	Button go = (Button) findViewById(R.id.buttonGo);
	final EditText input = (EditText) findViewById(R.id.editTextAddress);
	final TextView responseText = (TextView) findViewById(R.id.textViewResponse);
	
	...
}
HttpEntity entity = response.getEntity();
String data = EntityUtils.toString(entity);
responseText.setText(data);

Anpassung der Ausgabe

Bisher wird einfach der vom Webservice gelieferte XML-Code angezeigt. Aus diesem werden wir nun mittels XPath den Längen- und Breitengrad herausfiltern und für die Anzeige verwenden. Dafür nutzen wir die Klassen org.xml.sax.InputSource , javax.xml.xpath.XPath und einige weitere. Die Antwort des Webservice entählt einen Status. Bevor die Positionsdaten extrahiert werden können, sollte dieser überprüft werden:

@Override
public void onClick(View v) {
    String address = input.getText().toString();

    String PROTOCOL = "http";
    String HOST = "maps.google.com";
    String PATH = "/maps/api/geocode/";
    String OUTPUT = "xml";
    String PARAMETERS = "?sensor=false&address=";
    String url = PROTOCOL + "://" + HOST + PATH + OUTPUT + PARAMETERS;

    HttpClient client = new DefaultHttpClient();

    try {
        
        HttpGet request = new HttpGet(url + URLEncoder.encode(address, "UTF-8"));
        HttpResponse response = client.execute(request);
        HttpEntity entity = response.getEntity();
        String data = EntityUtils.toString(entity);
        
        InputSource source = new InputSource(new StringReader(data));
                    
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document doc = builder.parse(source);
        
        XPath xpath = XPathFactory.newInstance().newXPath();

        String status = (String) xpath.evaluate("//status/text()", doc, XPathConstants.STRING);

        if (!status.equals("OK")) {
            responseText.setText(status);
            return;
        }
        
    } catch (Exception e) {
        Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show();
    }

}

Die Positionsdaten befinden sich im Element result/geometry/location. Diese werden nun mittels XPath ermittelt und im Textfeld ausgegeben.

String lat = (String) xpath.evaluate("//geometry/location/lat/text()", doc,
        XPathConstants.STRING);

String lng = (String) xpath.evaluate("//geometry/location/lng/text()", doc,
        XPathConstants.STRING);

responseText.setText(String.format("Latitude: %s\nLongitude: %s", lat, lng));

Der gesamte Quelltext der Activity sollte nun in etwa wie folgt aussehen:

package de.hszigr.mobileapps;

import java.io.StringReader;
import java.net.URLEncoder;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class GMQueryActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        Button go = (Button) findViewById(R.id.buttonGo);
        final EditText input = (EditText) findViewById(R.id.editTextAddress);
        final TextView responseText = (TextView) findViewById(R.id.textViewResponse);

        go.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                String address = input.getText().toString();

                String PROTOCOL = "http";
                String HOST = "maps.google.com";
                String PATH = "/maps/api/geocode/";
                String OUTPUT = "xml";
                String PARAMETERS = "?sensor=false&address=";
                String url = PROTOCOL + "://" + HOST + PATH + OUTPUT + PARAMETERS;

                HttpClient client = new DefaultHttpClient();

                try {

                    HttpGet request = new HttpGet(url + URLEncoder.encode(address, "UTF-8"));
                    HttpResponse response = client.execute(request);
                    HttpEntity entity = response.getEntity();
                    String data = EntityUtils.toString(entity);

                    InputSource source = new InputSource(new StringReader(data));

                    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                    DocumentBuilder builder = factory.newDocumentBuilder();
                    Document doc = builder.parse(source);

                    XPath xpath = XPathFactory.newInstance().newXPath();

                    String status = (String) xpath.evaluate("//status/text()", doc,
                            XPathConstants.STRING);

                    if (!status.equals("OK")) {
                        responseText.setText(status);
                        return;
                    }

                    String lat = (String) xpath.evaluate("//geometry/location/lat/text()", doc,
                            XPathConstants.STRING);
                    
                    String lng = (String) xpath.evaluate("//geometry/location/lng/text()", doc,
                            XPathConstants.STRING);
                    
                    responseText.setText(String.format("Latitude: %s\nLongitude: %s", lat, lng));

                } catch (Exception e) {
                    Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG)
                            .show();
                }

            }
        });
    }
}

AsyncTask

Das oben vorgestellte Beispiel funktioniert bis zur Android Version 2.3. In der aktuellen Version 4.x können sogenannte Hintergrundaufgaben, beispielsweise eine GET Anfrage an einen Webservice, nicht mehr direkt im Hauptthread (UI) ausgeführt werden. Das macht Sinn, denn dauert die Abfrage eines Webservices mal länger, würde im oberen Bsp. das UI einfrieren. Auch kann kein Status zum aktuellen Geschehen abfragt werden.

Google stellt mit den AsyncTask ein Mittel für die Ausführung und Steuerung von kleinen Hintergrundaufgaben bereit. Dabei wird einen Threading-Rahmen mittels Klasse bereitgestellt.

Für die Ausführung des Tasks sind drei Parameter notwendig:

  1. Parameter, Typ der Eingangsparameter
  2. Fortschritt , Typ der Fortschrittsdaten, die während der Ausführung verarbeitet werden (Progressbar)
  3. Ergebnis, Typ des Rückgabewertes

private class GetDataFromService extends AsyncTask<Void, Void, String>

Weiterhin besitz der AsyncTask 4 Methoden, die Schrittweise ausgeführt werden:

  1. onPreExecute() Einrichtung und Start des AsyncTasks
  2. doInBackground() wird unmittelbar nach onPreExecute aufgerufen und führt die eigentliche Aufgabe (Abfrage WebService) durch
  3. onProgressUpdate(Progress...) wird während der Abarbeitung aufgerufen. Wird für die Anzeige des Fortschritts verwendet
  4. onPostExecute(Result) wird nach der Abarbeitung aufgerufen und aktualisiert das UI

	
private class GetDataFromService extends AsyncTask<Void, Void, String> {
     
	protected String doInBackground(Void... arg0) {
         
	//mache etwas....
	HttpGet request = new HttpGet(url + address);
	HttpResponse response = client.execute(request);
	HttpEntity entity = response.getEntity();
	data = EntityUtils.toString(entity);
	.....     
    }
 	return ergebnis;
}

	protected void onProgressUpdate(Integer... progress) {
	  //Operationen während der Ausführung (Bsp. Progressbar)
	 setProgressPercent(progress[0]);
  	}

 	protected void onPostExecute(Long result) {
       //Aktualisierung des UI mit den Ergebnisdaten
		TextView responseText = (TextView) findViewById(R.id.textViewResponse);
		responseText.setText(result.toString());
     }
 }

Das folgende Beispiel implementiert den bereits oben gezeigten Aufruf eines Webservice mittels AsyncTask.

package de.hszg.webserviceclient;

import java.io.StringReader;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity {

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		Button go = (Button) findViewById(R.id.buttonGo);

		go.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {

				new GetDataFromService().execute();

			}
		});
	}
	private class GetDataFromService extends AsyncTask<Void, Void, String> {

		private String PROTOCOL = "http";
		private String HOST = "maps.google.com";
		private String PATH = "/maps/api/geocode/";
		private String OUTPUT = "xml";	
		private String PARAMETERS = "?sensor=false&address=";
		private String url = PROTOCOL + "://" + HOST + PATH + OUTPUT
				+ PARAMETERS;
		private String data;
		private String result = null;

		@Override
		protected String doInBackground(Void... arg0) {

			EditText input = (EditText) findViewById(R.id.editTextAddress);
			String address = input.getText().toString();
			HttpClient client = new DefaultHttpClient();

			try {
				HttpGet request = new HttpGet(url + address);
				HttpResponse response = client.execute(request);
				HttpEntity entity = response.getEntity();
				data = EntityUtils.toString(entity);

				InputSource source = new InputSource(new StringReader(data));
				
				DocumentBuilderFactory factory = DocumentBuilderFactory
						.newInstance();
				DocumentBuilder builder = factory.newDocumentBuilder();
				Document doc = builder.parse(source);

				XPath xpath = XPathFactory.newInstance().newXPath();

				String status = (String) xpath.evaluate("//status/text()", doc,
						XPathConstants.STRING);
						
				if (!status.equals("OK")) {
					return status;
				}

				String lat = (String) xpath.evaluate(
						"//geometry/location/lat/text()", doc,
						XPathConstants.STRING);

				String lng = (String) xpath.evaluate(
						"//geometry/location/lng/text()", doc,
						XPathConstants.STRING);

				result = String.format("Latitude: %s\nLongitude: %s", lat, lng);

			} catch (Exception e) {
				Toast.makeText(getApplicationContext(), e.getMessage(),
						Toast.LENGTH_LONG).show();
			}

			return result;
		}

		@Override
		protected void onPostExecute(String result) {
			TextView responseText = (TextView) findViewById(R.id.textViewResponse);
			responseText.setText(result.toString());
		}

	}
}