Taste of Android :: Part-14


Bhaskar S 08/25/2013


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:

Current Weather Activity Image
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:

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:

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:

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:

Main Weather App Image
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:

Weather 11001 Image
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:

Weather 94012 Image
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:

Airplane Mode Image
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:

Network Alert Image
Figure-14.6

References

Taste of Android :: Part-1

Taste of Android :: Part-2

Taste of Android :: Part-3

Taste of Android :: Part-4

Taste of Android :: Part-5

Taste of Android :: Part-6

Taste of Android :: Part-7

Taste of Android :: Part-8

Taste of Android :: Part-9

Taste of Android :: Part-10

Taste of Android :: Part-11

Taste of Android :: Part-12

Taste of Android :: Part-13