Friday, March 22, 2013

Share IntentService among Fragments

The post "Perform background processing with IntentService" demonstrate how to use IntentService in Activity. In this exercise, we are going to modify the exercise of "Yahoo Weather" to demonstrate how to share one IntentService by three Fragments. Here one IntentService means one common IntentService class, not one common IntentService object.




Create MyIntentService.java, it's our common IntentService class, to load Yahoo Weather in background service.
package com.example.androidyahooweatherdom;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import android.app.IntentService;
import android.content.Intent;

public class MyIntentService extends IntentService {
 
 String ExtraAction;
 String woeid;
 MyWeather weatherResult;
 
 
 public static final String EXTRA_KEY_ACTION = "EXTRA_ACTION";
 public static final String EXTRA_KEY_WOEID = "EXTRA_WOEID";
 public static final String EXTRA_KEY_WEATHER = "EXTRA_WEATHER";
 
 class MyWeather{
  String description;
  String city;
  String region;
  String country;

  String windChill;
  String windDirection;
  String windSpeed;

  String sunrise;
  String sunset;

  String conditiontext;
  String conditiondate;
  
  String numberOfForecast;
  String forecast;

  public String toString(){
   
   return "\n- " + description + " -\n\n"
    + "city: " + city + "\n"
    + "region: " + region + "\n"
    + "country: " + country + "\n\n"

    + "Wind\n"
    + "chill: " + windChill + "\n"
    + "direction: " + windDirection + "\n"
    + "speed: " + windSpeed + "\n\n"

    + "Sunrise: " + sunrise + "\n"
    + "Sunset: " + sunset + "\n\n"

    + "Condition: " + conditiontext + "\n"
    + conditiondate +"\n"
    
    + "\n"
    + "number of forecast: " + numberOfForecast + "\n"
    + forecast;
   
  } 
 }

 public MyIntentService() {
  super("com.example.androidintentservice.MyIntentService");
 }

 @Override
 protected void onHandleIntent(Intent intent) {
  
  //get input
  ExtraAction = intent.getStringExtra(EXTRA_KEY_ACTION);
  woeid = intent.getStringExtra(EXTRA_KEY_WOEID);
  
  loadYahooWeather();
  
  //dummy delay
  try {
   Thread.sleep(3000);
  } catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  
  //return result
  Intent intentResponse = new Intent();
  intentResponse.setAction(ExtraAction);
  intentResponse.addCategory(Intent.CATEGORY_DEFAULT);
  intentResponse.putExtra(EXTRA_KEY_WEATHER, weatherResult.toString());
  sendBroadcast(intentResponse);
 }
 
 protected void loadYahooWeather(){
  
  String weatherString = QueryYahooWeather();
  Document weatherDoc = convertStringToDocument(weatherString);
  weatherResult = parseWeather(weatherDoc);
 }

 private MyWeather parseWeather(Document srcDoc){
     
     MyWeather myWeather = new MyWeather();

     //<description>Yahoo! Weather for New York, NY</description>
     myWeather.description = srcDoc.getElementsByTagName("description")
       .item(0)
       .getTextContent();

     //<yweather:location.../>
     Node locationNode = srcDoc.getElementsByTagName("yweather:location").item(0);
     myWeather.city = locationNode.getAttributes()
       .getNamedItem("city")
       .getNodeValue()
       .toString();
     myWeather.region = locationNode.getAttributes()
       .getNamedItem("region")
       .getNodeValue()
       .toString();
     myWeather.country = locationNode.getAttributes()
       .getNamedItem("country")
       .getNodeValue()
       .toString();

     //<yweather:wind.../>
     Node windNode = srcDoc.getElementsByTagName("yweather:wind").item(0);
     myWeather.windChill = windNode.getAttributes()
       .getNamedItem("chill")
       .getNodeValue()
       .toString();
     myWeather.windDirection = windNode.getAttributes()
       .getNamedItem("direction")
       .getNodeValue()
       .toString();
     myWeather.windSpeed = windNode.getAttributes()
       .getNamedItem("speed")
       .getNodeValue()
       .toString();

     //<yweather:astronomy.../>
     Node astronomyNode = srcDoc.getElementsByTagName("yweather:astronomy").item(0);
     myWeather.sunrise = astronomyNode.getAttributes()
       .getNamedItem("sunrise")
       .getNodeValue()
       .toString();
     myWeather.sunset = astronomyNode.getAttributes()
       .getNamedItem("sunset")
       .getNodeValue()
       .toString();

     //<yweather:condition.../>
     Node conditionNode = srcDoc.getElementsByTagName("yweather:condition").item(0);
     myWeather.conditiontext = conditionNode.getAttributes()
       .getNamedItem("text")
       .getNodeValue()
       .toString();
     myWeather.conditiondate = conditionNode.getAttributes()
       .getNamedItem("date")
       .getNodeValue()
       .toString();
     
     //Added to get elements of <yweather:forecast.../>
     NodeList forecastList = srcDoc.getElementsByTagName("yweather:forecast");
     
     myWeather.forecast = "";
     if(forecastList.getLength() > 0){
      myWeather.numberOfForecast = String.valueOf(forecastList.getLength());
      for(int i = 0; i < forecastList.getLength(); i++){
       Node forecastNode = forecastList.item(i);
       myWeather.forecast +=
         forecastNode
          .getAttributes()
          .getNamedItem("date")
          .getNodeValue()
          .toString() + " " +
         forecastNode
          .getAttributes()
          .getNamedItem("text")
          .getNodeValue()
          .toString() +
         " High: " + forecastNode
             .getAttributes()
             .getNamedItem("high")
             .getNodeValue()
             .toString() +
         " Low: " + forecastNode
             .getAttributes()
             .getNamedItem("low")
             .getNodeValue()
             .toString() + "\n";
      }
     }else{
      myWeather.numberOfForecast = "No forecast";
     }
     
     return myWeather; 
    }
    
    private Document convertStringToDocument(String src){
     
     Document dest = null;
     DocumentBuilderFactory dbFactory =
       DocumentBuilderFactory.newInstance();
     DocumentBuilder parser;
     
     try {
      parser = dbFactory.newDocumentBuilder();
      dest = parser.parse(new ByteArrayInputStream(src.getBytes())); 
     } catch (ParserConfigurationException e1) {
      e1.printStackTrace(); 
     } catch (SAXException e) {
      e.printStackTrace(); 
     } catch (IOException e) {
      e.printStackTrace(); 
     }
     
     return dest; 
    }

    private String QueryYahooWeather(){
     
     String qResult = "";
     String queryString = "http://weather.yahooapis.com/forecastrss?w=" + woeid;
     
     HttpClient httpClient = new DefaultHttpClient();
     HttpGet httpGet = new HttpGet(queryString);
       
     try {
      HttpEntity httpEntity = httpClient.execute(httpGet).getEntity();
      
      if (httpEntity != null){
       InputStream inputStream = httpEntity.getContent();
       Reader in = new InputStreamReader(inputStream);
       BufferedReader bufferedreader = new BufferedReader(in);
       StringBuilder stringBuilder = new StringBuilder();
          
       String stringReadLine = null;

       while ((stringReadLine = bufferedreader.readLine()) != null) {
        stringBuilder.append(stringReadLine + "\n"); 
       }
          
       qResult = stringBuilder.toString(); 
      } 
     } catch (ClientProtocolException e) {
      e.printStackTrace(); 
     } catch (IOException e) {
      e.printStackTrace();
     }
     
     return qResult; 
    }

}


Modify AndroidManifest.xml to add <service> of "MyIntentService", and add permission of "android.permission.INTERNET".
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.androidyahooweatherdom"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.androidyahooweatherdom.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name="MyIntentService"></service>
    </application>

</manifest>


res/layout/fragmentlayout.xml, layout of our fragments.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView
        android:id="@+id/status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textStyle="bold" />
    <ScrollView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
        <TextView
            android:id="@+id/weather"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
    </ScrollView>

</LinearLayout>


Creater a abstract class MyAbsYWeatherFragment extends Fragment, all our fragments will extend it. All it's sub-class have to override init_country() method to provide the target city of Yahoo Weather, a string of ACTION_RESPONSE for IntentFilter, and boolean retainInst to determine is it need retain instance. It will start our IntentService and handle all job for our fragments.
package com.example.androidyahooweatherdom;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public abstract class MyAbsYWeatherFragment extends Fragment {

 TextView weather, status;
 String stringWeatherResult;

 private MyBroadcastReceiver myBroadcastReceiver;
 
 String woeid;
 String ACTION_RESPONSE;
 boolean retainInst;
 abstract void init_country();
 
 public MyAbsYWeatherFragment(){
  super();
  init_country();
 }
 
 @Override
 public View onCreateView(LayoutInflater inflater, ViewGroup container,
   Bundle savedInstanceState) {
  View myFragmentView = inflater.inflate(R.layout.fragmentlayout, container, false);
  weather = (TextView)myFragmentView.findViewById(R.id.weather);
        status = (TextView)myFragmentView.findViewById(R.id.status);
        
        myBroadcastReceiver = new MyBroadcastReceiver();
        
        //register BroadcastReceiver
        IntentFilter intentFilter = new IntentFilter(ACTION_RESPONSE);
        intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
        getActivity().registerReceiver(myBroadcastReceiver, intentFilter);
        
  return myFragmentView;
 }

 @Override
 public void onActivityCreated(Bundle savedInstanceState) {
  // TODO Auto-generated method stub
  super.onActivityCreated(savedInstanceState);
  
  if(stringWeatherResult != null){
   weather.setText(stringWeatherResult);
   status.setText("Reloaded previous weatherResult");
  }else{
   status.setText("loadYahooWeather()");
   startServiceToLoadYahooWeather();
  }
  
  setRetainInstance(retainInst);
 }
 
 @Override
 public void onDestroyView() {
  // TODO Auto-generated method stub
  super.onDestroyView();
  //un-register BroadcastReceiver
  getActivity().unregisterReceiver(myBroadcastReceiver);
 }

 protected void startServiceToLoadYahooWeather(){
  Intent intentMyIntentService = new Intent(getActivity().getApplicationContext(), MyIntentService.class);
  intentMyIntentService.putExtra(MyIntentService.EXTRA_KEY_ACTION, ACTION_RESPONSE);
  intentMyIntentService.putExtra(MyIntentService.EXTRA_KEY_WOEID, woeid);
  getActivity().startService(intentMyIntentService);
 }
 
 public class MyBroadcastReceiver extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {
   stringWeatherResult = intent.getStringExtra(MyIntentService.EXTRA_KEY_WEATHER);

   FragmentActivity parentActivity = getActivity();
   if(parentActivity != null){
       parentActivity.runOnUiThread(new Runnable(){
           
           @Override
           public void run() {
            weather.setText(stringWeatherResult);
            status.setText("Finished"); 
           }});
      }
  }

 }

}


Create three sub-class of MyAbsYWeatherFragment, Fragment1, Fragment2 and Fragment3. They are the actual fragments in the layout.

Fragment1.java
package com.example.androidyahooweatherdom;

public class Fragment1 extends MyAbsYWeatherFragment {

 @Override
 void init_country() {
  //New York
  woeid = "2459115";
  ACTION_RESPONSE = "androidyahooweatherdom.RESPONSE.NewYork";
  retainInst = false;
 }
 
}


Fragment2.java
package com.example.androidyahooweatherdom;

public class Fragment2 extends MyAbsYWeatherFragment {

 @Override
 void init_country() {
  //New York
  woeid = "2459115";
  ACTION_RESPONSE = "androidyahooweatherdom.RESPONSE.NewYork";
  retainInst = true;
 }

}


Fragment3.java
package com.example.androidyahooweatherdom;

public class Fragment3 extends MyAbsYWeatherFragment {

 @Override
 void init_country() {
  //London
  woeid = "23416974";
  ACTION_RESPONSE = "androidyahooweatherdom.RESPONSE.London";
  retainInst = true;
 }

}


Modify /res/layout/activity_main.xml and /res/layout-land/activity_main.xml to add the three fragments in normal and landscape orientation.

/res/layout/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <fragment
        class="com.example.androidyahooweatherdom.Fragment1"
        android:id="@+id/fragment1"
        android:layout_width="match_parent"
        android:layout_height="0px"
        android:layout_weight="1" />
    <fragment
        class="com.example.androidyahooweatherdom.Fragment2"
        android:id="@+id/fragment2"
        android:layout_width="match_parent"
        android:layout_height="0px"
        android:layout_weight="1" />
    <fragment
        class="com.example.androidyahooweatherdom.Fragment3"
        android:id="@+id/fragment3"
        android:layout_width="match_parent"
        android:layout_height="0px"
        android:layout_weight="1" />

</LinearLayout>


/res/layout-land/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:context=".MainActivity" >

    <fragment
        class="com.example.androidyahooweatherdom.Fragment1"
        android:id="@+id/fragment1"
        android:layout_width="0px"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    <fragment
        class="com.example.androidyahooweatherdom.Fragment2"
        android:id="@+id/fragment2"
        android:layout_width="0px"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    <fragment
        class="com.example.androidyahooweatherdom.Fragment3"
        android:id="@+id/fragment3"
        android:layout_width="0px"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    
</LinearLayout>


MainActivity
package com.example.androidyahooweatherdom;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

public class MainActivity extends FragmentActivity {

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

}


- Fragment1 call setRetainInstance(flase), so everytime orientation changed, it will re-start and reload new weather.

- Fragment3 call setRetainInstance(true), so it will not reload weather.

- Fragment2 call setRetainInstance(true), it will not reload weather too. But it have the same Action string for IntentFilter of Fragment1, so when IntentService of Fragment1 finished, BroadcastReceiver of Fragment2 will be called also.



download filesDownload the files.

If you have RuntimeException: Unable to instantiate activity ComponentInfo, please read HERE.

2 comments:

Anonymous said...

Just downloaded your source code (Thanks for your great job!), and imported to the ADT, but error message showed after running this project as follows: "AndroidRuntime(1008): java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.example.androidyahooweatherdom/com.example.androidyahooweatherdom.MainActivity}: java.lang.ClassNotFoundException: Didn't find class "com.example.androidyahooweatherdom.MainActivity" on path: /data/app/com.example.androidyahooweatherdom-1.apk"
Did you have the same problem?

Erik said...

Thanks for your advice.

Please read java.lang.RuntimeException: Unable to instantiate activity ComponentInfo.