From 1998-2001 I was a software engineer for Mercator Software, working mostly with Visual Basic 6. One massive stock crash later I found myself doing a mix of Visual Basic, C#, and Java development at a Fortune 100 company. From 2003-2006 I was lead C#/ASP.NET developer for a content hub there. When the company moved that site to WebSphere Portal I went along for the ride and was a Portal lead developer/team lead from 2007-2010. In 2011 I moved into a Solution Architect role. In 2004 I completed my M.S. in Computer Science from University of Illinois: Chicago. My area of focus was artificial intelligence. Prior to that I earned a B.S. in Computer Science from Elmhurst College. In my spare time I've written a few nerdy Android applications that are freely available on the market. Hugues has posted 3 posts at DZone. View Full User Profile

Browsing Facebook Albums in Android

05.13.2011
| 9688 views |
  • submit to reddit

Let's say you want to browse some images from a Facebook album in an Android application. Overall it's easy enough to call the Facebook API to get albums and photos. Wrestling it all into UI controls takes a little more effort but isn't all that rough. Making it look like an asynchronous process to the user is where things get fun. All together it makes for an interesting tutorial, especially since it contains buzzwords like "Facebook" and "Android".

The topics included in this article are:

  • Calling the Facebook Graph API and parsing the results
  • Using the Android Spinner and Gallery widgets
  • Executing UserTasks to load external data without freezing the UI

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

 

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.

<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. In this demo the albums need to be public in Facebook.

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 internet 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){
this.pictureUrl=pictureUrl;
}
public String getSourceUrl(){
return sourceUrl;
}
public void setSourceUrl(String sourceUrl){
this.sourceUrl=sourceUrl;
}
}

Utilty Classes

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

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

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

 

The Activity

We're only going to create one Activity, aptly named FacebookAlbumDemoActivity.

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

private void updateAlbum(){
this.gallerySelectedAlbum.setEnabled(false);
this.textViewAlbumDescription.setText("Loading "+this.selectedAlbum.getName()+"...");
new UpdateAlbumGalleryTask().execute("");
}


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

The Facebook API is very fast but network times on an actual device can be spotty. So let's 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.


//UserTask to retrieve 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);
}
}
}


//UserTask to retrieve 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));
}
}
}


//UserTask to download 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);
}
}

 

View the original article and download the full source code here.

Published at DZone with permission of its author, Hugues Johnson.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags: