Taste of Android :: Part-14
Android Application Development - XIII
In this part, we will cover how to use JSON
and HTTP to invoke externally hosted web
services.
Let us create a new Android application named
DroidDayWeather which is a simple
utility
application to fetch and display the current weather condition for a
given zip code.
One can register (sign-up for free) at
WorldWeatherOnline to
access the current weather
data for worldwide locations using their local weather API. Upon
successful
registration, one will be assigned an unique API access key that will
allow them to fetch the weather data in XML, JSON or CSV formats.
To fetch the current weather for the zip code 11001, we will send
an HTTP request to the following
URL :
http://api.worldweatheronline.com/free/v1/weather.ashx?q=11001&format=json&key=<access-key>
where the <access-key> is the
unique API access key.
This HTTP request will generate the
following output:
Output.1
{ "data": { "current_condition": [{ "cloudcover": "75", "humidity": "74", "observation_time": "01:03 AM", "precipMM": "0.0", "pressure": "1011", "temp_C": "26", "temp_F": "79", "visibility": "14", "weatherCode": "116", "weatherDesc": [{ "value": "Partly Cloudy" }], "weatherIconUrl": [{ "value": "http:\/\/cdn.worldweatheronline.net\/images\/wsymbols01_png_64\/wsymbol_0004_black_low_cloud.png" }], "winddir16Point": "N", "winddirDegree": "10", "windspeedKmph": "7", "windspeedMiles": "4" }], "request": [{ "query": "11001", "type": "Zipcode" }], "weather": [{ "date": "2013-08-28", "precipMM": "1.3", "tempMaxC": "30", "tempMaxF": "87", "tempMinC": "21", "tempMinF": "69", "weatherCode": "113", "weatherDesc": [{ "value": "Sunny" }], "weatherIconUrl": [{ "value": "http:\/\/cdn.worldweatheronline.net\/images\/wsymbols01_png_64\/wsymbol_0001_sunny.png" }], "winddir16Point": "SSE", "winddirDegree": "164", "winddirection": "SSE", "windspeedKmph": "12", "windspeedMiles": "8" }] } }
We are interested in the values for the following fields:
humidity , temp_C
and weatherIconUrl .
Note that the field weatherIconUrl
points to the URL of the weather icon for
the current weather condition.
We will not go step-by-step to show the various
screens since we already did that in
Part-2
for the DroidTipCalculator
application.
Create a directory called drawable
under the directory res .
We will need to download two icons - one to represent our
application and the other to represent an unknown weather
condition. You can download tons of open source friendly icons from
the site IconArchive and save
them in the
directory res/drawable .
Modify the contents of the dimens.xml
file to look like the one shown in the
listing 14.1 below:
Listing-14.1
<?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="text_size">24sp</dimen> <dimen name="zero_width">0dp</dimen> <dimen name="thickness">1dp</dimen> <dimen name="padding_size">10dp</dimen> <dimen name="table_padding">5dp</dimen> </resources>
Next, modify the contents of the strings.xml
file to look like the one shown in the
listing 14.2 below:
Listing-14.2
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">DroidDayWeather</string> <string name="action_settings">Settings</string> <string name="title">Weather of the Day</string> <string name="zip_code">Zip Code</string> <string name="weather_desc">Weather</string> <string name="location">Location</string> <string name="temparature">Temparature</string> <string name="humidity">Humidity</string> </resources>
We will have one main layout definition file named
activity_day_weather.xml
for accepting the zip code as the input and then displaying the
current weather information.
The contents of the activity_day_weather.xml
layout file will look like the one shown in the listing 14.3 below:
Listing-14.3
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".DayWeatherActivity" > <LinearLayout android:layout_gravity="center" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="@dimen/padding_size" android:orientation="horizontal" > <TextView android:gravity="center" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingBottom="@dimen/padding_size" android:text="@string/title" android:textColor="#cc3300" android:textSize="@dimen/text_size" android:textStyle="bold" android:typeface="sans" /> </LinearLayout> <!-- Horizontal Line (Divider) --> <LinearLayout android:layout_gravity="center" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingBottom="@dimen/padding_size" android:paddingTop="@dimen/padding_size" android:orientation="horizontal" > <View android:layout_width="match_parent" android:layout_height="@dimen/thickness" android:background="@android:color/black" /> </LinearLayout> <LinearLayout android:layout_gravity="center" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingBottom="@dimen/padding_size" android:orientation="horizontal" > <TextView android:layout_width="@dimen/zero_width" android:layout_height="wrap_content" android:layout_weight="0.30" android:textSize="@dimen/text_size" android:textStyle="bold" android:text="@string/zip_code" /> <EditText android:id="@+id/zip_text" android:inputType="number" android:layout_width="@dimen/zero_width" android:layout_height="wrap_content" android:layout_weight="0.50" android:maxLength="5" android:text="" /> <Button android:id="@+id/ok_button" android:layout_width="@dimen/zero_width" android:layout_height="wrap_content" android:layout_weight=".20" android:text="Ok" /> </LinearLayout> <!-- Horizontal Line (Divider) --> <LinearLayout android:layout_gravity="center" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingBottom="@dimen/padding_size" android:paddingTop="@dimen/padding_size" android:orientation="horizontal" > <View android:layout_width="match_parent" android:layout_height="@dimen/thickness" android:background="@android:color/black" /> </LinearLayout> <TableLayout android:layout_width="match_parent" android:layout_height="match_parent" > <TableRow android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:padding="@dimen/table_padding" > <TextView android:layout_width="@dimen/zero_width" android:layout_height="wrap_content" android:layout_weight="0.8" android:text="@string/location" android:textSize="@dimen/text_size" android:textStyle="bold" android:typeface="sans" /> <TextView android:id="@+id/zip_txt" android:layout_width="@dimen/zero_width" android:layout_height="wrap_content" android:layout_weight="0.2" android:text="" android:textSize="@dimen/text_size" android:textStyle="bold" android:typeface="sans" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:padding="@dimen/table_padding" > <TextView android:id="@+id/desc_txt" android:layout_width="@dimen/zero_width" android:layout_height="wrap_content" android:layout_weight="0.8" android:text="" android:textSize="@dimen/text_size" android:textStyle="bold" android:typeface="sans" /> <ImageView android:id="@+id/icon_img" android:layout_width="@dimen/zero_width" android:layout_height="wrap_content" android:layout_weight="0.2" android:contentDescription="@string/weather_desc" android:src="@drawable/default_weather" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:padding="@dimen/table_padding" > <TextView android:layout_width="@dimen/zero_width" android:layout_height="wrap_content" android:layout_weight=".80" android:text="@string/temparature" android:textSize="@dimen/text_size" android:textStyle="bold" android:typeface="sans" /> <TextView android:id="@+id/temp_txt" android:layout_width="@dimen/zero_width" android:layout_height="wrap_content" android:layout_weight=".20" android:textSize="@dimen/text_size" android:textStyle="bold" android:typeface="sans" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:padding="@dimen/table_padding" > <TextView android:layout_width="@dimen/zero_width" android:layout_height="wrap_content" android:layout_weight=".80" android:text="@string/humidity" android:textSize="@dimen/text_size" android:textStyle="bold" android:typeface="sans" /> <TextView android:id="@+id/hum_txt" android:layout_width="@dimen/zero_width" android:layout_height="wrap_content" android:layout_weight=".20" android:textSize="@dimen/text_size" android:textStyle="bold" android:typeface="sans" /> </TableRow> </TableLayout> </LinearLayout>
Previewing the activity_day_weather.xml
layout file in the Graphical Mode in Eclipse will look like the
one shown in the figure 14.1 below:
Figure-14.1
We will need an object to encapsulate the current weather
information. This information is encapsulated in the class
CurrentWeather and the contents of the java
source file CurrentWeather.java will look
like the one shown in the listing 14.4 below:
Listing-14.4
package com.polarsparc.android.droiddayweather; public class CurrentWeather { private String description; private String humidity; private String iconUrl; private String temperature; private String zip; private byte[] icon; public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getHumidity() { return humidity; } public void setHumidity(String humidity) { this.humidity = humidity; } public String getIconUrl() { return iconUrl; } public void setIconUrl(String iconUrl) { if (iconUrl != null) { this.iconUrl = iconUrl.replace("\\", ""); } } public String getTemperature() { return temperature; } public void setTemperature(String temperature) { this.temperature = temperature; } public String getZip() { return zip; } public void setZip(String zip) { this.zip = zip; } public byte[] getIcon() { return icon; } public void setIcon(byte[] icon) { this.icon = icon; } }
In our simple weather application, we will need to to make two web
requests - one to fetch the current weather data and the other to fetch
the current weather condition icon. These two web requests are
encapsulated in the class WeatherServiceClient .
The contents of the java source file WeatherServiceClient.java
will look like the one shown in the listing 14.5 below:
Listing-14.5
package com.polarsparc.android.droiddayweather; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.StringBuilder; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import android.util.Log; /* * HTTP Client to access current weather data for a given zip code in North America. * * In order to access the free weather data, one has to register for an access key * at www.worldweatheronline.com * * http://api.worldweatheronline.com/free/v1/weather.ashx?q=08648&format=json&key=<access key> * */ public class WeatherServiceClient { private static int BUFFER_SIZE = 1024; private static String TAG = "WeatherServiceClient"; private static String API_KEY = "<API Access Key>"; private static String API_URL = "http://api.worldweatheronline.com/free/v1/weather.ashx?format=json&q=%s&key=%s"; /* * Method to fetch the current weather data for a give zip code */ public static String getCurrentWeatherData(String zip) { String data = null; HttpURLConnection http = null; InputStream input = null; try { String apiURL = String.format(API_URL, zip, API_KEY); Log.i(TAG, "getCurrentWeatherData() : apiURL = " + apiURL); URL url = new URL(apiURL); URLConnection conn = url.openConnection(); if (conn != null && (conn instanceof HttpURLConnection)) { http = (HttpURLConnection) conn; http.setAllowUserInteraction(false); http.setRequestMethod("GET"); http.setDoInput(true); http.setDoOutput(true); http.connect(); int resCode = http.getResponseCode(); if (resCode == HttpURLConnection.HTTP_OK) { input = http.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(input)); StringBuilder builder = new StringBuilder(); String line = null; while ((line = reader.readLine()) != null) { builder.append(line); } data = builder.toString(); } else { Log.e(TAG, "getCurrentWeatherData() : zip = " + zip + ", response code = " + resCode); } } } catch(Throwable ex) { Log.e(TAG, "getCurrentWeatherData()", ex); } finally { if (input != null) { try { input.close(); } catch(Throwable ex) { } } if (http != null) { try { http.disconnect(); } catch(Throwable ex) { } } } return data; } /* * Method to fetch the current weather icon. The url for fetching * the weather icon will be part of the current weather data */ public static byte[] getCurrentWeatherIcon(String link) { byte[] icon = null; InputStream input = null; ByteArrayOutputStream output = null; try { HttpClient client = new DefaultHttpClient(); HttpGet request = new HttpGet(link); Log.i(TAG, "getCurrentWeatherIcon() : link = " + link); HttpResponse response = client.execute(request); StatusLine statusLine = response.getStatusLine(); int resCode = statusLine.getStatusCode(); if (resCode == 200) { byte[] buffer = new byte[BUFFER_SIZE]; HttpEntity entity = response.getEntity(); input = entity.getContent(); output = new ByteArrayOutputStream(); while (input.read(buffer) != -1) { output.write(buffer); } icon = output.toByteArray(); } else { Log.e(TAG, "getCurrentWeatherData() : link = " + link + ", response code = " + resCode); } } catch(Throwable ex) { Log.e(TAG, "getCurrentWeatherIcon()", ex); } finally { if (output != null) { try { output.close(); } catch(Throwable ex) { } } if (input != null) { try { input.close(); } catch(Throwable ex) { } } } return icon; } // ----- Private ----- private WeatherServiceClient() { } }
From the code listing 14.5 above, we gather the following:
Android SDK provides two options to make web service calls - one
using the built-in Java HttpURLConnection
class and the other using the bundled Apache HttpClient
class
In the method getCurrentWeatherData() ,
we use the built-in Java HttpURLConnection
class to fetch the weather data for a given zip code
In the method getCurrentWeatherIcon() ,
we use the bundled Apache HttpClient class
to fetch the current weather condition icon
We will need an object to parse the current weather data fetched
from
the web in a JSON format and extract the
desired
fields. The JSON parser is encapsulated in
the
class JSONWeatherParser and the contents of
the
java source file JSONWeatherParser.java will
look
like the one shown in the listing 14.6 below:
Listing-14.6
package com.polarsparc.android.droiddayweather; import org.json.JSONObject; import org.json.JSONArray; import android.util.Log; public class JSONWeatherParser { private static String TAG = "JSONWeatherParser"; private static String TAG_DATA = "data"; private static String TAG_CURRENT_CONDITION = "current_condition"; private static String TAG_HUMIDITY = "humidity"; private static String TAG_TEMP_C = "temp_C"; private static String TAG_WEATHER_DESC = "weatherDesc"; private static String TAG_WEATHER_ICON_URL = "weatherIconUrl"; private static String TAG_VALUE = "value"; public static CurrentWeather getCurrentWeather(String raw) { CurrentWeather weather = null; JSONObject json = null; try { json = new JSONObject(raw); // Weather Data JSONObject data = json.getJSONObject(TAG_DATA); // data JSONArray current_array = data.getJSONArray(TAG_CURRENT_CONDITION); // current_condition JSONObject current = current_array.getJSONObject(0); // first instance String humidity = current.getString(TAG_HUMIDITY); // humidity String temp_c = current.getString(TAG_TEMP_C); // temp_C Log.i(TAG, "getCurrentWeatherData() : humidity = " + humidity); Log.i(TAG, "getCurrentWeatherData() : temp_c = " + temp_c); JSONArray desc_array = current.getJSONArray(TAG_WEATHER_DESC); // weatherDesc JSONObject desc = desc_array.getJSONObject(0); // first instance String description = desc.getString(TAG_VALUE); // value Log.i(TAG, "getCurrentWeatherData() : description = " + description); JSONArray icon_array = current.getJSONArray(TAG_WEATHER_ICON_URL); // weatherIconUrl JSONObject icon = icon_array.getJSONObject(0); // first instance String url = icon.getString(TAG_VALUE); // value Log.i(TAG, "getCurrentWeatherData() : url = " + url); weather = new CurrentWeather(); weather.setHumidity(humidity); weather.setTemperature(temp_c); weather.setDescription(description); weather.setIconUrl(url); } catch(Throwable ex) { Log.e(TAG, "getCurrentWeatherData()", ex); } return weather; } // ----- Private ----- private JSONWeatherParser() { } }
From the code listing 14.6 above, we gather the following:
Android SDK provides a built-in JSON
parser class called JSOnObject
In the method getCurrentWeather() ,
we pass the raw JSON string and get back
an instance of CurrentWeather object
Square Brackets [] in JSON
represents an array
Curly Braces {} in JSON
represents an object
In the following listing 14.7 below, we show the contents of the
main
application user interface:
Listing-14.7
package com.polarsparc.android.droiddayweather; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; public class DayWeatherActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_day_weather); final EditText zip = (EditText) findViewById(R.id.zip_text); final Button ok = (Button) findViewById(R.id.ok_button); ok.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String zipStr = zip.getText().toString(); if (zipStr != null && zipStr.length() == 5) { if (checkNetworkConnectivity()) { AsyncWeatherTask task = new AsyncWeatherTask(); task.execute(new String[] {zipStr}); } else { displayAlertDialog(); } } } }); } // ----- Private ----- private boolean checkNetworkConnectivity() { ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo info = manager.getActiveNetworkInfo(); if (info != null && info.isConnected()) { return true; } return false; } private void displayAlertDialog() { AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle("Network Connectivity Alert"); alert.setMessage("No Network Connectivity !!!"); alert.setNeutralButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // Do nothing } }); alert.show(); } // ----- Inner Class(es) ----- private class AsyncWeatherTask extends AsyncTask<String, Void, CurrentWeather> { @Override protected CurrentWeather doInBackground(String... params) { CurrentWeather weather = null; String json = WeatherServiceClient.getCurrentWeatherData(params[0]); if (json != null) { weather = JSONWeatherParser.getCurrentWeather(json); if (weather != null && weather.getIconUrl() != null) { byte[] icon = WeatherServiceClient.getCurrentWeatherIcon(weather.getIconUrl()); if (icon != null && icon.length > 0) { weather.setIcon(icon); weather.setZip(params[0]); } } } return weather; } @Override protected void onPostExecute(CurrentWeather weather) { if (weather != null) { final EditText edt = (EditText) findViewById(R.id.zip_text); edt.setText(""); final TextView zip = (TextView) findViewById(R.id.zip_txt); zip.setText(weather.getZip()); final TextView desc = (TextView) findViewById(R.id.desc_txt); desc.setText(weather.getDescription()); if (weather.getIcon() != null && weather.getIcon().length > 0) { Bitmap image = BitmapFactory.decodeByteArray(weather.getIcon(), 0, weather.getIcon().length); final ImageView icon = (ImageView) findViewById(R.id.icon_img); icon.setImageBitmap(image); } final TextView temp = (TextView) findViewById(R.id.temp_txt); temp.setText(weather.getTemperature()); final TextView hum = (TextView) findViewById(R.id.hum_txt); hum.setText(weather.getHumidity()); } } } }
From the code listing 14.7 above, we gather the following:
In the method checkNetworkConnectivity() ,
we check for network connectivity using the Android Framework class
NetworkInfo , an instance of which is
acquired via the class ConnectivityManager
If there is no network connectivity, we display an alert dialog
Web requests are made in a background AsyncTask
Finally, modify the contents of the AndroidManifest.xml
file to look like the one shown in the listing 14.8 below:
Listing-14.8
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.polarsparc.android.droiddayweather" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="14" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <application android:allowBackup="true" android:icon="@drawable/day_weather" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="com.polarsparc.android.droiddayweather.DayWeatherActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
We are now ready to test our DroidDayWeather
application on the virtual Android device we created in
Part-1 .
We will create a Run Configuration for DroidDailyTodo
as we did in Part-2
for DroidTipCalculator .
Once the run configuration for DroidDayWeather
is ready, we will Run
the application and the application will come to life as shown in the
following figure 14.2 below:
Figure-14.2
Type in the zip code of 11001 and click
the Ok button. This action will fetch the
current weather data as well as the current weather condition icon
and display it on the screen as shown in the following figure
14.3 below:
Figure-14.3
Next, try another zip code of 94012
and click the Ok button. This action will
fetch the current weather data as well as the current weather
condition icon and display it on the screen as shown in the
following figure 14.4 below:
Figure-14.4
Now let us turn off the Network Connectivity by going into
the Settings of the Virtual Device
(Android Emulator) and turn on the Airplane
mode as shown in the following figure 14.5 below:
Figure-14.5
If we try the zip code of 11001 now,
we will see a screen as shown in the following figure 14.6 below:
Figure-14.6