SingleForecastFragment
The SingleForecastFragment
is a subclass of Fragment
designed to display the current conditions for a city.
SingleForecastFragment package
Statement, import
Statements and FieldsFigure 14.35 begins the definition of class define SingleForecastFragment
and defines its fields. Lines 25–30 define various String
constants that are used as keys when we save and restore a SingleForecastFragment
’s state during orientation changes.
1 // SingleForecastFragment.java
2 // Displays forecast information for a single city.
3 package com.deitel.weatherviewer;
4
5 import android.content.Context;
6 import android.content.res.Resources;
7 import android.graphics.Bitmap;
8 import android.os.Bundle;
9 import android.view.Gravity;
10 import android.view.LayoutInflater;
11 import android.view.View;
12 import android.view.ViewGroup;
13 import android.widget.ImageView;
14 import android.widget.TextView;
15 import android.widget.Toast;
16
17 import com.deitel.weatherviewer.ReadForecastTask.ForecastListener;
18 import com.deitel.weatherviewer.ReadLocationTask.LocationLoadedListener;
19
20 public class SingleForecastFragment extends ForecastFragment
21 {
22 private String zipcodeString; // ZIP code for this forecast
23
24 // lookup keys for the Fragment's saved state
25 private static final String LOCATION_KEY = "location";
26 private static final String TEMPERATURE_KEY = "temperature";
27 private static final String FEELS_LIKE_KEY = "feels_like";
28 private static final String HUMIDITY_KEY = "humidity";
29 private static final String PRECIPITATION_KEY = "chance_precipitation";
30 private static final String IMAGE_KEY = "image";
31
32 // used to retrieve ZIP code from saved Bundle
33 private static final String ZIP_CODE_KEY = "id_key";
34
35 private View forecastView; // contains all forecast Views
36 private TextView temperatureTextView; // displays actual temperature
37 private TextView feelsLikeTextView; // displays "feels like" temperature
38 private TextView humidityTextView; // displays humidity
39
40 private TextView locationTextView;
41
42 // displays the percentage chance of precipitation
43 private TextView chanceOfPrecipitationTextView;
44 private ImageView conditionImageView; // image of current sky condition
45 private TextView loadingTextView;
46 private Context context;
47 private Bitmap conditionBitmap;
48
SingleForecastFragment
Overloaded Method newInstance
SingleForecastFragment
’s static newInstance
methods create and return a new Fragment
for the specified ZIP code. In the first version of the method (Fig. 14.36, lines 50–64), we create a new SingleForecastFragment
, then insert the ZIP code into a new Bundle
and pass this to Fragment
’s setArguments
method. This information will later be retrieved in the Fragment
’s overridden onCreate
method. The newInstance
method that takes a Bundle
as an argument (lines 67–72), reads the ZIP code from the given bundle then returns the result of calling the newInstance
method that takes a String
.
49 // creates a new ForecastFragment for the given ZIP code
50 public static SingleForecastFragment newInstance(String zipcodeString)
51 {
52 // create new ForecastFragment
53 SingleForecastFragment newForecastFragment =
54 new SingleForecastFragment();
55
56 Bundle argumentsBundle = new Bundle(); // create a new Bundle
57
58 // save the given String in the Bundle
59 argumentsBundle.putString(ZIP_CODE_KEY, zipcodeString);
60
61 // set the Fragement's arguments
62 newForecastFragment.setArguments(argumentsBundle);
63 return newForecastFragment; // return the completed ForecastFragment
64 } // end method newInstance
65
66 // create a ForecastFragment using the given Bundle
67 public static SingleForecastFragment newInstance(Bundle argumentsBundle)
68 {
69 // get the ZIP code from the given Bundle
70 String zipcodeString = argumentsBundle.getString(ZIP_CODE_KEY);
71 return newInstance(zipcodeString); // create new ForecastFragment
72 } // end method newInstance
73
SingleForecastFragment
Methods onCreate
, onSaveInstanceState
and getZipcode
In method onCreate
(Fig. 14.37, lines 75–82), the ZIP code String
is read from the Bundle
parameter and saved in SingleForecastFragment
’s zipcodeString
instance variable.
74 // create the Fragment from the saved state Bundle
75 @Override
76 public void onCreate(Bundle argumentsBundle)
77 {
78 super.onCreate(argumentsBundle);
79
80 // get the ZIP code from the given Bundle
81 this.zipcodeString = getArguments().getString(ZIP_CODE_KEY);
82 } // end method onCreate
83
84 // save the Fragment's state
85 @Override
86 public void onSaveInstanceState(Bundle savedInstanceStateBundle)
87 {
88 super.onSaveInstanceState(savedInstanceStateBundle);
89
90 // store the View's contents into the Bundle
91 savedInstanceStateBundle.putString(LOCATION_KEY,
92 locationTextView.getText().toString());
93 savedInstanceStateBundle.putString(TEMPERATURE_KEY,
94 temperatureTextView.getText().toString());
95 savedInstanceStateBundle.putString(FEELS_LIKE_KEY,
96 feelsLikeTextView.getText().toString());
97 savedInstanceStateBundle.putString(HUMIDITY_KEY,
98 humidityTextView.getText().toString());
99 savedInstanceStateBundle.putString(PRECIPITATION_KEY,
100 chanceOfPrecipitationTextView.getText().toString());
101 savedInstanceStateBundle.putParcelable(IMAGE_KEY, conditionBitmap);
102 } // end method onSaveInstanceState
103
104 // public access for ZIP code of this Fragment's forecast information
105 public String getZipcode()
106 {
107 return zipcodeString; // return the ZIP code String
108 } // end method getZIP code
109
Method onSaveInstanceState
(lines 85–102) saves the forecast information currently displayed by the Fragment
so we do not need to launch new AsyncTask
s after each orientation change. The text of each TextView
is added to the Bundle
parameter using Bundle
’s putString
method. The forecast image Bitmap
is included using Bundle
’s putParcelable
method. ForecastFragment
’s getZipcode
method (lines 105–108) returns a String
representing the ZIP code associated with this SingleForecastFragment
.
onCreateView
Method onCreateView
(Fig. 14.38) inflates and initializes ForecastFragment
’s View
hierarchy. The layout defined in forecast_fragment_layout.xml
is inflated with the given LayoutInflator
. We pass null
as the second argument to LayoutInflator
’s inflate
method. This argument normally specifies a ViewGroup
to which the newly inflated View
will be attached. It’s important not to attach the Fragment
’s root View
to any ViewGroup
in its onCreateView
method. This happens automatically later in the Fragment
’s lifecycle. We use View
’s findViewById
method to get references to each of the Fragment
’s View
s then return the layout’s root View
.
110 // inflates this Fragement's layout from xml
111 @Override
112 public View onCreateView(LayoutInflater inflater, ViewGroup container,
113 Bundle savedInstanceState)
114 {
115 // use the given LayoutInflator to inflate layout stored in
116 // forecast_fragment_layout.xml
117 View rootView = inflater.inflate(R.layout.forecast_fragment_layout,
118 null);
119
120 // get the TextView in the Fragment's layout hierarchy
121 forecastView = rootView.findViewById(R.id.forecast_layout);
122 loadingTextView = (TextView) rootView.findViewById(
123 R.id.loading_message);
124 locationTextView = (TextView) rootView.findViewById(R.id.location);
125 temperatureTextView = (TextView) rootView.findViewById(
126 R.id.temperature);
127 feelsLikeTextView = (TextView) rootView.findViewById(
128 R.id.feels_like);
129 humidityTextView = (TextView) rootView.findViewById(
130 R.id.humidity);
131 chanceOfPrecipitationTextView = (TextView) rootView.findViewById(
132 R.id.chance_of_precipitation);
133 conditionImageView = (ImageView) rootView.findViewById(
134 R.id.forecast_image);
135
136 context = rootView.getContext(); // save the Context
137
138 return rootView; // return the inflated View
139 } // end method onCreateView
140
onActivityCreated
Method onActivityCreated
(Fig. 14.39) is called after the Fragment
’s parent Activity
and the Fragment
’s View
have been created. We check whether the Bundle
parameter contains any data. If not, we hide all the View
s displaying forecast information and display a loading message. Then we launch a new ReadLocationTask
to begin populating this Fragment
’s data. If the Bundle
is not null
, we retrieve the information stored in the Bundle
by onSaveInstanceState
(Fig. 14.37) and display that information in the Fragment
’s View
s.
141 // called when the parent Activity is created
142 @Override
143 public void onActivityCreated(Bundle savedInstanceStateBundle)
144 {
145 super.onActivityCreated(savedInstanceStateBundle);
146
147 // if there is no saved information
148 if (savedInstanceStateBundle == null)
149 {
150 // hide the forecast and show the loading message
151 forecastView.setVisibility(View.GONE);
152 loadingTextView.setVisibility(View.VISIBLE);
153
154 // load the location information in a background thread
155 new ReadLocationTask(zipcodeString, context,
156 new WeatherLocationLoadedListener(zipcodeString)).execute();
157 } // end if
158 else
159 {
160 // display information in the saved state Bundle using the
161 // Fragment's Views
162 conditionImageView.setImageBitmap(
163 (Bitmap) savedInstanceStateBundle.getParcelable(IMAGE_KEY));
164 locationTextView.setText(savedInstanceStateBundle.getString(
165 LOCATION_KEY));
166 temperatureTextView.setText(savedInstanceStateBundle.getString(
167 TEMPERATURE_KEY));
168 feelsLikeTextView.setText(savedInstanceStateBundle.getString(
169 FEELS_LIKE_KEY));
170 humidityTextView.setText(savedInstanceStateBundle.getString(
171 HUMIDITY_KEY));
172 chanceOfPrecipitationTextView.setText(
173 savedInstanceStateBundle.getString(PRECIPITATION_KEY));
174 } // end else
175 } // end method onActivityCreated
176
ForecastListener
The weatherForecastListener
(Fig. 14.40) receives data from the ReadForecastTask
(Section 14.5.7). We first check that this Fragment
is still attached to the WeatherViewerActivity
using Fragment
’s isAdded
method. If not, the user must have navigated away from this Fragment
while the ReadForecastTask
was executing, so we exit without doing anything. If data was returned successfully we display that data in the Fragment
’s View
s.
177 // receives weather information from AsyncTask
178 ForecastListener weatherForecastListener = new ForecastListener()
179 {
180 // displays the forecast information
181 @Override
182 public void onForecastLoaded(Bitmap imageBitmap,
183 String temperatureString, String feelsLikeString,
184 String humidityString, String precipitationString)
185 {
186 // if this Fragment was detached while the background process ran
187 if (!SingleForecastFragment.this.isAdded())
188 {
189 return; // leave the method
190 } // end if
191 else if (imageBitmap == null)
192 {
193 Toast errorToast = Toast.makeText(context,
194 context.getResources().getString(
195 R.string.null_data_toast), Toast.LENGTH_LONG);
196 errorToast.setGravity(Gravity.CENTER, 0, 0);
197 errorToast.show(); // show the Toast
198 return; // exit before updating the forecast
199 } // end if
200
201 Resources resources = SingleForecastFragment.this.getResources();
202
203 // display the loaded information
204 conditionImageView.setImageBitmap(imageBitmap);
205 conditionBitmap = imageBitmap;
206 temperatureTextView.setText(temperatureString + (char)0x00B0 +
207 resources.getString(R.string.temperature_unit));
208 feelsLikeTextView.setText(feelsLikeString + (char)0x00B0 +
209 resources.getString(R.string.temperature_unit));
210 humidityTextView.setText(humidityString + (char)0x0025);
211 chanceOfPrecipitationTextView.setText(precipitationString +
212 (char)0x0025);
213 loadingTextView.setVisibility(View.GONE); // hide loading message
214 forecastView.setVisibility(View.VISIBLE); // show the forecast
215 } // end method onForecastLoaded
216 }; // end weatherForecastListener
217
LocationLoadedListener
The WeatherLocationLoadedListener
(Fig. 14.41) receives location information from the ReadLocationTask
(Section 14.5.6) and displays a String
constructed from that data in the locationTextView
. We then execute a new ReadForecastTask
to retrieve the forecast’s remaining data.
218 // receives location information from background task
219 private class WeatherLocationLoadedListener implements
220 LocationLoadedListener
221 {
222 private String zipcodeString; // ZIP code to look up
223
224 // create a new WeatherLocationLoadedListener
225 public WeatherLocationLoadedListener(String zipcodeString)
226 {
227 this.zipcodeString = zipcodeString;
228 } // end WeatherLocationLoadedListener
229
230 // called when the location information is loaded
231 @Override
232 public void onLocationLoaded(String cityString, String stateString,
233 String countryString)
234 {
235 if (cityString == null) // if there is no returned data
236 {
237 // display the error message
238 Toast errorToast = Toast.makeText(
239 context, context.getResources().getString(
240 R.string.null_data_toast), Toast.LENGTH_LONG);
241 errorToast.setGravity(Gravity.CENTER, 0, 0);
242 errorToast.show(); // show the Toast
243 return; // exit before updating the forecast
244 } // end if
245 // display the return information in a TextView
246 locationTextView.setText(cityString + " " + stateString + ", " +
247 zipcodeString + " " + countryString);
248 // load the forecast in a background thread
249 new ReadForecastTask(zipcodeString, weatherForecastListener,
250 locationTextView.getContext()).execute();
251 } // end method onLocationLoaded
252 } // end class LocationLoadedListener
253 } // end class SingleForecastFragment
ReadLocationTask
The ReadLocationTask
retrieves city, state and country names for a given ZIP code. The LocationLoadedListener
interface describes a listener capable of receiving the location data. String
s for the city, state and country are passed to the listener’s onLocationLoaded
method when the data is retrieved.
ReadLocationTask package
Statement, import
Statements and FieldsFigure 14.42 begins the definition of class ReadLocationTask
and defines the instance variables used when reading a location from the WeatherBug web services.
1 // ReadLocationTask.java
2 // Reads location information in a background thread.
3 package com.deitel.weatherviewer;
4
5 import java.io.IOException;
6 import java.io.InputStreamReader;
7 import java.io.Reader;
8 import java.net.MalformedURLException;
9 import java.net.URL;
10
11 import android.content.Context;
12 import android.content.res.Resources;
13 import android.os.AsyncTask;
14 import android.util.JsonReader;
15 import android.util.Log;
16 import android.view.Gravity;
17 import android.widget.Toast;
18
19 // converts ZIP code to city name in a background thread
20 class ReadLocationTask extends AsyncTask<Object, Object, String>
21 {
22 private static final String TAG = "ReadLocatonTask.java";
23
24 private String zipcodeString; // the ZIP code for the location
25 private Context context; // launching Activity's Context
26 private Resources resources; // used to look up String from xml
27
28 // Strings for each type of data retrieved
29 private String cityString;
30 private String stateString;
31 private String countryString;
32
33 // listener for retrieved information
34 private LocationLoadedListener weatherLocationLoadedListener;
35
ReadLocationTask
ConstructorNested interface LocationLoadedListener
(Fig. 14.43, lines 37–41) defines method onLocationLoaded
that’s implemented by several other classes so they can be notified when the ReadLocationTask
receives a response from the WeatherBug web services. The ReadLocationTask
constructor (lines 44–51) takes a ZIP code String
, the WeatherViewerActivity
’s Context
and a LocationLoadedListener
. We save the given Context
’s Resources
object so we can use it later to load String
s from the app’s XML resources.
36 // interface for receiver of location information
37 public interface LocationLoadedListener
38 {
39 public void onLocationLoaded(String cityString, String stateString,
40 String countryString);
41 } // end interface LocationLoadedListener
42
43 // public constructor
44 public ReadLocationTask(String zipCodeString, Context context,
45 LocationLoadedListener listener)
46 {
47 this.zipcodeString = zipCodeString;
48 this.context = context;
49 this.resources = context.getResources();
50 this.weatherLocationLoadedListener = listener;
51 } // end constructor ReadLocationTask
52
ReadLocationTask
Method doInBackground
In method doInBackground
(Fig. 14.44), we create an InputStreamReader
accessing the WeatherBug webservice at the location described by the URL
. We use this to create a JsonReader
so we can read the JSON data returned by the web service. (You can view the JSON document directly by opening the weatherServiceURL
in a browser.) JSON (JavaScript Object Notation)—a simple way to represent JavaScript objects as strings—is an alternative to XML for passing data between the client and the server. Each object in JSON is represented as a list of property names and values contained in curly braces, in the following format:
{ "propertyName1" : value1, "propertyName2'": value2 }
53 // load city name in background thread
54 @Override
55 protected String doInBackground(Object... params)
56 {
57 try
58 {
59 // construct Weatherbug API URL
60 URL url = new URL(resources.getString(
61 R.string.location_url_pre_zipcode) + zipcodeString +
62 "&api_key=YOUR_API_KEY");
63
64 // create an InputStreamReader using the URL
65 Reader forecastReader = new InputStreamReader(
66 url.openStream());
67
68 // create a JsonReader from the Reader
69 JsonReader forecastJsonReader = new JsonReader(forecastReader);
70 forecastJsonReader.beginObject(); // read the first Object
71
72 // get the next name
73 String name = forecastJsonReader.nextName();
74
75 // if the name indicates that the next item describes the
76 // ZIP code's location
77 if (name.equals(resources.getString(R.string.location)))
78 {
79 // start reading the next JSON Object
80 forecastJsonReader.beginObject();
81
82 String nextNameString;
83
84 // while there is more information to be read
85 while (forecastJsonReader.hasNext())
86 {
87 nextNameString = forecastJsonReader.nextName();
88 // if the name indicates that the next item describes the
89 // ZIP code's corresponding city name
90 if ((nextNameString).equals(
91 resources.getString(R.string.city)))
92 {
93 // read the city name
94 cityString = forecastJsonReader.nextString();
95 } // end if
96 else if ((nextNameString).equals(resources.
97 getString(R.string.state)))
98 {
99 stateString = forecastJsonReader.nextString();
100 } // end else if
101 else if ((nextNameString).equals(resources.
102 getString(R.string.country)))
103 {
104 countryString = forecastJsonReader.nextString();
105 } // end else if
106 else
107 {
108 forecastJsonReader.skipValue(); // skip unexpected value
109 } // end else
110 } // end while
111
112 forecastJsonReader.close(); // close the JsonReader
113 } // end if
114 } // end try
115 catch (MalformedURLException e)
116 {
117 Log.v(TAG, e.toString()); // print the exception to the LogCat
118 } // end catch
119 catch (IOException e)
120 {
121 Log.v(TAG, e.toString()); // print the exception to the LogCat
122 } // end catch
123
124 return null; // return null if the city name couldn't be found
125 } // end method doInBackground
126
Arrays are represented in JSON with square brackets in the following format:
[ value1, value2, value3 ]
Each value can be a string, a number, a JSON representation of an object, true, false
or null
. JSON is commonly used to communicate in client/server interaction.
JsonReader
has methods beginObject
and beginArray
to begin reading objects and arrays, respectively. Line 70 uses JsonReader
’ beginObject
method to read the first object in the JSON document. We get the name from the first name–value pair in the object with JsonReader
’s nextName
method (line 73), then check that it matches the expected name for a location information document. If so, we move to the next object (line 80), which describes the ZIP code’s location information, and read each name–value pair in the object using a loop (lines 85–110). If the name in a name–value pair matches one of the pieces of data we use to display weather information in this app, we save the corresponding value to one of ReadLocationTask
’s instance variables. Class JsonReader
provides methods for reading boolean
s, double
s, int
s, long
s and String
s—since we’re displaying all the data in String
format, we use only JsonReader
’s getString
method. All unrecognized names are skipped using JsonReader
’s skipValue
method. [Note: The code for reading the JSON data returned by the WeatherBug web services depends directly on the structure of the JSON document returned. If WeatherBug changes the format of this JSON data in the future, an exception may occur.]
ReadLocationTask
Method onPostExecute
Method onPostExecute
(Fig. 14.45) delivers the results to the GUI thread for display. If the retrieved data is not null
(i.e., the web service call returned data), we pass the location information String
s to the stored LocationLoadedListener
’s onLocationLoaded
method. Otherwise, we display a Toast
informing the user that the location information retrieval failed.
127 // executed back on the UI thread after the city name loads
128 protected void onPostExecute(String nameString)
129 {
130 // if a city was found to match the given ZIP code
131 if (cityString != null)
132 {
133 // pass the information back to the LocationLoadedListener
134 weatherLocationLoadedListener.onLocationLoaded(cityString,
135 stateString, countryString);
136 } // end if
137 else
138 {
139 // display Toast informing that location information
140 // couldn't be found
141 Toast errorToast = Toast.makeText(context, resources.getString(
142 R.string.invalid_zipcode_error), Toast.LENGTH_LONG);
143 errorToast.setGravity(Gravity.CENTER, 0, 0); // center the Toast
144 errorToast.show(); // show the Toast
145 } // end else
146 } // end method onPostExecute
147 } // end class ReadLocationTask
ReadForecastTask
The ReadForecastTask
retrieves the current weather conditions for a given ZIP code.
ReadForecastTask package
Statement, import
Statements and FieldsFigure 14.46 begins the definition of class ReadForecastTask
. The String
instance variables store the text for the weather conditions. A Bitmap
stores an image of the current conditions. The bitmapSampleSize
variable is used to specify how to downsample the image Bitmap
.
1 // ReadForecastTask.java
2 // Reads weather information off the main thread.
3 package com.deitel.weatherviewer;
4
5 import java.io.IOException;
6 import java.io.InputStreamReader;
7 import java.io.Reader;
8 import java.net.MalformedURLException;
9 import java.net.URL;
10
11 import android.content.Context;
12 import android.content.res.Resources;
13 import android.graphics.Bitmap;
14 import android.graphics.BitmapFactory;
15 import android.os.AsyncTask;
16 import android.util.JsonReader;
17 import android.util.Log;
18
19 class ReadForecastTask extends AsyncTask<Object, Object, String>
20 {
21 private String zipcodeString; // the ZIP code of the forecast's city
22 private Resources resources;
23
24 // receives weather information
25 private ForecastListener weatherForecastListener;
26 private static final String TAG = "ReadForecastTask.java";
27
28 private String temperatureString; // the temperature
29 private String feelsLikeString; // the "feels like" temperature
30 private String humidityString; // the humidity
31 private String chanceOfPrecipitationString; // chance of precipitation
32 private Bitmap iconBitmap; // image of the sky condition
33
34 private int bitmapSampleSize = -1;
35
36 // interface for receiver of weather information
37 public interface ForecastListener
38 {
39 public void onForecastLoaded(Bitmap image, String temperature,
40 String feelsLike, String humidity, String precipitation);
41 } // end interface ForecastListener
42
The ForecastListener
interface (lines 37–41) describes a listener capable of receiving the forecast image Bitmap
and String
s representing the current temperature, feels-like temperature, humidity and chance of precipitation.
ReadForecastTask
Constructor and setSampleSize
MethodsThe ReadForecastTask
constructor (Fig. 14.47, lines 44–50) takes a ZIP code String
, a ForecastListener
and the WeatherViewerActivity
’s Context
.
43 // creates a new ReadForecastTask
44 public ReadForecastTask(String zipcodeString,
45 ForecastListener listener, Context context)
46 {
47 this.zipcodeString = zipcodeString;
48 this.weatherForecastListener = listener;
49 this.resources = context.getResources();
50 } // end constructor ReadForecastTask
51
52 // set the sample size for the forecast's Bitmap
53 public void setSampleSize(int sampleSize)
54 {
55 this.bitmapSampleSize = sampleSize;
56 } // end method setSampleSize
57
The setSampleSize
method (lines 53–56) sets the downsampling rate when loading the forecast’s image Bitmap
. If this method is not called, the Bitmap
is not downsampled. The WeatherProvider
uses this method because there is a strict limit on the size of Bitmap
s that can be passed using a RemoteViews
object. This is because the RemoteViews
object communicates with the app widget across processes.
ReadForecastTask
Methods doInBackground
and onPostExecute
The doInBackground
method (Fig. 14.48, lines 59–101) gets and parses the WeatherBug JSON document representing the current weather conditions in a background thread. We create a URL
pointing to the web service then use it to construct a JsonReader
. JsonReader
’s beginObject
and nextName
methods are used to read the first name of the first object in the document (lines 75 and 78). If the name matches the String
specified in the String
resource R.string.hourly_forecast
, we pass the JsonReader
to the readForecast
method to parse the forecast. The onPostExecute
method (lines 104–110) returns the retrieved String
s to the ForecastLoadedListener
’s onForecastLoaded
method for display.
58 // load the forecast in a background thread
59 protected String doInBackground(Object... args)
60 {
61 try
62 {
63 // the url for the WeatherBug JSON service
64 URL webServiceURL = new URL(resources.getString(
65 R.string.pre_zipcode_url) + zipcodeString + "&ht=t&ht=i&"
66 + "ht=cp&ht=fl&ht=h&api_key=YOUR_API_KEY");
67
68 // create a stream Reader from the WeatherBug url
69 Reader forecastReader = new InputStreamReader(
70 webServiceURL.openStream());
71
72 // create a JsonReader from the Reader
73 JsonReader forecastJsonReader = new JsonReader(forecastReader);
74
75 forecastJsonReader.beginObject(); // read the first Object
76
77 // get the next name
78 String name = forecastJsonReader.nextName();
79
80 // if its the name expected for hourly forecast information
81 if (name.equals(resources.getString(R.string.hourly_forecast)))
82 {
83 readForecast(forecastJsonReader); // read the forecast
84 } // end if
85
86 forecastJsonReader.close(); // close the JsonReader
87 } // end try
88 catch (MalformedURLException e)
89 {
90 Log.v(TAG, e.toString());
91 } // end catch
92 catch (IOException e)
93 {
94 Log.v(TAG, e.toString());
95 } // end catch
96 catch (IllegalStateException e)
97 {
98 Log.v(TAG, e.toString() + zipcodeString);
99 } // end catch
100 return null;
101 } // end method doInBackground
102
103 // update the UI back on the main thread
104 protected void onPostExecute(String forecastString)
105 {
106 // pass the information to the ForecastListener
107 weatherForecastListener.onForecastLoaded(iconBitmap,
108 temperatureString, feelsLikeString, humidityString,
109 chanceOfPrecipitationString);
110 } // end method onPostExecute
111
ReadForecastTask
Method getIconBitmap
The static getIconBitmap
method (Fig. 14.49) converts a condition String
to a Bitmap
. The WeatherBug JSON document provides the relative path to the forecast’ image on the WeatherBug website. We create a URL
pointing to the image’s location. We load the image from the WeatherBug server using BitmapFactory
’s static decodeStream
method.
112 // get the sky condition image Bitmap
113 public static Bitmap getIconBitmap(String conditionString,
114 Resources resources, int bitmapSampleSize)
115 {
116 Bitmap iconBitmap = null; // create the Bitmap
117 try
118 {
119 // create a URL pointing to the image on WeatherBug's site
120 URL weatherURL = new URL(resources.getString(
121 R.string.pre_condition_url) + conditionString +
122 resources.getString(R.string.post_condition_url));
123
124 BitmapFactory.Options options = new BitmapFactory.Options();
125 if (bitmapSampleSize != -1)
126 {
127 options.inSampleSize = bitmapSampleSize;
128 } // end if
129
130 // save the image as a Bitmap
131 iconBitmap = BitmapFactory.decodeStream(weatherURL.
132 openStream(), null, options);
133 } // end try
134 catch (MalformedURLException e)
135 {
136 Log.e(TAG, e.toString());
137 } // end catch
138 catch (IOException e)
139 {
140 Log.e(TAG, e.toString());
141 } // end catch
142
143 return iconBitmap; // return the image
144 } // end method getIconBitmap
145
ReadForecastTask
Method readForecast
The readForecast
method (Fig. 14.50) parses a single current conditions forecast using the JsonReader
parameter. JsonReader
’s beginArray
and beginObject
methods (lines 151–152) are used to start reading the first object in the next array in the JSON document. We then loop through each name in the object and compare them to the expected names for the information we’d like to display. JsonReade
r’s skipValue
method is used to skip the information we don’t need.
146 // read the forecast information using the given JsonReader
147 private String readForecast(JsonReader reader)
148 {
149 try
150 {
151 reader.beginArray(); // start reading the next array
152 reader.beginObject(); // start reading the next object
153
154 // while there is a next element in the current object
155 while (reader.hasNext())
156 {
157 String name = reader.nextName(); // read the next name
158
159 // if this element is the temperature
160 if (name.equals(resources.getString(R.string.temperature)))
161 {
162 // read the temperature
163 temperatureString = reader.nextString();
164 } // end if
165 // if this element is the "feels-like" temperature
166 else if (name.equals(resources.getString(R.string.feels_like)))
167 {
168 // read the "feels-like" temperature
169 feelsLikeString = reader.nextString();
170 } // end else if
171 // if this element is the humidity
172 else if (name.equals(resources.getString(R.string.humidity)))
173 {
174 humidityString = reader.nextString(); // read the humidity
175 } // end else if
176 // if this next element is the chance of precipitation
177 else if (name.equals(resources.getString(
178 R.string.chance_of_precipitation)))
179 {
180 // read the chance of precipitation
181 chanceOfPrecipitationString = reader.nextString();
182 } // end else if
183 // if the next item is the icon name
184 else if (name.equals(resources.getString(R.string.icon)))
185 {
186 // read the icon name
187 iconBitmap = getIconBitmap( reader.nextString(), resources,
188 bitmapSampleSize);
189 } // end else if
190 else // there is an unexpected element
191 {
192 reader.skipValue(); // skip the next element
193 } // end else
194 } // end while
195 } // end try
196 catch (IOException e)
197 {
198 Log.e(TAG, e.toString());
199 } // end catch
200 return null;
201 } // end method readForecast
202 } // end ReadForecastTask
FiveDayForecastFragment
The FiveDayForecastFragment
displays the five-day forecast for a single city.
FiveDayForecastFragment package
Statement, import
Statements and FieldsIn Fig. 14.51, we begin class FiveDayForecastFragment
and define the fields used throughout the class.
1 // FiveDayForecastFragment.java
2 // Displays the five day forecast for a single city.
3 package com.deitel.weatherviewer;
4
5 import android.content.Context;
6 import android.content.res.Configuration;
7 import android.os.Bundle;
8 import android.view.Gravity;
9 import android.view.LayoutInflater;
10 import android.view.View;
11 import android.view.ViewGroup;
12 import android.widget.ImageView;
13 import android.widget.LinearLayout;
14 import android.widget.TextView;
15 import android.widget.Toast;
16
17 import com.deitel.weatherviewer.ReadFiveDayForecastTask.
FiveDayForecastLoadedListener;
18 import com.deitel.weatherviewer.ReadLocationTask.LocationLoadedListener;
19
20 public class FiveDayForecastFragment extends ForecastFragment
21 {
22 // used to retrieve ZIP code from saved Bundle
23 private static final String ZIP_CODE_KEY = "id_key";
24 private static final int NUMBER_DAILY_FORECASTS = 5;
25
26 private String zipcodeString; // ZIP code for this forecast
27 private View[] dailyForecastViews = new View[NUMBER_DAILY_FORECASTS];
28
29 private TextView locationTextView;
30
FiveDayForecastFragment
Overloaded newInstance
MethodsSimilar to the SingleForecastFragment
, we provide overloaded newInstance
method (Fig. 14.52) to create new FiveDayForecastFragment
s. The first method (lines 32–46) takes a ZIP code String
. The other (lines 49–55) takes a Bundle
containing the ZIP code String
, extracts the ZIP code and passes it to the first method. Lines 38 and 41 create and configure a Bundle
containing the ZIP code String
, then pass it to Fragment
’s setArguments
method so it can be used in onCreate
(Fig. 14.53).
31 // creates a new FiveDayForecastFragment for the given ZIP code
32 public static FiveDayForecastFragment newInstance(String zipcodeString)
33 {
34 // create new ForecastFragment
35 FiveDayForecastFragment newFiveDayForecastFragment =
36 new FiveDayForecastFragment();
37
38 Bundle argumentsBundle = new Bundle(); // create a new Bundle
39
40 // save the given String in the Bundle
41 argumentsBundle.putString(ZIP_CODE_KEY, zipcodeString);
42
43 // set the Fragement's arguments
44 newFiveDayForecastFragment.setArguments(argumentsBundle);
45 return newFiveDayForecastFragment; // return the completed Fragment
46 } // end method newInstance
47
48 // create a FiveDayForecastFragment using the given Bundle
49 public static FiveDayForecastFragment newInstance(
50 Bundle argumentsBundle)
51 {
52 // get the ZIP code from the given Bundle
53 String zipcodeString = argumentsBundle.getString(ZIP_CODE_KEY);
54 return newInstance(zipcodeString); // create new Fragment
55 } // end method newInstance
56
FiveDayForecastFragment
Methods onCreate
and getZipCode
The ZIP code is read in the Fragment
’s onCreate
method (Fig. 14.53, lines 58–65). Fragment
’s getArguments
method retrieves the Bundle
then Bundle
’s getString
method accesses the ZIP code String
. Method getZipcode
(lines 68–71) is called by the WeatherViewerActivity
to get the FiveDayForecastFragment
’s ZIP code.
57 // create the Fragment from the saved state Bundle
58 @Override
59 public void onCreate(Bundle argumentsBundle)
60 {
61 super.onCreate(argumentsBundle);
62
63 // get the ZIP code from the given Bundle
64 this.zipcodeString = getArguments().getString(ZIP_CODE_KEY);
65 } // end method onCreate
66
67 // public access for ZIP code of this Fragment's forecast information
68 public String getZipcode()
69 {
70 return zipcodeString; // return the ZIP code String
71 } // end method getZipcode
72
FiveDayForecastFragment
Method onCreateView
The Fragment
’s layout is created in method onCreateView
(Fig. 14.54). We inflate the layout defined in five_day_forecast.xml
using the given LayoutInflator
and pass null
as the second argument. We check the orientation of the device here to determine which layout to use for each daily forecast View
. We then inflate five of the selected layouts and add each View
to the container LinearLayout
. Next we execute a ReadLocationTask
to retrieve the location information for this Fragment
’s corresponding city.
73 // inflates this Fragement's layout from xml
74 @Override
75 public View onCreateView(LayoutInflater inflater, ViewGroup container,
76 Bundle savedInstanceState)
77 {
78 // inflate the five day forecast layout
79 View rootView = inflater.inflate(R.layout.five_day_forecast_layout,
80 null);
81 // get the TextView to display location information
82 locationTextView = (TextView) rootView.findViewById(R.id.location);
83
84 // get the ViewGroup to contain the daily forecast layouts
85 LinearLayout containerLinearLayout =
86 (LinearLayout) rootView.findViewById(R.id.containerLinearLayout);
87
88 int id; // int identifier for the daily forecast layout
89
90 // if we are in landscape orientation
91 if (container.getContext().getResources().getConfiguration().
92 orientation == Configuration.ORIENTATION_LANDSCAPE)
93 {
94 id = R.layout.single_forecast_layout_landscape;
95 } // end if
96 else // portrait orientation
97 {
98 id = R.layout.single_forecast_layout_portrait;
99 containerLinearLayout.setOrientation(LinearLayout.VERTICAL);
100 } // end else
101
102 // load five daily forecasts
103 View forecastView;
104 for (int i = 0; i < NUMBER_DAILY_FORECASTS; i++)
105 {
106 forecastView = inflater.inflate(id, null); // inflate new View
107
108 // add the new View to the container LinearLayout
109 containerLinearLayout.addView(forecastView);
110 dailyForecastViews[i] = forecastView;
111 } // end for
112
113 // load the location information in a background thread
114 new ReadLocationTask(zipcodeString, rootView.getContext(),
115 new WeatherLocationLoadedListener(zipcodeString,
116 rootView.getContext())).execute();
117
118 return rootView;
119 } // end method onCreateView
120
LocationLoadedListener
FiveDayForecastFragment
’s WeatherLocationLoadedListener
(Fig. 14.55) is similar to the other LocationLoadedListener
’s in the app. It receives data from a ReadLocationTask
and displays a formatted String
of location information using the locationTextView
.
121 // receives location information from background task
122 private class WeatherLocationLoadedListener implements
123 LocationLoadedListener
124 {
125 private String zipcodeString; // ZIP code to look up
126 private Context context;
127
128 // create a new WeatherLocationLoadedListener
129 public WeatherLocationLoadedListener(String zipcodeString,
130 Context context)
131 {
132 this.zipcodeString = zipcodeString;
133 this.context = context;
134 } // end WeatherLocationLoadedListener
135
136 // called when the location information is loaded
137 @Override
138 public void onLocationLoaded(String cityString, String stateString,
139 String countryString)
140 {
141 if (cityString == null) // if there is no returned data
142 {
143 // display error message
144 Toast errorToast = Toast.makeText(context,
145 context.getResources().getString(R.string.null_data_toast),
146 Toast.LENGTH_LONG);
147 errorToast.setGravity(Gravity.CENTER, 0, 0);
148 errorToast.show(); // show the Toast
149 return; // exit before updating the forecast
150 } // end if
151
152 // display the return information in a TextView
153 locationTextView.setText(cityString + " " + stateString + ", " +
154 zipcodeString + " " + countryString);
155
156 // load the forecast in a background thread
157 new ReadFiveDayForecastTask(
158 weatherForecastListener,
159 locationTextView.getContext()).execute();
160 } // end method onLocationLoaded
161 } // end class WeatherLocationLoadedListener
162
FiveDayForecastLoadedListener
The FiveDayForecastLoadedListener
(Fig. 14.56) receives an array of five DailyForecast Object
s in its onForecastLoaded
method. We display the information in the DailyForecast
s by passing them to method loadForecastIntoView
(Fig. 14.57).
163 // receives weather information from AsyncTask
164 FiveDayForecastLoadedListener weatherForecastListener =
165 new FiveDayForecastLoadedListener()
166 {
167 // when the background task looking up location information finishes
168 @Override
169 public void onForecastLoaded(DailyForecast[] forecasts)
170 {
171 // display five daily forecasts
172 for (int i = 0; i < NUMBER_DAILY_FORECASTS; i++)
173 {
174 // display the forecast information
175 loadForecastIntoView(dailyForecastViews[i], forecasts[i]);
176 } // end for
177 } // end method onForecastLoaded
178 }; // end FiveDayForecastLoadedListener
179
FiveDayForecastFragment
Method loadForecastIntoView
The loadForecastIntoView
method (Fig. 14.57) displays the information in the given DailyForecast
using the given View
. After ensuring that this Fragment
is still attached to the WeatherViewerActivity
and the given DailyForecast
is not empty, we get references to each child View
in the given ViewGroup
. These child View
s are used to display each data item in the DailyForecast
.
180 // display the given forecast information in the given View
181 private void loadForecastIntoView(View view,
182 DailyForecast dailyForecast)
183 {
184 // if this Fragment was detached while the background process ran
185 if (!FiveDayForecastFragment.this.isAdded())
186 {
187 return; // leave the method
188 } // end if
189 // if there is no returned data
190 else if (dailyForecast == null ||
191 dailyForecast.getIconBitmap() == null)
192 {
193 // display error message
194 Toast errorToast = Toast.makeText(view.getContext(),
195 view.getContext().getResources().getString(
196 R.string.null_data_toast), Toast.LENGTH_LONG);
197 errorToast.setGravity(Gravity.CENTER, 0, 0);
198 errorToast.show(); // show the Toast
199 return; // exit before updating the forecast
200 } // end else if
201
202 // get all the child Views
203 ImageView forecastImageView = (ImageView) view.findViewById(
204 R.id.daily_forecast_bitmap);
205 TextView dayOfWeekTextView = (TextView) view.findViewById(
206 R.id.day_of_week);
207 TextView descriptionTextView = (TextView) view.findViewById(
208 R.id.daily_forecast_description);
209 TextView highTemperatureTextView = (TextView) view.findViewById(
210 R.id.high_temperature);
211 TextView lowTemperatureTextView = (TextView) view.findViewById(
212 R.id.low_temperature);
213
214 // display the forecast information in the retrieved Views
215 forecastImageView.setImageBitmap(dailyForecast.getIconBitmap());
216 dayOfWeekTextView.setText(dailyForecast.getDay());
217 descriptionTextView.setText(dailyForecast.getDescription());
218 highTemperatureTextView.setText(dailyForecast.getHighTemperature());
219 lowTemperatureTextView.setText(dailyForecast.getLowTemperature());
220 } // end method loadForecastIntoView
221 } // end class FiveDayForecastFragment
ReadFiveDayForecastTask
The ReadFiveDayForecastTask
is an AsyncTask
which uses a JsonReader
to load five-day forecasts from the WeatherBug web service.
ReadFiveDayForecastTask package
Statement, import
Statements, Fields and Nested Interface FiveDayForecastLoadedListener
Figure 14.58 begins the definition of class ReadFiveDayForecastTask
and defines the fields used throughout the class. The FiveDayForecastLoadedListener
interface (lines 30–33) describes a listener capable of receiving five DailyForecast
s when the background task returns data to the GUI thread for display.
1 // ReadFiveDayForecastTask.java
2 // Read the next five daily forecasts in a background thread.
3 package com.deitel.weatherviewer;
4
5 import java.io.IOException;
6 import java.io.InputStreamReader;
7 import java.io.Reader;
8 import java.net.MalformedURLException;
9 import java.net.URL;
10
11 import android.content.Context;
12 import android.content.res.Resources;
13 import android.content.res.Resources.NotFoundException;
14 import android.graphics.Bitmap;
15 import android.os.AsyncTask;
16 import android.util.JsonReader;
17 import android.util.Log;
18
19 class ReadFiveDayForecastTask extends AsyncTask<Object, Object, String>
20 {
21 private static final String TAG = "ReadFiveDayForecastTask";
22
23 private String zipcodeString;
24 private FiveDayForecastLoadedListener weatherFiveDayForecastListener;
25 private Resources resources;
26 private DailyForecast[] forecasts;
27 private static final int NUMBER_OF_DAYS = 5;
28
29 // interface for receiver of weather information
30 public interface FiveDayForecastLoadedListener
31 {
32 public void onForecastLoaded(DailyForecast[] forecasts);
33 } // end interface FiveDayForecastLoadedListener
34
ReadFiveDayForecastTask
ConstructorThe ReadFiveDayForecastTask
constructor (Fig. 14.59) receives the selected city’s zipcodeString
, a FiveDayForecastLoadedListener
and the WeatherViewerActivity
’s Context
. We initialize the array to hold the five DailyForecast
s.
35 // creates a new ReadForecastTask
36 public ReadFiveDayForecastTask(String zipcodeString,
37 FiveDayForecastLoadedListener listener, Context context)
38 {
39 this.zipcodeString = zipcodeString;
40 this.weatherFiveDayForecastListener = listener;
41 this.resources = context.getResources();
42 this.forecasts = new DailyForecast[NUMBER_OF_DAYS];
43 } // end constructor ReadFiveDayForecastTask
44
ReadFiveDayForecastTask
Method doInBackground
Method doInBackground
(Fig. 14.60) invokes the web service in a separate thread. We create an InputStreamReader
accessing the WeatherBug web service at the location described by the webServiceURL
. After accessing the first object in the JSON document (line 62), we read the next name and ensure that it describes a forecast list. We then begin reading the next array (line 70) and call forecastJsonRead
’s skipValue
to skip the next object. This skips all the values in the first object that describes the current weather conditions. Next, we call readDailyForecast
for the next five objects, which contain the next five daily forecasts.
45 @Override
46 protected String doInBackground(Object... params)
47 {
48 // the url for the WeatherBug JSON service
49 try
50 {
51 URL webServiceURL = new URL("http://i.wxbug.net/REST/Direct/" +
52 "GetForecast.ashx?zip="+ zipcodeString + "&ht=t&ht=i&"
53 + "nf=7&ht=cp&ht=fl&ht=h&api_key=YOUR_API_KEY");
54
55 // create a stream Reader from the WeatherBug url
56 Reader forecastReader = new InputStreamReader(
57 webServiceURL.openStream());
58
59 // create a JsonReader from the Reader
60 JsonReader forecastJsonReader = new JsonReader(forecastReader);
61
62 forecastJsonReader.beginObject(); // read the next Object
63
64 // get the next name
65 String name = forecastJsonReader.nextName();
66
67 // if its the name expected for hourly forecast information
68 if (name.equals(resources.getString(R.string.forecast_list)))
69 {
70 forecastJsonReader.beginArray(); // start reading first array
71 forecastJsonReader.skipValue(); // skip today's forecast
72
73 // read the next five daily forecasts
74 for (int i = 0; i < NUMBER_OF_DAYS; i++)
75 {
76 // start reading the next object
77 forecastJsonReader.beginObject();
78
79 // if there is more data
80 if (forecastJsonReader.hasNext())
81 {
82 // read the next forecast
83 forecasts[i] = readDailyForecast(forecastJsonReader);
84 } // end if
85 } // end for
86 } // end if
87
88 forecastJsonReader.close(); // close the JsonReader
89
90 } // end try
91 catch (MalformedURLException e)
92 {
93 Log.v(TAG, e.toString());
94 } // end catch
95 catch (NotFoundException e)
96 {
97 Log.v(TAG, e.toString());
98 } // end catch
99 catch (IOException e)
100 {
101 Log.v(TAG, e.toString());
102 } // end catch
103 return null;
104 } // end method doInBackground
105
ReadFiveDayForecastTask
Methods readDailyForecast
and onPostExecute
Each forecast JSON object is read and processed using the readDailyForecast
method (Fig. 14.61, lines 107–161). We create a new String
array with four items and a Bitmap
to store all the forecast information. We check whether there are any unread items in the object using forecastReader
’s hasNext
method. If so, we read the next name and check if it matches one of the pieces of data we want to display. If there’s a match, we read the value using JsonReader
’s nextString
method. We pass the icon’s String
to our getIconBitmap
method to get a Bitmap
from the WeatherBug website. We skip the values of unrecognized names using JsonReader
’s skipValue
method. DailyForecast
objects encapsulate the weather information for each day.
106 // read a single daily forecast
107 private DailyForecast readDailyForecast(JsonReader forecastJsonReader)
108 {
109 // create array to store forecast information
110 String[] dailyForecast = new String[4];
111 Bitmap iconBitmap = null; // store the forecast's image
112
113 try
114 {
115 // while there is a next element in the current object
116 while (forecastJsonReader.hasNext())
117 {
118 String name = forecastJsonReader.nextName(); // read next name
119
120 if (name.equals(resources.getString(R.string.day_of_week)))
121 {
122 dailyForecast[DailyForecast.DAY_INDEX] =
123 forecastJsonReader.nextString();
124 } // end if
125 else if (name.equals(resources.getString(
126 R.string.day_prediction)))
127 {
128 dailyForecast[DailyForecast.PREDICTION_INDEX] =
129 forecastJsonReader.nextString();
130 } // end else if
131 else if (name.equals(resources.getString(R.string.high)))
132 {
133 dailyForecast[DailyForecast.HIGH_TEMP_INDEX] =
134 forecastJsonReader.nextString();
135 } // end else if
136 else if (name.equals(resources.getString(R.string.low)))
137 {
138 dailyForecast[DailyForecast.LOW_TEMP_INDEX] =
139 forecastJsonReader.nextString();
140 } // end else if
141 // if the next item is the icon name
142 else if (name.equals(resources.getString(R.string.day_icon)))
143 {
144 // read the icon name
145 iconBitmap = ReadForecastTask.getIconBitmap(
146 forecastJsonReader.nextString(), resources, 0);
147 } // end else if
148 else // there is an unexpected element
149 {
150 forecastJsonReader.skipValue(); // skip the next element
151 } // end else
152 } // end while
153 forecastJsonReader.endObject();
154 } // end try
155 catch (IOException e)
156 {
157 Log.e(TAG, e.toString());
158 } // end catch
159
160 return new DailyForecast(dailyForecast, iconBitmap);
161 } // end method readDailyForecast
162
163 // update the UI back on the main thread
164 protected void onPostExecute(String forecastString)
165 {
166 weatherFiveDayForecastListener.onForecastLoaded(forecasts);
167 } // end method onPostExecute
168 } // end class ReadFiveDayForecastTask
The onPostExecute
method (lines 164–167) returns the results to the GUI thread for display. We pass the array of DailyForecast
s back to the FiveDayForecastFragment
using its FiveDayForecastListener
’s onForecastLoaded
method.
DailyForecast
The DailyForecast
(Fig. 14.62) class encapsulates the information of a single day’s weather forecast. The class defines four public index constants used to pull information from the String array
storing the weather data. Bitmap iconBitmap
stores the forecast’s image.
1 // DailyForecast.java
2 // Represents a single day's forecast.
3 package com.deitel.weatherviewer;
4
5 import android.graphics.Bitmap;
6
7 public class DailyForecast
8 {
9 // indexes for all the forecast information
10 public static final int DAY_INDEX = 0;
11 public static final int PREDICTION_INDEX = 1;
12 public static final int HIGH_TEMP_INDEX = 2;
13 public static final int LOW_TEMP_INDEX = 3;
14
15 final private String[] forecast; // array of all forecast information
16 final private Bitmap iconBitmap; // image representation of forecast
17
18 // create a new DailyForecast
19 public DailyForecast(String[] forecast, Bitmap iconBitmap)
20 {
21 this.forecast = forecast;
22 this.iconBitmap = iconBitmap;
23 } // end DailyForecast constructor
24
25 // get this forecast's image
26 public Bitmap getIconBitmap()
27 {
28 return iconBitmap;
29 } // end method getIconBitmap
30
31 // get this forecast's day of the week
32 public String getDay()
33 {
34 return forecast[DAY_INDEX];
35 } // end method getDay
36
37 // get short description of this forecast
38 public String getDescription()
39 {
40 return forecast[PREDICTION_INDEX];
41 } // end method getDescription
42
43 // return this forecast's high temperature
44 public String getHighTemperature()
45 {
46 return forecast[HIGH_TEMP_INDEX];
47 } // end method getHighTemperature
48
49 // return this forecast's low temperature
50 public String getLowTemperature()
51 {
52 return forecast[LOW_TEMP_INDEX];
53 } // end method getLowTemperature
54 } // end class DailyForecast
The DailyForecast
constructor takes a String array
assumed to be in the correct order so that the index constants match the correct underlying data. We also provide public accessor methods for each piece of data in a DailyForecast
.
WeatherProvider
The WeatherProvider
class extends AppWidgetProvider
to update the Weather Viewer app widget. AppWidgetProvider
s are special BroadcastReceiver
s which listen for all broadcasts relevant to their app’s app widget.
WeatherProvider package
Statement, import
Statements and ConstantFigure 14.63 begins the definition of class ReadFiveDayForecastTask
and defines the fields used throughout the class. The BITMAP_SAMPLE_SIZE
constant was chosen to downsample the Bitmap
to a size that can be used with RemoteViews
—a View
hierarchy that can be displayed in another process. Android restricts the amount of data that can be passed between processes.
1 // WeatherProvider.java
2 // Updates the Weather app widget
3 package com.deitel.weatherviewer;
4
5 import android.app.IntentService;
6 import android.app.PendingIntent;
7 import android.appwidget.AppWidgetManager;
8 import android.appwidget.AppWidgetProvider;
9 import android.content.ComponentName;
10 import android.content.Context;
11 import android.content.Intent;
12 import android.content.SharedPreferences;
13 import android.content.res.Resources;
14 import android.graphics.Bitmap;
15 import android.widget.RemoteViews;
16 import android.widget.Toast;
17
18 import com.deitel.weatherviewer.ReadForecastTask.ForecastListener;
19 import com.deitel.weatherviewer.ReadLocationTask.LocationLoadedListener;
20
21 public class WeatherProvider extends AppWidgetProvider
22 {
23 // sample size for the forecast image Bitmap
24 private static final int BITMAP_SAMPLE_SIZE = 4;
25
WeatherProvider
Methods onUpdate
, getZipcode
and onReceive
The onUpdate
method (Fig. 14.64, lines 27–32) responds to broadcasts with actions matching AppWidgetManager
’s ACTION_APPWIDGET_UPDATE
constant. In this case, we call our startUpdateService
method (Fig. 14.64) to update the weather conditions.
26 // updates all installed Weather App Widgets
27 @Override
28 public void onUpdate(Context context,
29 AppWidgetManager appWidgetManager, int[] appWidgetIds)
30 {
31 startUpdateService(context); // start new WeatherService
32 } // end method onUpdate
33
34 // gets the saved ZIP code for this app widget
35 private String getZipcode(Context context)
36 {
37 // get the app's SharedPreferences
38 SharedPreferences preferredCitySharedPreferences =
39 context.getSharedPreferences(
40 WeatherViewerActivity.SHARED_PREFERENCES_NAME,
41 Context.MODE_PRIVATE);
42
43 // get the ZIP code of the preferred city from SharedPreferences
44 String zipcodeString = preferredCitySharedPreferences.getString(
45 WeatherViewerActivity.PREFERRED_CITY_ZIPCODE_KEY,
46 context.getResources().getString(R.string.default_zipcode));
47 return zipcodeString; // return the ZIP code string
48 } // end method getZipcode
49
50 // called when this AppWidgetProvider receives a broadcast Intent
51 @Override
52 public void onReceive(Context context, Intent intent)
53 {
54 // if the preferred city was changed in the app
55 if (intent.getAction().equals(
56 WeatherViewerActivity.WIDGET_UPDATE_BROADCAST_ACTION))
57 {
58 startUpdateService(context); // display the new city's forecast
59 } // end if
60 super.onReceive(context, intent);
61 } // end method onReceive
62
Method getZipcode
(lines 35–48) returns the preferred city’s ZIP code from the app’s SharedPreferences
.
Method onReceive
(lines 51–61) is called when the WeatherProvider
receives a broadcast. We check whether the given Intent
’s action matches WeatherViewerActivity.WIDGET_UPDATE_BROADCAST
. The WeatherViewerActivity
broadcasts an Intent
with this action when the preferred city changes, so the app widget can update the weather information accordingly. We call startUpdateService
to display the new city’s forecast.
WeatherProvider
Method startUpdateService
The startUpdateService
method (Fig. 14.65) starts a new IntentService
of type WeatherService
(Fig. 14.66) to update the app widget’s forecast in a background thread.
63 // start new WeatherService to update app widget's forecast information
64 private void startUpdateService(Context context)
65 {
66 // create a new Intent to start the WeatherService
67 Intent startServiceIntent;
68 startServiceIntent = new Intent(context, WeatherService.class);
69
70 // include the ZIP code as an Intent extra
71 startServiceIntent.putExtra(context.getResources().getString(
72 R.string.zipcode_extra), getZipcode(context));
73 context.startService(startServiceIntent);
74 } // end method startUpdateService
75
WeatherProvider
Nested Class WeatherService
The WeatherService IntentService
(Fig. 14.66) retrieves information from the WeatherBug web service and updates the app widget’s View
s. IntentService
’s constructor (lines 80–83) takes a String
used to name the Service
’s worker Thread
—the String
can be used for debugging purposes. Method onHandleIntent
(lines 89–101) is called when the WeatherService
is started. We get the Resources
from our application Context
and get the ZIP code from the Intent
that started the Service
. Then, we launch a ReadLocationTask
to read location information for the given ZIP code.
76 // updates the Weather Viewer app widget
77 public static class WeatherService extends IntentService
78 implements ForecastListener
79 {
80 public WeatherService()
81 {
82 super(WeatherService.class.toString());
83 } // end WeatherService constructor
84
85 private Resources resources; // the app's Resources
86 private String zipcodeString; // the preferred city's ZIP code
87 private String locationString; // the preferred city's location text
88
89 @Override
90 protected void onHandleIntent(Intent intent)
91 {
92 resources = getApplicationContext().getResources();
93
94 zipcodeString = intent.getStringExtra(resources.getString(
95 R.string.zipcode_extra));
96
97 // load the location information in a background thread
98 new ReadLocationTask(zipcodeString, this,
99 new WeatherServiceLocationLoadedListener(
100 zipcodeString)).execute();
101 } // end method onHandleIntent
102
WeatherService
Nested Class onForecastLoaded
MethodMethod onForecastLoaded
(Fig. 14.67) is called when the AsyncTask
finishes reading weather information from the WeatherBug webservice. We first check if the returned Bitmap
is null
. If it is, the ReadForecastTask
failed to return valid data, so we simply display a Toast
. Otherwise, we create a new PendingIntent
(lines 118–120) that will be used to launch the WeatherViewerActivity
if the user touches the app widget. A PendingIntent
represents an Intent
and an action to perform with that Intent
. A PendingIntent
can be passed across processes, which is why we use one here.
103 // receives weather information from the ReadForecastTask
104 @Override
105 public void onForecastLoaded(Bitmap image, String temperature,
106 String feelsLike, String humidity, String precipitation)
107 {
108 Context context = getApplicationContext();
109
110 if (image == null) // if there is no returned data
111 {
112 Toast.makeText(context, context.getResources().getString(
113 R.string.null_data_toast), Toast.LENGTH_LONG);
114 return; // exit before updating the forecast
115 } // end if
116
117 // create PendingIntent to launch WeatherViewerActivity
118 Intent intent = new Intent(context, WeatherViewerActivity.class);
119 PendingIntent pendingIntent = PendingIntent.getActivity(
120 getBaseContext(), 0, intent, 0);
121
122 // get the App Widget's RemoteViews
123 RemoteViews remoteView = new RemoteViews(getPackageName(),
124 R.layout.weather_app_widget_layout);
125
126 // set the PendingIntent to launch when the app widget is clicked
127 remoteView.setOnClickPendingIntent(R.id.containerLinearLayout,
128 pendingIntent);
129
130 // display the location information
131 remoteView.setTextViewText(R.id.location, locationString);
132
133 // display the temperature
134 remoteView.setTextViewText(R.id.temperatureTextView,
135 temperature + (char)0x00B0 + resources.getString(
136 R.string.temperature_unit));
137
138 // display the "feels like" temperature
139 remoteView.setTextViewText(R.id.feels_likeTextView, feelsLike +
140 (char)0x00B0 + resources.getString(R.string.temperature_unit));
141
142 // display the humidity
143 remoteView.setTextViewText(R.id.humidityTextView, humidity +
144 (char)0x0025);
145
146 // display the chance of precipitation
147 remoteView.setTextViewText(R.id.precipitationTextView,
148 precipitation + (char)0x0025);
149
150 // display the forecast image
151 remoteView.setImageViewBitmap(R.id.weatherImageView, image);
152
153 // get the Component Name to identify the widget to update
154 ComponentName widgetComponentName = new ComponentName(this,
155 WeatherProvider.class);
156
157 // get the global AppWidgetManager
158 AppWidgetManager manager = AppWidgetManager.getInstance(this);
159
160 // update the Weather AppWdiget
161 manager.updateAppWidget(widgetComponentName, remoteView);
162 } // end method onForecastLoaded
163
When updating an app widget from an AppWidgetProvider
, you do not update the app widget’s View
s directly. The app widget is actually in a separate process from the AppWidgetProvider
. Communication between the two is achieved through an object of class RemoteViews
. We create a new RemoteViews
object for the app widget’s layout (lines 123–124). We then pass the PendingIntent
to remoteView
’s setOnClickPendingIntent
(lines 127–128), which registers the app widget’s PendingIntent
that’s launched when the user touches the app widget to lauch the Weather Viewer app. We specify the layout ID of the root View
in the app widget’s View
hierarchy. We update the app widget’s TextView
s by passing each TextView
resource ID and the desired text to RemoteView
’s setTextViewText
method. The image is displayed in an ImageView
using RemoteView
’s setImageViewBitmap
. We create a new ComponentName
(lines 154–155) representing the WeatherProvider
application component. We get a reference to this app’s AppWidgetManager
using its static getInstance
method (line 158). We pass the ComponentName
and RemoteViews
to AppWidgetManager
’s updateAppWidget
method (line 161) to apply the changes made to the RemoteViews
to the app widget’s Views
.
WeatherService
’s WeatherServiceLocationLoadedListener
ClassThe WeatherServiceLocationLoadedListener
(Fig. 14.68) receives location information read from the WeatherBug web service in an AsyncTask
. In onLocationLoaded
(lines 177–202), we construct a String
using the returned data then execute a new ReadForecastTask
to begin reading the weather information for the current weather conditions of the preferred city. We set the forecast Bitmap
’s sample size using ReadForecastTask
’s setSampleSize
method. There is a size limit on Bitmap
s that can displayed using RemoteViews
.
164 // receives location information from background task
165 private class WeatherServiceLocationLoadedListener
166 implements LocationLoadedListener
167 {
168 private String zipcodeString; // ZIP code to look up
169
170 // create a new WeatherLocationLoadedListener
171 public WeatherServiceLocationLoadedListener(String zipcodeString)
172 {
173 this.zipcodeString = zipcodeString;
174 } // end WeatherLocationLoadedListener
175
176 // called when the location information is loaded
177 @Override
178 public void onLocationLoaded(String cityString,
179 String stateString, String countryString)
180 {
181 Context context = getApplicationContext();
182
183 if (cityString == null) // if there is no returned data
184 {
185 Toast.makeText(context, context.getResources().getString(
186 R.string.null_data_toast), Toast.LENGTH_LONG);
187 return; // exit before updating the forecast
188 } // end if
189
190 // display the return information in a TextView
191 locationString = cityString + " " + stateString + ", " +
192 zipcodeString + " " + countryString;
193
194 // launch a new ReadForecastTask
195 ReadForecastTask readForecastTask = new ReadForecastTask(
196 zipcodeString, (ForecastListener) WeatherService.this,
197 WeatherService.this);
198
199 // limit the size of the Bitmap
200 readForecastTask.setSampleSize(BITMAP_SAMPLE_SIZE);
201 readForecastTask.execute();
202 } // end method onLocationLoaded
203 } // end class WeatherServiceLocationLoadedListener
204 } // end class WeatherService
205 } // end WeatherProvider
In this chapter, we presented the Weather Viewer app and its companion app widget. The app used various features new to Android 3.x.
You learned how to use fragments to create and manage portions of the app’s GUI. You used subclasses of Fragment
, DialogFragment
and ListFragment
to create a robust user interface and to take advantage of a tablet’s screen size. You learned that each Fragment
has a life cycle and it must be hosted in a parent Activity
. You used a a FragmentManager
to manage the Fragment
s and a FragmentTransaction
to add, remove and transition between Fragment
s.
You used the Android 3.x action bar at the top of the screen to display the app’s options menu and tabbed navigation elements. You also used long-touch event handling to allow the user to select a city as the preferred one or to delete the city. The app also used JsonReader
to read JSON objects containing the weather data from the WeatherBug web services.
You created a a companion app widget (by extending class AppWidgetProvider
) to display the current weather conditions for the user’s preferred city, as set in the app. To launch the app when the user touched the widget, you used a PendingIntent
. When the user changed preferred cities, the app used an Intent
to broadcast the change to the app widget.
We hope you enjoyed reading Android How to Program as much as we enjoyed writing it. We’d appreciate your feedback. Please send your questions, comments, suggestions and corrections to [email protected]. Check out our growing list of Android-related Resource Centers at www.deitel.com/ResourceCenters.html
. To stay up to date with the latest news about Deitel publications and corporate training, sign up for the free weekly Deitel® Buzz Online e-mail newsletter at www.deitel.com/newsletter/subscribe.html
, and follow us on Facebook (www.deitel.com/deitelfan
) and Twitter (@deitel
). To learn more about Deitel & Associates’ worldwide on-site programming training for your company or organization, visit www.deitel.com/training or e-mail [email protected].
14.1. Fill in the blanks in each of the following statements:
a. A ListFragment
is a Fragment
containing a(n) __________.
b. A FragmentTransaction
(package android.app
) obtained from the __________ allows an Activity
to add, remove and transition between Fragment
s.
c. We extend class AppWidgetProvider
__________(package android.appwidget
), a subclass of (package android.content
), to create an app widget and allow it to receive notifications from the system when the app widget is enabled, disabled, deleted or updated.
d. You can force an item to appear in the ActionBar
by using the always
value of attribute __________ but you risk overlapping menu items by doing so.
14.2. State whether each of the following is true or false. If false, explain why.
a. Fragments were introduced in Android 3.x and cannot be used with earlier versions of Android.
b. The action bar can display the app’s options menu, navigation elements (such as tabbed navigation) and other interactive GUI components.
c. Unlike activities, services need not be registered in the manifest.
d. JSON (JavaScript Object Notation)—a simple way to represent JavaScript objects as numbers—is an alternative to XML for passing data between the client and the server.
e. Arrays are represented in JSON with curly braces in the following format:
{ value1, value2, value3 }
f. Class JsonReader
provides methods for reading boolean
s, double
s, int
s, long
s and String
s.
g. A PendingIntent
cannot be passed across processes.
h. Use Fragment
s to create reusable components and make better use of the screen real estate in a tablet app.
a. ListView
.
b. FragmentManager
.
c. BroadcastReceiver
.
d. android:showAsAction
.
a. False. Though fragments were introduced in Android 3.x, there’s a compatibility package that enables you to use them with earlier versions of Android.
b. True.
c. False. Like activities, all services must be registered in the manifest; otherwise, they cannot be executed.
d. False. JSON (JavaScript Object Notation)—a simple way to represent JavaScript objects as strings—is an alternative to XML for passing data between the client and the server.
e. False. Arrays are represented in JSON with square brackets.
f. True.
g. False. A PendingIntent
can be passed across processes.
h. True.
14.4. Fill in the blanks in each of the following statements:
a. An Activity
’s __________ (package android.content.res
) can be used to determine the current orientation.
b. Use a(n) __________ (package android.util
) to read JSON objects.
c. The attribute android:showAsAction
defines how a menu item should appear in the ActionBar
. The value __________ specifies that this item should be visible in the ActionBar
if there’s room to lay it out completely.
d. We get the name from the next name–value pair in a JSON object by calling JsonReader
’s __________ method.
e. You use a FragmentManager
to manage Fragment
s and a(n) __________ to add, remove and transition between Fragment
s.
14.5. State whether each of the following is true or false. If false, explain why.
a. Fragments are a key feature of Android 3.x.
b. The base class of all fragments is BaseFragment
(package android.app
).
c. Like an Activity
, each Fragment
has a life cycle.
d. Fragment
s can be executed independently of a parent Activity
.
e. It’s common practice to allow a user to launch an app by touching the app’s companion widget on the device’s home screen.
f. Each object in JSON is represented as a list of property names and values contained in curly braces, in the following format:
{ "propertyName1" : value1, "propertyName2'": value2 }
g. Each value in a JSON array can be a string, a number, a JSON representation of an object, true
, false
or null
.
h. When updating an app widget from an AppWidgetProvider
, you update the app widget’s View
s directly.
14.6. (Enhanced Weather Viewer App) Make the following enhancements to the Weather Viewer app—some of these require the Facebook and Twitter web-service APIs:
a. Include video of the current local forecast.
b. Include hourly, two-day and 10-day forecasts.
c. Use location-based services and alerts to warn users about severe weather nearby.
d. Allow users to post weather notices on Twitter and Facebook.
e. Allow users to record video or take pictures of current weather conditions (e.g., storms) and submit them to be shared with other users via Facebook and Twitter.
14.7. (Enhanced Favorite Twitter Searches App) Make the following enhancements to the Favorite Twitter Searches app—some of these require the Twitter web-service APIs:
a. Create an option for following the top five Twitter trends—popular topics being discussed on Twitter.
b. Add the ability to retweet tweets that you find in your searches.
c. Add a feature that suggests people to follow based on the user’s favorite Twitter searches.
d. Add translation capabilities to read Tweets in other languages.
e. Share on Facebook.
f. View all replies related to a tweet.
g. Enable the user to reply to a tweet in the search results.
h. Create an App Widget for the Favorite Twitter Searches app that allows the user to perform searches with the app from the home screen.
14.8. (Twitter App) Investigate the Twitter APIs, then use the APIs in an app that includes at least three of the following features:
a. Post a tweet from within the app to Twitter and Facebook simultaneously.
b. Group tweets from favorite twitterers into lists (e.g., friends, colleagues, celebrities).
c. Hide specific twitterers from the feed without “unfollowing” them.
d. Manage multiple accounts from the same app.
e. Color code tweets in the feed from favorite twitterers or tweets that contain specific keywords.
f. Save tweets to a document to read later.
g. Geo tag tweets so readers can see the user’s location when the tweet was posted.
h. Reply to tweets from within the app.
i. Retweet from within the app.
j. Use the APIs from a URL shortening service to enable the user to shorten URLs to include in tweets.
k. Save drafts of tweets to post later.
l. Display updates when a favorite posts a new tweet.
14.9. (Enhanced Shopping List App) Enhance the app from Exercise 10.9 with location services so that the user is alerted when near a business that offers an item or service on the list. Use web services to find the stores with the best prices.
14.10. (Enhanced Jigsaw Puzzle Quiz App Enhancement) Enhance the app you created in Exercise 8.12 by using Flickr web services (www.flickr.com/services/api/
) to obtain the images displayed in the app.
14.11. (Enhanced Quiz App) Modify the Flag Quiz app in Chapter 6 to create your own quiz app that shows videos rather than images. Possible quizzes could include U.S. presidents, world landmarks, movie stars, recording artists, and more. Consider using YouTube web services to obtain videos for display in the app. (Be sure to read the YouTube API terms of service at http://code.google.com/apis/youtube/terms.html
.)
14.12. (Enhanced Word Scramble Game App) Modify the app from Exercise 5.6 to use an online dictionary’s web services to select the words and the definitions that are used for hints.
14.13. (Enhanced Crossword Puzzle Generator App) Modify the app from Exercise 10.12 to use an online dictionary’s web services to select the words and the definitions that are used for hints.
14.14. (Enhanced Color Swiper App) Modify the app from Exercise 13.12 to use Flickr web services (www.flickr.com/services/api/
) to obtain the images displayed in the app. Allow the user to specify search terms for selecting images from Flickr.
14.15. (Sudoku App) Modify and enhance the Open Sudoku app available at http://code.google.com/p/opensudoku-android/
. Allow the users to take a picture of a Sudoku game from a book, magazine or newspaper and play the game on the device.
Web services, inexpensive computers, abundant high-speed Internet access, open source software and many other elements have inspired new, exciting, lightweight business models that people can launch with only a small investment. Some types of websites with rich and robust functionality that might have required hundreds of thousands or even millions of dollars to build in the 1990s can now be built for nominal sums. In Chapter 1, we introduced the application-development methodology of mashups, in which you can rapidly develop powerful and intriguing applications by combining (often free) complementary web services and other forms of information feeds. One of the first mashups was www.housingmaps.com, which combines the real estate listings provided by www.craigslist.org with the mapping capabilities of Google Maps—the most widely-used web-service API—to offer maps that show the locations of apartments for rent in a given area. Figure 1.8 provided a list of several popular web services available from companies including Google, Facebook, eBay, Netflix, Skype and more.
Check out the catalog of web-service APIs at www.programmableweb.com and the apps in Android Market for inspiration. It’s important to read the terms of service for the APIs before building your apps. Some APIs are free while others may charge fees. There also may be restrictions on the frequency with which your app may query the server.
14.16. (Mashup) Use your imagination to create a mashup app using at least two APIs of your choice.
14.17. (News Aggregator App) Use web services to create a news aggregator app that gathers news from multiple sources.
14.18. (Enhanced News Aggregator App) Enhance the News Aggregator app using a maps API. Allow the user to select a region of the world. When the user clicks on a region, display the headlines from the multiple news sources.
14.19. (Shopping Mashup App) Create a location-based shopping app using APIs from CityGrid® (www.citygridmedia.com/developer/
) or a similar shopping service. Add background music to your app using APIs from a service such as Last.fm (www.last.fm/api
) so the user can listen while shopping.
14.20. (Daily Deals Mashup App) Create a local daily deals app using Groupon APIs (www.groupon.com/pages/api
) or those of a similar service.
14.21. (Wine Country Mashup App) Create a mashup using a mapping API to help a wine enthusiast plan a trip to wine country. Allow the user to select a type of wine, and identify on a map vineyards that produce that wine. Include information about the wine and about the vineyards.
14.22. (Idiomatic Expressions Translator Mashup App) An idiomatic expression is a common, often strange saying whose meaning cannot be understood from the words in the expression. For example, you might say your favorite sports team is going to “eat [their opponent] for lunch,” or “blow [their opponent] out of the water” to indicate that you predict your team will win decisively. Search the web to find popular idiomatic expressions. Create an app that allows the user to enter an idiomatic expression by text or speech, then translate the expression into a foreign language and then back to English. Use a translation API (such as Bing) to perform the translation. Allow the user to select the foreign language. Display the results in English—they may be funny or interesting.
14.23. (Name That Song App) Check your favorite music sites to see if they have a web services API. Using a music web services API, create a quiz app (similar to the Flag Quiz app in Chapter 6) that plays a song and asks the user to name the song. Other features to include:
a. Add three lifelines that allow you to call one contact, SMS one contact and e-mail one contact for help answering a question. Once each lifeline is used, disable the capability for that quiz.
b. Add a timer function so that the user must answer each question within 10 seconds.
c. Add multiplayer functionality that allows two users to play on the same device.
d. Add muliplayer functionality to allow users on different devices to compete in the same game.
e. Keep track of the user’s score and display it as a percentage at the bottom of the screen throughout the quiz.