14.5.5. Class SingleForecastFragment

The SingleForecastFragment is a subclass of Fragment designed to display the current conditions for a city.

SingleForecastFragment package Statement, import Statements and Fields

Figure 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   


Fig. 14.35. SingleForecastFragment package statement, import statements and fields.

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   


Fig. 14.36. SingleForecastFragment overloaded method newInstance.

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  


Fig. 14.37. SingleForecastFragment methods onCreate, onSaveInstanceState and getZipcode.

Method onSaveInstanceState (lines 85–102) saves the forecast information currently displayed by the Fragment so we do not need to launch new AsyncTasks 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.

Overriding Method 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 Views 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        


Fig. 14.38. Overriding method onCreateView.

Overriding Method 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 Views 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 Views.


 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  


Fig. 14.39. Overriding method onActivityCreated.

Implementing Interface 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 Views.


 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  


Fig. 14.40. Implementing interface ForecastListener.

Implementing Interface 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


Fig. 14.41. Implementing interface LocationLoadedListener.

14.5.6. Class 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. Strings 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 Fields

Figure 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   


Fig. 14.42. ReadLocationTask package statement, import statements and fields.

Nested Interface LocationLoadedListener and the ReadLocationTask Constructor

Nested 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 Strings 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   


Fig. 14.43. Nested interface LocationLoadedListener and ReadLocationTask’s constructor.

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  


Fig. 14.44. ReadLocationTask method doInBackground.

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 JsonReaderbeginObject 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 booleans, doubles, ints, longs and Strings—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 Strings 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


Fig. 14.45. ReadLocationTask method onPostExecute.

14.5.7. Class ReadForecastTask

The ReadForecastTask retrieves the current weather conditions for a given ZIP code.

ReadForecastTask package Statement, import Statements and Fields

Figure 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   


Fig. 14.46. ReadForecastTask package statement, import statements and fields.

The ForecastListener interface (lines 37–41) describes a listener capable of receiving the forecast image Bitmap and Strings representing the current temperature, feels-like temperature, humidity and chance of precipitation.

ReadForecastTask Constructor and setSampleSize Methods

The 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   


Fig. 14.47. ReadForecastTask constructor and setSampleSize methods.

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 Bitmaps 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 Strings 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  


Fig. 14.48. ReadForecastTask methods doInBackground and onPostExecute.

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  


Fig. 14.49. ReadForecastTask method getIconBitmap.

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. JsonReader’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


Fig. 14.50. ReadForecastTask method readForecast.

14.5.8. Class FiveDayForecastFragment

The FiveDayForecastFragment displays the five-day forecast for a single city.

FiveDayForecastFragment package Statement, import Statements and Fields

In 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      


Fig. 14.51. FiveDayForecastFragment package statement, import statements and fields.

FiveDayForecastFragment Overloaded newInstance Methods

Similar to the SingleForecastFragment, we provide overloaded newInstance method (Fig. 14.52) to create new FiveDayForecastFragments. 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   


Fig. 14.52. FiveDayForecastFragment overloaded newInstance methods.

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   


Fig. 14.53. FiveDayForecastFragment methods onCreate and getZipCode.

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  


Fig. 14.54. FiveDayForecastFragment method onCreateView.

Implementing Interface 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  


Fig. 14.55. Implementing interface LocationLoadedListener.

Implementing Interface FiveDayForecastLoadedListener

The FiveDayForecastLoadedListener (Fig. 14.56) receives an array of five DailyForecast Objects in its onForecastLoaded method. We display the information in the DailyForecasts 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


Fig. 14.56. Implementing interface FiveDayForecastLoadedListener.

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 Views 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


Fig. 14.57. FiveDayForecastFragment method loadForecastIntoView.

14.5.9. Class 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 DailyForecasts 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   


Fig. 14.58. Class ReadFiveDayForecast.

ReadFiveDayForecastTask Constructor

The 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 DailyForecasts.


 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   


Fig. 14.59. ReadFiveDayForecast constructor.

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


Fig. 14.60. ReadFiveDayForecastTask method doInBackground.

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


Fig. 14.61. ReadFiveDayForecastTask methods readDailyForecast and onPostExecute.

The onPostExecute method (lines 164–167) returns the results to the GUI thread for display. We pass the array of DailyForecasts back to the FiveDayForecastFragment using its FiveDayForecastListener’s onForecastLoaded method.

14.5.10. Class 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


Fig. 14.62. 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.

14.5.11. Class WeatherProvider

The WeatherProvider class extends AppWidgetProvider to update the Weather Viewer app widget. AppWidgetProviders are special BroadcastReceivers which listen for all broadcasts relevant to their app’s app widget.

WeatherProvider package Statement, import Statements and Constant

Figure 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    


Fig. 14.63. WeatherProvider package statement, import statements and constant.

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   


Fig. 14.64. WeatherProvider methods onUpdate, getZipcode and onReceive.

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   


Fig. 14.65. WeatherProvider method startUpdateService.

WeatherProvider Nested Class WeatherService

The WeatherService IntentService (Fig. 14.66) retrieves information from the WeatherBug web service and updates the app widget’s Views. 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   


Fig. 14.66. WeatherProvider nested class WeatherService.

WeatherService Nested Class onForecastLoaded Method

Method 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  


Fig. 14.67. WeatherService nested class onForecastLoaded method.

When updating an app widget from an AppWidgetProvider, you do not update the app widget’s Views 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 TextViews 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 Class

The 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 Bitmaps 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


Fig. 14.68. WeatherService’s WeatherServiceLocationLoadedListener class.

14.6. Wrap-Up

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 Fragments and a FragmentTransaction to add, remove and transition between Fragments.

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.

Staying in Contact with Deitel & Associates, Inc.

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].

Self-Review Exercises

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 Fragments.

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 booleans, doubles, ints, longs and Strings.

g. A PendingIntent cannot be passed across processes.

h. Use Fragments to create reusable components and make better use of the screen real estate in a tablet app.

Answers to Self-Review Exercises

14.1.

a. ListView.

b. FragmentManager.

c. BroadcastReceiver.

d. android:showAsAction.

14.3.

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.

Exercises

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 Fragments and a(n) __________ to add, remove and transition between Fragments.

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. Fragments 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 Views 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 and Mashups

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset