Browsing Facebook Albums from an Android Application

What this will hopefully cover

A couple weeks ago I wrote a little Android application for this podcast I co-host. One of the features it includes is the ability to browse our Facebook albums. Sure you can do that already in the web browser unless you don't use Facebook or don't want to admit to your friends that you like retro gaming podcasts. Also, I just thought it would be cool to say I wrote something like that.

It turns out calling the Facebook API to get albums and photos is pretty easy. Wrestling it all into UI controls takes a little more effort but isn't all that rough. All together I figured it would make for an interesting tutorial since it contains buzzwords like "Facebook" and "Android".

The topics included somewhere in here are:

I'm going to skip all the basics like installing/using Eclipse and the Android SDK. There's plenty of material out there already and I'm going to assume you've made it past the "Hello World" material.

When we're all done the end product should look something like this:

Browsing Facebook albums

Browsing Facebook albums

Viewing an image from a Facebook album

Viewing an image from a Facebook album


Layouts

We're going to create two layouts - one for the main Activity and one for a dialog to display full-size images.

Let's go for the main Activity first. We need a Spinner to select albums, a TextView will display the album description and messages to the user, and finally a Gallery to show the album thumbnails.

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

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

    android:orientation="vertical"

    android:layout_width="fill_parent"

    android:layout_height="fill_parent">

  <TextView  

    android:layout_width="fill_parent" 

    android:layout_height="wrap_content" 

    android:id="@+id/TextViewSelectAlbum"

    android:layout_marginTop="4px" 

    android:text="@string/selectalbum"

  />

  <Spinner android:id="@+id/SpinnerSelectAlbum" 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:enabled="false" 

  />

  <TextView 

    android:layout_width="wrap_content" 

    android:layout_height="wrap_content" 

    android:id="@+id/TextViewAlbumDescription" 

  />

  <Gallery android:id="@+id/GallerySelectedAlbum" 

    android:layout_width="fill_parent" 

    android:layout_height="wrap_content"

  />

</LinearLayout>

Now let's create the layout for the image dialog which just contains an ImageView. The downside to this is the image will be scaled to fit the device screen. Someone smarter than me could make this pan & zoom the image instead.

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

<LinearLayout

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

  android:layout_width="wrap_content"

  android:layout_height="wrap_content"

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

  <ImageView 

    android:id="@+id/ImageViewSourceImage" 

    android:layout_width="fill_parent" 

    android:layout_height="fill_parent" 

    android:src="@drawable/downloading"

    android:scaleType="centerInside">

  </ImageView>

</LinearLayout>

The next part is kinda weird. We need a style that will be used by the Adapter for the Gallery. Rather than going with the layouts this needs to be in res/values/attrs.xml. You can probably name it something else but whatever the case it needs to be in res/values. The contents of the file should include the following:

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

<resources>

    <declare-styleable name="AlbumGallery">

        <attr name="android:galleryItemBackground" />

    </declare-styleable>

</resources>


Other setup tasks

We need to add a couple string values in res/values/strings.xml for the Facebook URLs. That magic number 226949532466 you see below should be changed to whatever page you want to browse the albums from. Oh, and of course the albums need to be public in Facebook. Because I'm lazy I'm not dealing with stuff that needs some kind of authentication, maybe I'll try that next time.

Anyway, there are two Facebook APIs we'll be using - one to get a list of albums for a page and another to get the contents of a specific album. If you're curious what the raw output from these APIs looks like then just punch them into your web browser.

<!-- CHANGE 226949532466 TO YOUR FACEBOOK PAGE ID -->

<string name="facebook_url_albums">https://graph.facebook.com/226949532466/albums</string>

<string name="facebook_url_photos">https://graph.facebook.com/[ALBUMID]/photos</string>

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" />


Data objects

As noted a second ago, we're going to be calling two different Facebook APIs. The first brings back a list of albums - we need to grab the albums IDs for the subsequent call to get the individual photos. The name and description are also handy because they're a little more meaningful to a user than the 12+ digit ID.

public class AlbumItem{

  private String id;

  private String name;

  private String description;

  public AlbumItem(String id,String name,String description){

    this.id=id;

    this.name=name;

    this.description=description;

  }

  public String getId(){

    return id;

  }

  public void setId(String id){

    this.id=id;

  }

  public String getName(){

    return name;

  }

  public void setName(String name){

    this.name=name;

  }

  public String getDescription(){

    return description;

  }

  public void setDescription(String description){

    this.description=description;

  }

  @Override

  public String toString(){

    //returning name because that is what will be displayed in the Spinner control

    return(this.name);

  }

}

The second call pulls down the list of photos in an album. There are two things we care about - the picture URL which is really the thumbnail image and the source URL which is the full-size image.

public class PhotoItem{

  private String pictureUrl;

  private String sourceUrl;

  public PhotoItem(String pictureUrl,String sourceUrl){

    this.pictureUrl=pictureUrl;

    this.sourceUrl=sourceUrl;

  }

  public String getPictureUrl(){

    return pictureUrl;

  }

  public void setPictureUrl(String pictureUrl){

    //fix for Android 2.3

    CharSequence target="\\/";

    CharSequence replace="/";

    String fixedUrl=pictureUrl.replace(target,replace);

    this.pictureUrl=fixedUrl;

  }

  public String getSourceUrl(){

    return sourceUrl;

  }

  public void setSourceUrl(String sourceUrl){

    this.sourceUrl=sourceUrl;

  }

}


Utility classes

I still think like a VB6 programmer, I probably always will to some degree. When I see a problem I think - hmmm, how can I break this up into a series of reusable modules? The next few small classes are the result of that exercise, some of which I wrote prior to this application.

We're pulling down images from the internet, sometimes that takes a while so here's a small cache to store them. This makes flipping between albums a lot faster.

public abstract class ImageCache{

  private static HashMap<String,Bitmap> hashMap;

  public static synchronized Bitmap get(String imageUrl){

    if(hashMap==null){hashMap=new HashMap<String,Bitmap>();}

    return(hashMap.get(imageUrl));

  }

  public static synchronized void put(String imageUrl,Bitmap bitmap){

    if(hashMap==null){hashMap=new HashMap<String,Bitmap>();}

    hashMap.put(imageUrl,bitmap);

  }

}

I had a different implementation for this next module but ran into some weird problems. After some searching I found a question on StackOverflow that was close enough to what I was experiencing. The suggested fix there worked here too luckily.

public abstract class HttpFetch{

  //see - http://stackoverflow.com/questions/4414839/bitmapfactory-decodestream-returns-null-without-exception

  public static InputStream fetch(String address) throws MalformedURLException,IOException{

      HttpGet httpRequest=new HttpGet(URI.create(address));

      HttpClient httpclient=new DefaultHttpClient();

      HttpResponse response=(HttpResponse)httpclient.execute(httpRequest);

      HttpEntity entity=response.getEntity();

      BufferedHttpEntity bufHttpEntity=new BufferedHttpEntity(entity);

      InputStream instream=bufHttpEntity.getContent();

      return(instream);

  }

  public static Bitmap fetchBitmap(String imageAddress) throws MalformedURLException,IOException{

    return(BitmapFactory.decodeStream(fetch(imageAddress)));

  }

}

The nice thing about going back and looking at code I wrote is identifying future improvements. I think I can scrap most of this next class in favor of the previous one. Anyway, for now here's a method to invoke a REST service over HTTP.

public abstract class RestInvoke{

  public static String invoke(String restUrl) throws Exception{

    String result=null;

    HttpClient httpClient=new DefaultHttpClient();  

    HttpGet httpGet=new HttpGet(restUrl); 

    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);

  }  

}

Next up is a class to parse the results of the Facebook API calls. They return everything in JSON and the data structures are really simple to understand. Oh, we don't actually call parseLatestStatus from this demo but it was there from the application I originally wrote.

public abstract class FacebookJSONParser{

  public static String parseLatestStatus(String json){

    String latestStatus="";

    String startTag="\"message\":\"";

    int indexOf=json.indexOf(startTag);

    if(indexOf>0){

      int start=indexOf+startTag.length();

      String endTag="\",";

      return(json.substring(start,json.indexOf(endTag,start)));

    }

    return(latestStatus);

  }

  public static ArrayList<AlbumItem> parseAlbums(String json) throws JSONException{

    ArrayList<AlbumItem> albums=new ArrayList<AlbumItem>();

    JSONObject rootObj=new JSONObject(json);

    JSONArray itemList=rootObj.getJSONArray("data");

    int albumCount=itemList.length();

    for(int albumIndex=0;albumIndex<albumCount;albumIndex++){

      JSONObject album=itemList.getJSONObject(albumIndex);

      String description="";

      try{

        description=album.getString("description");

      }catch(JSONException x){/*not implemented*/}

      albums.add(new AlbumItem(album.getString("id"),album.getString("name"),description));

    }

    return(albums);

  }

  public static ArrayList<PhotoItem> parsePhotos(String json) throws JSONException{

    ArrayList<PhotoItem> photos=new ArrayList<PhotoItem>();

    JSONObject rootObj=new JSONObject(json);

    JSONArray itemList=rootObj.getJSONArray("data");

    int photoCount=itemList.length();

    for(int photoIndex=0;photoIndex<photoCount;photoIndex++){

      JSONObject photo=itemList.getJSONObject(photoIndex);

      photos.add(new PhotoItem(photo.getString("picture"),photo.getString("source")));

    }

    return(photos);

  }

}

Finally we have the Adapter that's going to store the items in the Gallery widget.

public class AlbumImageAdapter extends BaseAdapter{

  private final static String TAG="AlbumImageAdapter";

  private ArrayList<PhotoItem> photos;

  private Context context;

  private int albumGalleryItemBackground;

  public AlbumImageAdapter(Context context,ArrayList<PhotoItem> photos){

    this.context=context;

    this.photos=photos;

    TypedArray ta=context.obtainStyledAttributes(R.styleable.AlbumGallery);

    albumGalleryItemBackground=ta.getResourceId(R.styleable.AlbumGallery_android_galleryItemBackground,0);

    ta.recycle();

  }

  @Override

  public int getCount(){

    return(this.photos.size());

  }

  @Override

  public Object getItem(int position){

    return(this.photos.get(position));

  }

  @Override

  public long getItemId(int position){

    return(position);

  }

  @Override

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

    ImageView imageView=new ImageView(context);

    imageView.setScaleType(ImageView.ScaleType.FIT_XY);

    imageView.setBackgroundResource(albumGalleryItemBackground);

    //load the image

    String pictureUrl=this.photos.get(position).getPictureUrl();

    try{

      Bitmap bitmap=ImageCache.get(pictureUrl);

      if(bitmap==null){

        bitmap=HttpFetch.fetchBitmap(pictureUrl);

        ImageCache.put(pictureUrl,bitmap);

      }

      imageView.setImageBitmap(bitmap);

    }catch(Exception x){

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

    }

    return(imageView);

  }

}


Activity

We're only going to create one Activity, aptly named FacebookAlbumDemoActivity. Here are the local variables we need to declare.

private final static String TAG="FacebookAlbumDemoActivity";

private ArrayAdapter<AlbumItem> albumArrayAdapter;

private AlbumItem selectedAlbum;

private PhotoItem selectedImage;

//references to ui controls

private Spinner spinnerSelectAlbum;

private TextView textViewAlbumDescription;

private Gallery gallerySelectedAlbum;

private ImageView imageViewSelectedImage;

In the OnCreate method let's grab references to all the UI widgets.

@Override

public void onCreate(Bundle savedInstanceState){

  super.onCreate(savedInstanceState);

  setContentView(R.layout.main);

  this.spinnerSelectAlbum=(Spinner)this.findViewById(R.id.SpinnerSelectAlbum);

  this.spinnerSelectAlbum.setOnItemSelectedListener(new AlbumSelectedListener());

  this.textViewAlbumDescription=(TextView)this.findViewById(R.id.TextViewAlbumDescription);

  this.gallerySelectedAlbum=(Gallery)this.findViewById(R.id.GallerySelectedAlbum);

  this.gallerySelectedAlbum.setOnItemClickListener(new OnPhotoClickedListener());

}

In the OnResume method (which also fires after OnCreate is complete) we'll start the UserTask to get the albums. All the UserTasks are at the end of this tutorial, after we wire up the UI we'll tackle them, be patient.

@Override

protected void onResume(){

  super.onResume();

  //check for albums

  if((this.albumArrayAdapter==null)||(this.albumArrayAdapter.getCount()<1)){

    new GetAlbumsTask().execute("");

  }

}  

Here's the listener for the Spinner used to select an album.

public class AlbumSelectedListener implements OnItemSelectedListener{

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

    selectedAlbum=(AlbumItem)spinnerSelectAlbum.getItemAtPosition(pos);

    updateAlbum();

  }

  public void onNothingSelected(AdapterView<?> parent){/* not implemented */}

}  

Here's the listener for when an image is clicked in the Gallery.

public class OnPhotoClickedListener implements OnItemClickListener{

  @Override

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

    selectedImage=(PhotoItem)gallerySelectedAlbum.getItemAtPosition(position);

    showImageDialog();

  }

}

This could probably be moved to AlbumSelectedListener instead of being a separate method because it's so small, no big deal either way.

private void updateAlbum(){

  this.gallerySelectedAlbum.setEnabled(false);

  this.textViewAlbumDescription.setText("Loading "+this.selectedAlbum.getName()+"...");

  new UpdateAlbumGalleryTask().execute("");

}

Let's finish this section with a method to show the full-size image dialog.

private void showImageDialog(){

  try{

    //create the dialog

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

    View layout=inflater.inflate(R.layout.viewimagelayout,(ViewGroup)findViewById(R.id.ViewImageDialogRoot));

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

    builder.setView(layout);

    builder.setTitle("View Image");

    builder.setPositiveButton("Close",null);

    AlertDialog imageDialog=builder.create();

    //show the dialog

    imageDialog.show();

    this.imageViewSelectedImage=(ImageView)imageDialog.findViewById(R.id.ImageViewSourceImage);

    //load the image

    new DownloadImageTask().execute("");

  }catch(Exception x){

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

  }

}


User task - get the albums

The Facebook API is very fast from my experience but network times on an actual device can be spotty. So I put all the external calls into UserTasks to grab data & images in the background. A thread could work too but that's more complicated because threads can't touch any widgets on the Activity. UserTasks, on the other hand, can modify widgets. That and I hadn't worked with UserTasks yet and wanted to try 'em out.

This first UserTask retrieves the albums.

private class GetAlbumsTask extends UserTask<String,String,String>{

  public String doInBackground(String... params){

    try{

      //invoke the API to get albums

      long startTime=System.currentTimeMillis();

      String albumsJson=RestInvoke.invoke(getResources().getString(R.string.facebook_url_albums));

      long endTime=System.currentTimeMillis();

      Log.d(TAG,"Time to download albums="+(endTime-startTime)+"ms");

      //parse the albums

      startTime=endTime;

      ArrayList<AlbumItem> albums=FacebookJSONParser.parseAlbums(albumsJson);

      endTime=System.currentTimeMillis();

      Log.d(TAG,"Time to parse albums="+(endTime-startTime)+"ms");

      //update the select album spinner

      if(albums.size()>0){

        albumArrayAdapter=new ArrayAdapter<AlbumItem>(getApplicationContext(),android.R.layout.simple_spinner_item,albums);

      }

      return("");

    }catch(Exception x){

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

      return(null);

    }

   }

   public void onPostExecute(String result){

    if((albumArrayAdapter==null)||(albumArrayAdapter.isEmpty())){

      ArrayList<String> defaultList=new ArrayList<String>();

      defaultList.add("No albums found");

      ArrayAdapter<String> defaultAdapter=new ArrayAdapter<String>(getApplicationContext(),android.R.layout.simple_spinner_item,defaultList);

      spinnerSelectAlbum.setAdapter(defaultAdapter);

      spinnerSelectAlbum.setEnabled(false);

    }else{

      spinnerSelectAlbum.setAdapter(albumArrayAdapter);

      spinnerSelectAlbum.setEnabled(true);

    }

   }

}


User task - update the gallery

The second UserTask retrieves the images from an album when one is selected.

private class UpdateAlbumGalleryTask extends UserTask<String,String,ArrayList<PhotoItem>>{

  public ArrayList<PhotoItem> doInBackground(String... urls){

    try{

      Resources r=getResources();

      //invoke the API to get posts

      long startTime=System.currentTimeMillis();

      String facebookUrlPhotos=(r.getString(R.string.facebook_url_photos)).replace("[ALBUMID]",selectedAlbum.getId());

      String photosJson=RestInvoke.invoke(facebookUrlPhotos);

      long endTime=System.currentTimeMillis();

      Log.d(TAG,"Time to download photos="+(endTime-startTime)+"ms");

      //parse out the latest status

      startTime=endTime;

      ArrayList<PhotoItem> photos=FacebookJSONParser.parsePhotos(photosJson);

      endTime=System.currentTimeMillis();

      Log.d(TAG,"Time to parse latest status="+(endTime-startTime)+"ms");

      return(photos);

    }catch(Exception x){

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

      return(null);

    }

   }

   public void onPostExecute(ArrayList<PhotoItem> photos){

     if((photos==null)||(photos.size()<1)){

       textViewAlbumDescription.setText("Unable to download "+selectedAlbum.getName());

     }else{

       textViewAlbumDescription.setText(selectedAlbum.getDescription());

       gallerySelectedAlbum.setAdapter(new AlbumImageAdapter(getApplicationContext(),photos));

     }

   }

}


User task - download full-size image

The third and final UserTask downloads an image.

private class DownloadImageTask extends UserTask<String,String,Bitmap>{

  public Bitmap doInBackground(String... params){

    try{

      Log.d(TAG,"DownloadImageTask.doInBackground");

      String imageUrl=selectedImage.getSourceUrl();

      Log.d(TAG,"DownloadImageTask.doInBackground, imageUrl="+imageUrl);

      Bitmap bitmap=ImageCache.get(imageUrl);

      if(bitmap!=null){return(bitmap);}

      long startTime=System.currentTimeMillis();

      bitmap=HttpFetch.fetchBitmap(imageUrl);

      long endTime=System.currentTimeMillis();

      Log.d(TAG,"DownloadEpisodeLogoTask.doInBackground, Time to fetch bitmap="+(endTime-startTime)+"ms");

      ImageCache.put(imageUrl,bitmap);

      return(bitmap);

    }catch(IOException iox){

      Log.e(TAG,"DownloadImageTask",iox);

      return(null);

    }

   }

   public void onPostExecute(Bitmap result){

     imageViewSelectedImage.setImageBitmap(result);

   }

}

So go put that all together and you'll have a nice little working demo of how to browse Facebook albums. If you're feeling lazy then just download the source code...


Download

Source code for this demo - update res/values/strings.xml to point to your albums or something.


Tweet