Archive‎ > ‎Fall 2009‎ > ‎Course Project‎ > ‎Buddy Suite‎ > ‎Tutorial #3‎ > ‎

Android UI

For an introduction to Android UI, visit this page.

For tutorials covering the mostly used UI components, visit this page.

MyFacebookApp

In this tutorial we will develop an application that retrieves a user's friends from Facebook and displays their picture, status, profile URL and website address.

This tutorial has three parts:

  • Designing the GUI for the MyFacebookApp Android application.
  • Using FBRocket to get Facebook friends' details
  • Downloading friends' pictures in the background and displaying them

Designing the GUI for the MyFacebookApp Android application

First, we have to create a new Android project. For instructions, consult the first tutorial, or visit this page. Name your project MyFacebookApp. The application should have the same name. Name the main Activity FacebookActivity.

To edit the GUI of the application open the main.xml document. It can be found in the MyFacebookApp/res/layout folder. Our application will have:

  • a ListView that displays details about the user's Facebook friends, and
  • a Gallery that only shows the user's friends' pictures

Thus, the main.xml file should look like the following:

main.xml

<?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">
    <ListView android:divider="#121212" android:dividerHeight="2px"
        android:id="@+id/FullDetailsGallery"
        android:gravity="fill" android:spacing="10px" android:background="#ffffff"
        android:layout_width="fill_parent" android:layout_height="fill_parent"
        android:layout_weight="8">
    </ListView>
    <Gallery android:id="@+id/SmallFriendGallery"
        android:layout_height="50px" android:background="#ffffffff"
        android:layout_alignParentBottom="true" android:layout_centerInParent="true"
        android:gravity="center" android:spacing="2px" android:layout_width="fill_parent"
        android:layout_weight="2"></Gallery>
</LinearLayout>


The ListView is called FullDetailsGallery. This is the UI element that will display details about the user's Facebook friends (picture, status, profile URL and website address). The android:layout_width="fill_parent" and android:layout_height="fill_parent" set the width and the height, respectively, of this UI element. The "fill_parent" values dictates that this UI element is to take up all the available drawing space. The android:layout_weight="8" instructs Android how to distribute the available vertical space after all the UI elements have been drawn. You can find a tutorial about ListView here.
The Gallery is called SmallFriendGallery. You can find a tutorial about Gallery here.

Displaying Friends

We display friends in the FullDetailsGallery. This is a ListView and requires a ListAdapter to back-up its data. We extend the class BaseAdapter that implements the ListAdapter interface. The SmallFriendGallery only displays friends pictures. To do this, it requires a SpinnerAdapter to back-up its data. The BaseAdapter also implements the required interface. Both of these interfaces use the public abstract View getView (int position, View convertView, ViewGroup parent) method to retrieve the View to be displayed.

The new class is called MyFriendsAdapter. It also implements another interface, ImageViewResolver which will be described later.

The code for MyFriendsAdapter is the following:

MyFriendsAdapter

public class MyFriendsAdapter extends BaseAdapter implements ImageViewResolver {

    private static class Pair {
        private final View smallView;
        private final View bigView;

        public View getSmallView() {
            return smallView;
        }

        public View getBigView() {
            return bigView;
        }

        public Pair(View smallView, View bigView) {
            super();
            this.smallView = smallView;
            this.bigView = bigView;
        }

    }

    private final Object smallOne;
    private final Object bigOne;
    private final List<Pair> myFriends;
    private final Map<String, Pair> mapFromFriendNameToTriple;
    private final Context context;
    private Map<String, AsyncTask<Friend, Integer, Drawable>> startedTasks;

    public MyFriendsAdapter(Context context, Object s, Object b) {
        this.context = context;
        smallOne = s;
        bigOne = b;
        mapFromFriendNameToTriple = new HashMap<String, Pair>();
        myFriends = new Vector<Pair>();
        startedTasks = new HashMap<String, AsyncTask<Friend, Integer, Drawable>>();
    }

    @Override
    public int getCount() {
        Log.e("SIZE", "The size: " + myFriends.size());
        return myFriends.size();
    }

    @Override
    public Object getItem(int position) {
        return myFriends.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Log.e("VIEW", "" + position);
        Pair t = myFriends.get(position);
        if (parent.equals(bigOne)) {
            return t.getBigView();
        }
        if (parent.equals(smallOne)) {
            return t.smallView;
        }
        Toast.makeText(context, "Could not find the friend", Toast.LENGTH_LONG);
        return null;
    }

    public void addFriend(final Friend friend) {
        ImageView small = new ImageView(context);
        small.setLayoutParams(new Gallery.LayoutParams(50, 50));
        small.setScaleType(ScaleType.CENTER_INSIDE);

        LayoutInflater inflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.friend,
                null, false);
        ((TextView) layout.findViewById(R.id.Name)).setText(friend.name);
        ((TextView) layout.findViewById(R.id.ProfileURL))
                .setText(friend.profile_url);
        ((TextView) layout.findViewById(R.id.Status))
                .setText(friend.status.message + " @ " + friend.status.time);
        ((TextView) layout.findViewById(R.id.Website)).setText(friend.website);
        Pair triple = new Pair(small, layout);
        mapFromFriendNameToTriple.put(friend.name, triple);
        myFriends.add(triple);
        if (friend.pic == null || friend.pic.trim().equals("")) {
            imageForFriend(friend.name, context.getResources().getDrawable(
                    R.drawable.none));

        } else {

            AsyncTask<Friend, Integer, Drawable> task = new FriendImageDownloader(
                    this);
            startedTasks.put(friend.name, task);
            task.execute(friend);
        }

    }

    public void addFriends(List<Friend> friends) {
        for (Friend friend : friends) {
            addFriend(friend);
        }
    }

    public boolean areAllItemsEnabled() {
        return true;
    }

    @Override
    public synchronized void imageForFriend(String friend, Drawable image) {
        startedTasks.remove(friend);
        Pair t = mapFromFriendNameToTriple.get(friend);
        ViewGroup layout = (ViewGroup) t.getBigView();
        ((ImageView) layout.findViewById(R.id.MyFriend))
                .setImageDrawable(image);
        ((ImageView) t.getSmallView()).setImageDrawable(image);
        Log.e("IMAGE", "An image for " + friend + " has been downloaded");
        notifyDataSetChanged();
    }
}

This class provides Views to both the ListView and the Gallery, depending on who is asking for them.

Interesting is the use of the LayoutInflater class. This class allows us to turn an xml specification of a GUI into a View which we can then display.

As the code in function addFriend shows, the layout for a Friend is defined by the R.layout.friend resource. This resource corresponds to a file named "friend.xml" that must be in the MyFacebookApp/res/layout/ folder. The contents of this file are:

friend.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:padding="10px" android:orientation="vertical"
    android:layout_width="fill_parent" android:layout_height="fill_parent">
    <LinearLayout android:id="@+id/Details"
        android:orientation="horizontal" android:layout_width="fill_parent"
        android:layout_weight="3" android:layout_height="fill_parent">
        <ImageView android:id="@+id/MyFriend" android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:src="@drawable/none"></ImageView>
        <TextView android:id="@+id/Name" android:layout_width="fill_parent"
            android:textColor="#000000" android:layout_height="wrap_content"
            android:layout_weight="1"></TextView>
    </LinearLayout>
    <TextView android:id="@+id/Status" android:layout_width="fill_parent"
        android:textColor="#000000" android:layout_height="wrap_content"
        android:layout_weight="1"></TextView>
    <TextView android:id="@+id/ProfileURL" android:layout_width="fill_parent"
        android:textColor="#000000" android:layout_height="wrap_content"
        android:layout_weight="1"></TextView>
    <TextView android:id="@+id/Website" android:layout_width="fill_parent"
        android:textColor="#000000" android:layout_height="wrap_content"
        android:layout_weight="1"></TextView>
</LinearLayout>

Responding to user input

We will use the Gallery type of View to interact with our application. When the user clicks on the picture of a Friend, we will change the selection of the ListView to focus on the person just clicked. To do this, we need to call the setOnItemSelectedListener method on the Gallery object and provide an OnItemSelectedListener object. This object's onItemSelected method is called when the user clicks on a picture.

Next, we need to change the selection of the ListView, by calling the setSelection method on the ListView specifying the order of the item to be selected.

The code that does this is the following:

Responding to user clicks

smallGallery.setOnItemSelectedListener(new OnItemSelectedListener() {

            @Override
            public void onItemSelected(AdapterView<?> arg0, View arg1,
                    int arg2, long arg3) {
                fullGallery.setSelection(arg2);
               
            }

            @Override
            public void onNothingSelected(AdapterView<?> arg0) {
                // TODO Auto-generated method stub

            }
        });

In this code smallGallery is a Gallery type of object.

Using FBRocket to get Facebook friends' details

To access the user's Facebook friends we will use the FBRocket Android library. The library contains a small number of classes, whose documentation you can access here.

Installing the FBRocket library

Follow these steps to use FBRocket in your project:

  • Right-click your project and go to Properties
  • Open the Java Build Path panel from the left-hand side
  • Click on Add External JARs... and browse to the location of the FBRocket JAR
  • You're done! Now you need to write a class implementing LoginListener and create a new FBRocket object!

Querying for Friends

We will develop a new Activity class whose purpose is to retrieve a user's Friends.
Create a new Activity called MyFacebookApp The code should look like the following:

MyFacebookApp

public class MyFacebookApp extends Activity{
    /** Called when the activity is first created. */
    private FBRocket fbRocket;
    private ProgressDialog progressDialog;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        progressDialog = new ProgressDialog(this);
        progressDialog
                .setMessage("Retrieving friends from Facebook. Please wait");
        progressDialog.show();
        runOnUiThread(new Runnable() {

            @Override
            public void run() {
                fbRocket = new FBRocket(MyFacebookApp.this,
                        "AndroidUITutorial", "36086283d40a543c3465fae00e7ae85a");

                fbRocket.login(new LoginListener() {
                   
                    @Override
                    public void onLoginSuccess(Facebook facebook) {
                        Vector<Friend> result = new Vector<Friend>();
                        try {
                            Log.e("SUCCESS", "Logged in");
                            List<String> uids = facebook.getFriendUIDs();
                           
                            for (String uid : uids) {
                                Friend friend = facebook.getFriend(uid);
                                result.add(friend);
                            }
                           

                        } catch (ServerErrorException e) { // TODO Auto-generated catch block
                            Toast.makeText(MyFacebookApp.this, "Failed to retrieve all friends", Toast.LENGTH_LONG);
                            e.printStackTrace();
                        }
                        ((MyApplication) getApplication()).putFriends(result);
                       
                        progressDialog.dismiss();
                        setResult(0);
                        finish();
                       
                    }
                   
                    @Override
                    public void onLoginFail() {
                        progressDialog.dismiss();
                        Toast.makeText(MyFacebookApp.this, "Facebook login failed", Toast.LENGTH_LONG);
                        setResult(0, null);
                        progressDialog.dismiss();
                        finish();
                       
                    }
                }, R.layout.main);
            }
        });

    }
}

Before we can access a user's friends' list, we need to create an object of type FBRocket. The constructor accepts as parameters an Activity, the Facebook application's name, and the secret API key.

After we have an FBRocket object we call its login method that takes as parameters a LoginListener object that will be called after the user has logged in to Facebook.

onLoginSuccess(Facebook facebook) is called if the user has correctly logged in to Facebook. This is done by the following code.

Retrieving the Friends

List<String> uids = facebook.getFriendUIDs();
    for (String uid : uids) {
        Friend friend = facebook.getFriend(uid);
        result.add(friend);
    }

This Activity will be called by our main Activity, FacebookActivity. To be able to pass the list of Friends back to the invoking Activity we have to rely on a trick. We extend the Application class to have methods to allow us to set the list of Friends, and to retrieve the list of Friends.

MyApplication

public class MyApplication extends Application {
   
    private List<Friend> myFriends;
   
    public void putFriends(List<Friend> list){
        this.myFriends = list;
    }
    public List<Friend> getMyFriends(){
        return myFriends;
    }

}

We also need to change the AndroidManifest.xml document like this:

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="fb.slv" android:versionCode="1" android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name" android:name="MyApplication">
        <activity android:name=".FacebookActivity" android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <uses-permission android:name="android.permission.INTERNET"></uses-permission>
        </activity>
        <activity android:name=".MyFacebookApp" android:label="@string/app_name"></activity>
        <uses-permission android:name="android.permission.INTERNET"></uses-permission>
    </application>
    <uses-sdk android:minSdkVersion="3" />
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
</manifest>

Now, we can store the retrieved Friends by calling ((MyApplication) getApplication()).putFriends(result);

Getting the Friends

The main Activity, FacebookActivity, will start the new Activity, MyFacebookApp, and will wait until the latter finishes. This is done by the following piece of code:

Starting an Application and waiting for the result

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Intent intent = new Intent(this, MyFacebookApp.class);
        startActivityForResult(intent, 0);
}

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == 0) {
            List<Friend> result = ((MyApplication) getApplication())
                    .getMyFriends();
            myFriends.addFriends(result);
        }
}

Downloading friends' pictures in the background and displaying them

The Android UI is single-threaded. That means that only one thread is able to modify the UI. When dealing with Android UI, it is imperative to perform long running operations in th eUI thread. This will block the application's user interface.

Android provides different ways of performing long operations, like downloading file over the Internet. You can find a list here.

In this tutorial, we will use two mechanisms to not block the UI:

  • Activity.runOnUIThread
  • AsyncTask

Activity.runOnUIThread is used in the implementation of MyFacebookApp when loggin into Facebook.

To download friend pictures, we use the FriendImageDownloader class. It extends the AsyncTask class. To start the download, MyFriendsAdapter executes the following code:

Starting to download pictures

AsyncTask<Friend, Integer, Drawable> task = new FriendImageDownloader(
                    this);
startedTasks.put(friend.name, task);
task.execute(friend);

To keep the design as decoupled as possible, after the image has been downloaded, FriendImageDownloader calls the imageForFriend method on an object of type ImageViewResolver that is implemented by our MyFriendsAdapter class. The purpose of the ImageViewResolver interface is to notify it that the picture for a friend has been downloaded. The implementation of the imageForFriend in the MyFriendsAdapter class is the following:

Updating a friend's picture

@Override
    public synchronized void imageForFriend(String friend, Drawable image) {
        startedTasks.remove(friend);
        Pair t = mapFromFriendNameToTriple.get(friend);
        ViewGroup layout = (ViewGroup) t.getBigView();
        ((ImageView) layout.findViewById(R.id.MyFriend))
                .setImageDrawable(image);
        ((ImageView) t.getSmallView()).setImageDrawable(image);
        Log.e("IMAGE", "An image for " + friend + " has been downloaded");
        notifyDataSetChanged();
    }