Integrating Android Applications with the eBay API

Stuff You Need

Alright, I'm not feeling real talkative today so let's just cut to the chase. So you want to display some eBay listings on an Android device? This article will explain how to do it or you can just scroll to the bottom and download the source code.

Here's what you need to get started:


Getting Started

Fire-up Eclipse and create a new Android project. Lets choose the lowest version so it runs on anything, if you want to target a newer API then you can guess what to do.

Create a new Android project

The first class we're going to create is something to store an auction listing. Comparable is implemented to allow for sorting by date. If you want to sort by something different then change the compareTo method.

public class Listing implements Comparable<Listing>{

  private String id;

  private String title;

  private String imageUrl;

  private String listingUrl;

  private String location;

  private String shippingCost;

  private String currentPrice;

  private String auctionSource;

  private Date startTime;

  private Date endTime;

  private boolean auction;

  private boolean buyItNow;

  [whole bunch of get/set methods clipped]

  

  @Override

  public int compareTo(Listing another){

    return(another.startTime.compareTo(this.startTime));

  }

}

The next class is to store the results of an eBay search. It contains an ArrayList of the previous class and an error to track anything that went badly.

public class SearchResult{

  public final static int RESULT_SUCCESS=0;

  public final static int RESULT_ERROR=1;

  private int resultCode;

  private Exception error;

  private ArrayList<Listing> listings;

  public SearchResult(){

    this.listings=new ArrayList<Listing>();

  }

  

  [more get/set methods clipped]

  

  public void append(SearchResult toAppend){

    this.listings.addAll(toAppend.getListings());

  }

  public void sort(){

    Collections.sort(this.listings);

  }

}

Now we'll create a resource file for all the eBay string constants. These could be regular constants but externalizing them opens the remote possibility that we could upgrade to a newer version of the service without changing code. Change the affiliate.trackingId value to the campaign ID mentioned earlier. I left mine in the source code so I can profit from lazy developers. The sandbox and production IDs I changed because there's a limit to how many web service calls you can make per month and I don't need one of the aforementioned lazy developers breaking an application I have on the Android market.

<?xml version="1.0" encoding="utf-8"?>

<resources>

    <string name="ebay_request_template">^1services/search/FindingService/v1?OPERATION-NAME=findItemsByKeywords&SERVICE-VERSION=1.0.0&SECURITY-APPNAME=^2&RESPONSE-DATA-FORMAT=JSON&REST-PAYLOAD&keywords=^3&paginationInput.entriesPerPage=10&affiliate.networkId=9&affiliate.trackingId=5336695910&affiliate.customId=ebaydemo&sortOrder=StartTimeNewest</string> 

    <string name="ebay_source_name">eBay</string> 

    <string name="ebay_appid_sandbox">REPLACE_WITH_YOUR_SANDBOX_ID</string> 

    <string name="ebay_appid_production">REPLACE_WITH_YOUR_PRODUCTION_ID</string>

    <string name="ebay_wsurl_sandbox">http://sandbox.ebay.com/</string>

    <string name="ebay_wsurl_production">http://svcs.ebay.com/</string>

    <string name="ebay_tag_findItemsByKeywordsResponse">findItemsByKeywordsResponse</string>

    <string name="ebay_tag_searchResult">searchResult</string>

    <string name="ebay_tag_item">item</string>

    <string name="ebay_tag_itemId">itemId</string>

    <string name="ebay_tag_title">title</string>

    <string name="ebay_tag_galleryURL">galleryURL</string>

    <string name="ebay_tag_viewItemURL">viewItemURL</string>

    <string name="ebay_tag_location">location</string>

    <string name="ebay_tag_sellingStatus">sellingStatus</string>

    <string name="ebay_tag_currentPrice">currentPrice</string>

    <string name="ebay_tag_value">__value__</string>

    <string name="ebay_tag_currencyId">\@currencyId</string>

    <string name="ebay_tag_shippingInfo">shippingInfo</string>

    <string name="ebay_tag_shippingServiceCost">shippingServiceCost</string>

    <string name="ebay_tag_listingInfo">listingInfo</string>

    <string name="ebay_tag_listingType">listingType</string>

    <string name="ebay_tag_buyItNowAvailable">buyItNowAvailable</string>

    <string name="ebay_tag_startTime">startTime</string>

    <string name="ebay_tag_endTime">endTime</string>

    <string name="ebay_value_auction">auction</string>

    <string name="ebay_value_storeinventory">StoreInventory</string>

    <string name="ebay_value_fixedprice">StoreInventory</string>

    <string name="ebay_value_true">true</string>

</resources>

Sooner or later we're going to need access to the internets so add the permission to the manifest file.

<uses-permission android:name="android.permission.INTERNET" />


Calling the eBay API

This demo uses the REST version of the API to return results in JSON. There's an XML version of the API too and either are supported in the Android SDK. I don't see any obvious advantage one format has over the other. I'm sure I'll get a blowhard email explaining why one is vastly superior to the other.

This class creates the request string and invokes the service. Here is where you first find out whether you correctly added the internet permission to your application and changed the application ID values to real ones in the resource file shown above.

public class EbayInvoke{

  private static String appID;

  private static String ebayURL;

  private Resources resources;

  public EbayInvoke(Context context){

    this.resources=context.getResources();

    if(Build.PRODUCT.toLowerCase().indexOf("sdk")>-1){

      /* 

      the sandbox URLs are pretty useless as they only return a success code but no results

      if you really want to use them then swap out the next two lines

      appID=this.resources.getString(R.string.ebay_appid_sandbox);

      ebayURL=this.resources.getString(R.string.ebay_wsurl_sandbox);

      */

      appID=this.resources.getString(R.string.ebay_appid_production);

      ebayURL=this.resources.getString(R.string.ebay_wsurl_production);

    }else{

      appID=this.resources.getString(R.string.ebay_appid_production);

      ebayURL=this.resources.getString(R.string.ebay_wsurl_production);

    }

  }

  public String search(String keyword) throws Exception{

    String jsonResponse=null;

    jsonResponse=invokeEbayRest(keyword);

    if((jsonResponse==null)||(jsonResponse.length()<1)){

      throw(new Exception("No result received from invokeEbayRest("+keyword+")"));

    }

    return(jsonResponse);

  }

  private String getRequestURL(String keyword){

    CharSequence requestURL=

      TextUtils.expandTemplate(this.resources.getString(R.string.ebay_request_template),ebayURL,appID,keyword);

    return(requestURL.toString());

  }

  private String invokeEbayRest(String keyword) throws Exception{

    String result=null;

    HttpClient httpClient=new DefaultHttpClient();  

    HttpGet httpGet=new HttpGet(this.getRequestURL(keyword));

    HttpResponse response=httpClient.execute(httpGet);

    HttpEntity httpEntity=response.getEntity();

    if(httpEntity!=null){

      InputStream in=httpEntity.getContent();

          BufferedReader reader=new BufferedReader(new InputStreamReader(in));

          StringBuffer temp=new StringBuffer();

          String currentLine=null;

          while((currentLine=reader.readLine())!=null){

               temp.append(currentLine);

          }

          result=temp.toString();

      in.close();

    }

    return(result);

  }

}

At this point we have a giant string containing the results. Next it's time to do something with it.


Parsing the Results

In the constructor for the parser class we're initializing the date format for later use. Since this is not a thread-safe operation it's best to set the reference once in a synchronized block. This really won't be a problem on this application since it's not creating instances of this class from multiple threads. If you port this code to a multi-user web application I promise something bad will happen if you don't do this.

public class EbayParser{

  private final static String TAG="EbayParser";

  private static SimpleDateFormat dateFormat;

  private Resources resources;

  public EbayParser(Context context){

    synchronized(this){

      if(dateFormat==null){

        dateFormat=new SimpleDateFormat("[\"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\"]");

      }

    }

    this.resources=context.getResources();

  }

This next block parses the results. The amount of object nesting almost drove me insane. Except for the new JSONObject call this actually runs pretty darn fast.

The XML version and JAXB would have been easier than all this code except for two problems - 1) JAXB is not supported on Android. 2) JAXBContext is the most poorly performing class I've encountered in my entire life. It makes that new JSONObject call look like the F-117 Nighthawk. Whoever wrote it should never write code again. Sure I've written my share of terrible code. However, if I wrote something that I thought was going to be used by millions of people I would make sure that obtaining a new instance didn't run for a minimum of 10 seconds while holding a lock on the classloader (thereby blocking all other threads from creating objects).

  public ArrayList<Listing> parseListings(String jsonResponse) throws Exception{

    ArrayList<Listing> listings=new ArrayList<Listing>();

    JSONObject rootObj=new JSONObject(jsonResponse);

    JSONArray itemList=rootObj

      .getJSONArray(this.resources.getString(R.string.ebay_tag_findItemsByKeywordsResponse))

      .getJSONObject(0)

      .getJSONArray(this.resources.getString(R.string.ebay_tag_searchResult))

      .getJSONObject(0)

      .getJSONArray(this.resources.getString(R.string.ebay_tag_item));

    int itemCount=itemList.length();

    for(int itemIndex=0;itemIndex<itemCount;itemIndex++){

      try{

        Listing listing=this.parseListing(itemList.getJSONObject(itemIndex));

        listing.setAuctionSource(this.resources.getString(R.string.ebay_source_name));

        listings.add(listing);

      }catch(JSONException jx){

        /* if something goes wrong log &&nbsp;move to the next item */

        Log.e(TAG,"parseListings: jsonResponse="+jsonResponse,jx);

      }

    }

    return(listings);

  }

  private Listing parseListing(JSONObject jsonObj) throws JSONException{

    /*

     * Things outside of a try/catch block are fields that are required and should throw an exception if not found

     * Things inside of a try/catch block are fields we can live without

     */

    Listing listing=new Listing();

    /* get items at the root of the object

     * id, title, and URL are required

     * image and location are optional */

    listing.setId(jsonObj.getString(this.resources.getString(R.string.ebay_tag_itemId)));

    listing.setTitle(this.stripWrapper(jsonObj.getString(this.resources.getString(R.string.ebay_tag_title))));

    listing.setListingUrl(this.stripWrapper(jsonObj.getString(this.resources.getString(R.string.ebay_tag_viewItemURL))));

    try{

      listing.setImageUrl(this.stripWrapper(jsonObj.getString(this.resources.getString(R.string.ebay_tag_galleryURL))));

    }catch(JSONException jx){

      Log.e(TAG,"parseListing: parsing image URL",jx);

      listing.setImageUrl(null);

    }

    try{

      listing.setLocation(this.stripWrapper(jsonObj.getString(this.resources.getString(R.string.ebay_tag_location))));

    }catch(JSONException jx){

      Log.e(TAG,"parseListing: parsing location",jx);

      listing.setLocation(null);

    }

    //get stuff under sellingStatus - required

    JSONObject sellingStatusObj=

      jsonObj.getJSONArray(this.resources.getString(R.string.ebay_tag_sellingStatus)).getJSONObject(0);

    JSONObject currentPriceObj=

      sellingStatusObj.getJSONArray(this.resources.getString(R.string.ebay_tag_currentPrice)).getJSONObject(0);

    listing.setCurrentPrice(

      this.formatCurrency(currentPriceObj.getString(

      this.resources.getString(R.string.ebay_tag_value)),

      currentPriceObj.getString(this.resources.getString(R.string.ebay_tag_currencyId))));

    //get stuff under shippingInfo - optional

    try{

      JSONObject shippingInfoObj=

      jsonObj.getJSONArray(this.resources.getString(R.string.ebay_tag_shippingInfo)).getJSONObject(0);

      JSONObject shippingServiceCostObj=

        shippingInfoObj.getJSONArray(this.resources.getString(R.string.ebay_tag_shippingServiceCost)).getJSONObject(0);

      listing.setShippingCost(

        this.formatCurrency(shippingServiceCostObj.getString(

        this.resources.getString(R.string.ebay_tag_value)),currentPriceObj.getString(

        this.resources.getString(R.string.ebay_tag_currencyId))));

    }catch(JSONException jx){

      Log.e(TAG,"parseListing: parsing shipping cost",jx);

      listing.setShippingCost("Not listed");

    }

    //get stuff under listingInfo

    try{

      JSONObject listingInfoObj=jsonObj.getJSONArray(

        this.resources.getString(R.string.ebay_tag_listingInfo)).getJSONObject(0);

      try{

        String listingType=this.stripWrapper(

          listingInfoObj.getString(this.resources.getString(R.string.ebay_tag_listingType)));

        if(listingType.toLowerCase().indexOf(this.resources.getString(R.string.ebay_value_auction))>-1){

          listing.setAuction(true);

          try{

            String buyItNowAvailable=

              this.stripWrapper(listingInfoObj.getString(this.resources.getString(R.string.ebay_tag_buyItNowAvailable)));

            if(buyItNowAvailable.equalsIgnoreCase(this.resources.getString(R.string.ebay_value_true))){

              listing.setBuyItNow(true);

            }else{

              listing.setBuyItNow(false);

            }

          }catch(JSONException jx){

            Log.e(TAG,"parseListing: parsing but it now",jx);

          }

        }else{

          listing.setAuction(false);

          listing.setBuyItNow(true);

        }

      }catch(JSONException jx){

        Log.e(TAG,"parseListing: parsing listing type",jx);

      }

      //get start and end dates - optional

      try{

        Date startTime=dateFormat.parse(listingInfoObj.getString(this.resources.getString(R.string.ebay_tag_startTime)));

        listing.setStartTime(startTime);

        Date endTime=dateFormat.parse(listingInfoObj.getString(this.resources.getString(R.string.ebay_tag_endTime)));

        listing.setEndTime(endTime);

      }catch(Exception x){ //generic - both ParseException and JSONException can be thrown, same result either way

        Log.e(TAG,"parseListing: parsing start and end dates",x);

        listing.setStartTime(null);

        listing.setEndTime(null);

      }

     }catch(JSONException jx){

      Log.e(TAG,"parseListing: parsing listing info",jx);

      listing.setStartTime(null);

      listing.setEndTime(null);

     }

    //alright, all done

    return(listing);

  }

The next two methods deal with formatting data. The first formats currency to address eBay's propensity for chopping off trailing zeros. The second takes the wrapper brackets off strings.

  private String formatCurrency(String amount,String currencyCode){

    StringBuffer formattedText=new StringBuffer(amount);

    try{

      //add trailing zeros

      int indexOf=formattedText.indexOf(".");

      if(indexOf>=0){

        if(formattedText.length()-indexOf==2){

          formattedText.append("0");

        }

      }

      //add dollar sign

      if(currencyCode.equalsIgnoreCase("USD")){

        formattedText.insert(0,"$");

      }else{

        formattedText.append(" ");

        formattedText.append(currencyCode);

      }

    }catch(Exception x){

      Log.e(TAG,"formatCurrency",x);

    }

    return(formattedText.toString());

  }

  private String stripWrapper(String s){

    try{

      int end=s.length()-2;

      return(s.substring(2,end));

    }catch(Exception x){

      Log.e(TAG,"stripWrapper",x);

      return(s);

    }

  }


Create Layouts

So far everything we've seen can be used on any Java application. Let's get to the Android part already by creating some layouts. The first layout is used for items to display in a ListView.

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

  xmlns:android="http://schemas.android.com/apk/res/android"

  android:id="@+id/listviewitem_root" 

  android:layout_width="wrap_content"

  android:layout_height="wrap_content" 

  android:orientation="horizontal">

  <ImageView 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:id="@+id/listviewitem_image" 

    android:paddingLeft="4px" 

    android:paddingRight="4px">

  </ImageView>

  <LinearLayout 

    android:id="@+id/listviewitem_linearlayout" 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:orientation="vertical">

    <TextView 

      android:id="@+id/listviewitem_title" 

      android:layout_width="wrap_content" 

      android:layout_height="wrap_content" 

      android:textStyle="bold">

    </TextView>

    <TextView 

      android:id="@+id/listviewitem_price" 

      android:layout_width="wrap_content" 

      android:layout_height="wrap_content">

    </TextView>

    <TextView 

      android:id="@+id/listviewitem_shipping" 

      android:layout_width="wrap_content" 

      android:layout_height="wrap_content">

    </TextView>

  </LinearLayout>

</LinearLayout>

The next layout is used for a dialog to display the details of an auction.

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

  xmlns:android="http://schemas.android.com/apk/res/android"

  android:orientation="vertical"

  android:id="@+id/listingdetaildialog_root" 

  android:layout_height="fill_parent" 

  android:layout_width="fill_parent">

  <ImageView 

    android:id="@+id/listingdetail_image" 

    android:textSize="12sp" 

    android:layout_height="fill_parent" 

    android:layout_width="fill_parent" 

    android:clickable="true">

  </ImageView>

  <TextView 

    android:id="@+id/listingdetail_auctionsource" 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:textSize="12sp">

  </TextView>

  <TextView 

    android:id="@+id/listingdetail_starttime" 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:textSize="12sp">

  </TextView>

  <TextView 

    android:id="@+id/listingdetail_endtime" 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:textSize="12sp">

  </TextView>

  <TextView 

    android:id="@+id/listingdetail_price" 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:textSize="12sp">

  </TextView>

  <TextView 

    android:id="@+id/listingdetail_listingtype" 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:textSize="12sp">

  </TextView>

  <TextView 

    android:id="@+id/listingdetail_shipping" 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:textSize="12sp">

  </TextView>

  <TextView 

    android:id="@+id/listingdetail_location" 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:textSize="12sp">

  </TextView>

  <TextView 

    android:id="@+id/listingdetail_link" 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:textSize="12sp" 

    android:linksClickable="true" 

    android:autoLink="web">

  </TextView>

</LinearLayout>

The final layout is used for a search dialog.

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

  xmlns:android="http://schemas.android.com/apk/res/android"

  android:layout_width="fill_parent"

  android:layout_height="wrap_content"

  android:id="@+id/searchdialog_root">

  <TextView 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:id="@+id/searchdialog_captiontextview" 

    android:text="@string/searchdialog_caption">

  </TextView>

  <EditText 

    android:layout_width="fill_parent" 

    android:layout_height="wrap_content" 

    android:id="@+id/searchdialog_keyword">

  </EditText>

</LinearLayout>


Create the ArrayAdapter

In this demo we're displaying eBay listings in a ListView. To do that we need an ArrayAdapter that inflates the first layout we created and sets the field values. Along the way we need to load images from URLs.

The getImage method is used later on to display the image in the listing detail dialog. It saves an ounce of time to reuse what we already downloaded. Just like how you should save the results of any calls to JAXBContext.newInstance in a static object since the performance of that call is so horrifically bad and can potentially block all other threads in the system from executing.

public class ListingArrayAdapter extends ArrayAdapter<Listing>{

  private final static String TAG="ListingArrayAdapter";

  private Context context;

  private SearchResult listings;

  private int resourceId;

  private Drawable images[];

  public ListingArrayAdapter(Context context,int resourceId,SearchResult listings){

    super(context,resourceId,listings.getListings());

    this.images=new Drawable[listings.getListings().size()];

    this.context=context;

    this.listings=listings;

    this.resourceId=resourceId;

  }

  @Override

  public View getView(int position,View convertView,ViewGroup parent){

    LayoutInflater inflater=(LayoutInflater)this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    View layout=inflater.inflate(resourceId,null);

    Listing listing=this.listings.getListings().get(position);

    TextView titleField=(TextView)layout.findViewById(R.id.listviewitem_title);

    titleField.setText(listing.getTitle());

    TextView priceField=(TextView)layout.findViewById(R.id.listviewitem_price);

    priceField.setText("Current price: "+listing.getCurrentPrice());

    TextView shippingField=(TextView)layout.findViewById(R.id.listviewitem_shipping);

    shippingField.setText("Shipping: "+listing.getShippingCost());

    if(listing.getListingUrl()!=null){

      try{

        URL url=new URL(listing.getImageUrl());

        InputStream is=(InputStream)url.getContent();

        Drawable image=Drawable.createFromStream(is,"src");

        ImageView imageView=new ImageView(context);

        imageView=(ImageView)layout.findViewById(R.id.listviewitem_image);

        imageView.setImageDrawable(image);

        this.images[position]=image;

      }catch(Exception x){

        Log.e(TAG,"getView - in if(listing.getListingUrl()!=null)",x);

      }

    }

    return(layout);    

  }

  public Drawable getImage(int position){

    return(this.images[position]);

  }

}


Duct Tape it all Together

All the UI code and controllers are crammed into the ebayDemoActivity class which is a lot like sticking everything in C# code-behind for you .NET developers or in your form for VB developers or in your JSF backing bean for... oh you get the point already. This demo isn't meant to be a lesson on MVC patterns so feel free to refactor this anyway you prefer.

This first part is what I'll classify as "basic activity stuff". It has all the private fields used throughout the class, the onCreate & onResume methods, and the menu logic. Let's talk about what each of these are doing:

onCreate: This is called when the activity is first created. Everything in here is basic setup stuff.

onResume: This is called when the activity gains or regains focus. Here's where we make the call to load the list. The if block is to prevent weirdness if a user clicks the back button after opening an auction link.

onCreateOptionsMenu: This is called when the user clicks the menu button on their device. For this demo we're only including two menu options.

onOptionsItemSelected: This is what happens when someone clicks one of the two options created above.

public class ebayDemoActivity extends ListActivity{

  private final static String TAG="ebayDemoActivity";

  private static EbayInvoke ebayInvoke;

  private static EbayParser ebayParser;

  private static ProgressDialog progressDialog;

  private String searchTerm="phantasy+star+3"; //intial value for demo

  private SearchResult listings;

  private ListingArrayAdapter adapter;

  private Listing selectedListing;

  private int selectedPosition;

  //listing detail dialog

  private AlertDialog listingDetailDialog;

  private ImageView imageViewImage;

  private TextView textViewStartTime;

  private TextView textViewEndTime;

  private TextView textViewListingType;

  private TextView textViewPrice;

  private TextView textViewShipping;

  private TextView textViewLocation;

  private TextView textViewLink;

  //filter dialog

  private AlertDialog keywordDialog;

  private EditText keywordTextbox;

  //menu constants

  private final static int MENU_KEYWORD=0;

  private final static int MENU_QUIT=1;

  public void onCreate(Bundle savedInstanceState){

    super.onCreate(savedInstanceState);

    try{

      ListView listView=this.getListView();

      listView.setTextFilterEnabled(true);

      listView.setOnItemClickListener(selectItemListener);

    }catch(Exception x){

      Log.e(TAG,"onCreate",x);

      this.showErrorDialog(x);

    }

  }

  @Override

  protected void onResume(){

    super.onResume();

    if((this.listingDetailDialog!=null)&&(this.listingDetailDialog.isShowing())){

      return;

    }

    this.refreshListings();

  

  @Override

  public boolean onCreateOptionsMenu(Menu menu){

    try{

      menu.add(0,MENU_KEYWORD,0,"Search Term");

      menu.add(0,MENU_QUIT,1,"Quit");

      return(true);

    }catch(Exception x){

      Log.e(TAG,"onCreateOptionsMenu",x);

      return(false);

    }

  }

  @Override

  public boolean onOptionsItemSelected(MenuItem item){

    try{

      switch(item.getItemId()){

      case MENU_KEYWORD:{this.showKeywordDialog();return(true);}

      case MENU_QUIT:{this.finish();return(true);}

      default:{return(false);}

      }

    }catch(Exception x){

      Log.e(TAG,"onOptionsItemSelected",x);

      return(false);

    }

  }

}

The next block contains all the stuff for handling dialogs in the application, they are:

showKeywordDialog: This displays the dialog to change the search term. This is another example of how to use a LayoutInflater if you needed it.

onKeywordDialogCancelListener: This fires when the cancel button is pressed on the keyword dialog, it is intentionally empty but if you wanted something to happen here's where it would go.

onKeywordDialogPositiveListener: This fires when the OK button is pressed on keyword dialog. Let's use it to check if the keyword has actually changed and if so reload the listings.

showListingDetailDialog: This displays the dialog to view auction details.

onListingDetailDialogCloseListener: This fires when then the auction detail dialog is closed, also intentionally blank.

showErrorDialog: This is a quick all-purpose error handler. It displays a message that is probably not very user-friendly and logs it.

  private void showKeywordDialog(){

    try{

      //create the dialog

      if(this.keywordDialog==null){

        LayoutInflater inflater=(LayoutInflater)this.getSystemService(LAYOUT_INFLATER_SERVICE);

        View layout=inflater.inflate(R.layout.searchdialog,(ViewGroup)findViewById(R.id.searchdialog_root));

        AlertDialog.Builder builder=new AlertDialog.Builder(this);

        builder.setView(layout);

        builder.setTitle("Search Keyword");

        builder.setPositiveButton("OK",onKeywordDialogPositiveListener);

        builder.setNegativeButton("Cancel",onKeywordDialogCancelListener);

        this.keywordTextbox=(EditText)layout.findViewById(R.id.searchdialog_keyword);

        this.keywordDialog=builder.create();

      }

      //show the dialog

      this.keywordDialog.show();

    }catch(Exception x){

      Log.e(TAG,"showFilterDialog",x);

    }

  }

  OnClickListener onKeywordDialogCancelListener=new OnClickListener(){

    @Override

    public void onClick(DialogInterface dialog,int which){/*not implemented*/}

  };

  OnClickListener onKeywordDialogPositiveListener=new OnClickListener(){

    @Override

    public void onClick(DialogInterface dialog,int which){

      String newSearchTerm=keywordTextbox.getText().toString().replace(" ","+");

      if(!newSearchTerm.equals(searchTerm)){

        searchTerm=newSearchTerm;

        refreshListings();

      }

    }

  };

  private void showListingDetailDialog(){

    try{

      //I don't think this can actually happen so this is just a sanity check

      if(this.selectedListing==null){return;}

      //create the listing detail dialog

      if(this.listingDetailDialog==null){

        LayoutInflater inflater=(LayoutInflater)this.getSystemService(LAYOUT_INFLATER_SERVICE);

        View layout=inflater.inflate(R.layout.listingdetail,(ViewGroup)findViewById(R.id.listingdetaildialog_root));

        AlertDialog.Builder builder=new AlertDialog.Builder(this);

        builder.setView(layout);

        builder.setTitle(this.selectedListing.getTitle());

        builder.setPositiveButton("Close",onListingDetailDialogCloseListener);

        this.imageViewImage=(ImageView)layout.findViewById(R.id.listingdetail_image);

        this.textViewStartTime=(TextView)layout.findViewById(R.id.listingdetail_starttime);

        this.textViewEndTime=(TextView)layout.findViewById(R.id.listingdetail_endtime);

        this.textViewListingType=(TextView)layout.findViewById(R.id.listingdetail_listingtype);

        this.textViewPrice=(TextView)layout.findViewById(R.id.listingdetail_price);

        this.textViewShipping=(TextView)layout.findViewById(R.id.listingdetail_shipping);

        this.textViewLocation=(TextView)layout.findViewById(R.id.listingdetail_location);

        this.textViewLink=(TextView)layout.findViewById(R.id.listingdetail_link);

        this.listingDetailDialog=builder.create();

      }

      //set the values

      this.textViewStartTime.setText(

        Html.fromHtml("<b>Start Time:</b>  "+this.selectedListing.getStartTime().toLocaleString()));

      this.textViewEndTime.setText(

        Html.fromHtml("<b>End Time:</b>  "+this.selectedListing.getEndTime().toLocaleString()));

      this.textViewPrice.setText(

        Html.fromHtml("<b>Price:</b>  "+this.selectedListing.getCurrentPrice()));

      this.textViewShipping.setText(

        Html.fromHtml("<b>Shipping Cost:</b>  "+this.selectedListing.getShippingCost()));

      this.textViewLocation.setText(

        Html.fromHtml("<b>Location</b>  "+this.selectedListing.getLocation()));

      String listingType=new String("<b>Listing Type:</b>  ");

      if(this.selectedListing.isAuction()){

        listingType=listingType+"Auction";

        if(this.selectedListing.isBuyItNow()){

          listingType=listingType+", "+"Buy it now";

        }

      }else if(this.selectedListing.isBuyItNow()){

        listingType=listingType+"Buy it now";

      }else{

        listingType=listingType+"Not specified";

      }

      //url field

      this.textViewListingType.setText(Html.fromHtml(listingType));

      StringBuffer html=new StringBuffer("<a href='");

      html.append(this.selectedListing.getListingUrl());

      html.append("'>");

      html.append("View original listing on ");

      html.append(this.selectedListing.getAuctionSource());

      html.append("</a>");

      this.textViewLink.setText(Html.fromHtml(html.toString()));

      this.textViewLink.setOnClickListener(urlClickedListener);

      //set the image

      this.imageViewImage.setImageDrawable(this.adapter.getImage(this.selectedPosition));

      //show the dialog

      this.listingDetailDialog.setTitle(this.selectedListing.getTitle());

      this.listingDetailDialog.show();

    }catch(Exception x){

      if((this.listingDetailDialog!=null)&&(this.listingDetailDialog.isShowing())){

        this.listingDetailDialog.dismiss();

      }

      Log.e(TAG,"showListingDetailDialog",x);

    }

  }

  OnClickListener onListingDetailDialogCloseListener=new OnClickListener(){

    @Override

    public void onClick(DialogInterface dialog,int which){/*not implemented*/}

  };

  private void showErrorDialog(Exception x){

    try{

      new AlertDialog.Builder(this)

         .setTitle(R.string.app_name)

         .setMessage(x.getMessage())

         .setPositiveButton("Close", null)

         .show();  

    }catch(Exception reallyBadTimes){

      Log.e(TAG,"showErrorDialog",reallyBadTimes);

    }

  }

There are other user generated events we should probably bother ourselves to handle:

selectItemListener: This figures out what to do when a user clicks on a list item. This demo doesn't do anything with long clicks but it's the same pattern to implement.

urlClickedListener: This is what fires when a user clicks on a URL in the listing detail dialog.

launchBrowser: Don't ask me how this works, I got it from Google. Actually, this reminds me a lot of how to invoke external applications from VB6. You'd make a shell call and hope for the best. So think of this as doing shell("iexplore.exe someurl").

  OnItemClickListener selectItemListener=new OnItemClickListener(){

    @Override

    public void onItemClick(AdapterView<?> parent,View view,int position,long id){

      try{

        selectedPosition=position;

        selectedListing=(Listing)adapter.getItem(position);

        showListingDetailDialog();

      }catch(Exception x){

        Log.e(TAG,"selectItemListener.onItemClick",x);

      }

    }

  };

  View.OnClickListener urlClickedListener=new View.OnClickListener(){

    @Override

    public void onClick(View v){

      launchBrowser();

    }

  };

  private void launchBrowser(){

    try{

      Intent browserIntent=new Intent(Intent.ACTION_VIEW,Uri.parse(this.selectedListing.getListingUrl()));

      this.startActivity(browserIntent);

    }catch(Exception x){

      Log.e(TAG,"launchBrowser",x);

      this.showErrorDialog(x);

    }

  }

OK, now for the really important part - kicking off the search and updating the UI when it's done. This is done by displaying a progress dialog and starting a separate thread to execute the search. When that thread completes it sends a notification which triggers the UI updates. Let's break it all down:

loadListHandler: This receives the notification from LoadListThread when it's complete and calls loadListAdapter.

loadListAdapter: This rebuilds the ArrayAdapter (ListingArrayAdapter seen previously) behind the ListView.

LoadListThread: This is the thread that executes the search and parsing operations. When it's complete it notifies its handler (loadListHandler).

refreshListings: This is where the progress dialog is created and LoadListThread is launched.

  private final Handler loadListHandler=new Handler(){

    public void handleMessage(Message message){

      loadListAdapter();

    }

  };

  private void loadListAdapter(){

    this.adapter=new ListingArrayAdapter(this,R.layout.listviewitem,listings);

    this.setListAdapter(this.adapter);

    if(progressDialog!=null){

      progressDialog.cancel();

    }

  }

  private class LoadListThread extends Thread{

    private Handler handler;

    private Context context;

    

    public LoadListThread(Handler handler,Context context){

      this.handler=handler;

      this.context=context;

    }

    public void run(){

      String searchResponse="";

      try{

        if(ebayInvoke==null){

          ebayInvoke=new EbayInvoke(this.context);

        }

        if(ebayParser==null){

          ebayParser=new EbayParser(this.context);

        }

           searchResponse=ebayInvoke.search(searchTerm);

           if(listings==null){

             listings=new SearchResult();

           }

        listings.setListings(ebayParser.parseListings(searchResponse));

        this.handler.sendEmptyMessage(RESULT_OK);

      }catch(Exception x){

        Log.e(TAG,"LoadListThread.run(): searchResponse="+searchResponse,x);

        listings.setError(x);

        if((progressDialog!=null)&&(progressDialog.isShowing())){

          progressDialog.dismiss();

        }

        showErrorDialog(x);

      }

    }

  }

  private void refreshListings(){

    try{

      if(progressDialog==null){

        progressDialog=new ProgressDialog(this);

      }

      progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);

      progressDialog.setMessage("Searching for auctions...");

      progressDialog.setCancelable(false);

      progressDialog.show();

      LoadListThread loadListThread=new LoadListThread(this.loadListHandler,this.getApplicationContext());

      loadListThread.start();

    }catch(Exception x){

      Log.e(TAG,"onResume",x);

      if((progressDialog!=null)&&(progressDialog.isShowing())){

        progressDialog.dismiss();

      }

      this.showErrorDialog(x);

    }

  }

}


Screenshots

Listings
Listing detail
Search dialog

Download

Source code for this demo - you must update res/values/ebaydemo.xml with your eBay API keys for this to work.