illab.nabal.proxy.FacebookProxy.java Source code

Java tutorial

Introduction

Here is the source code for illab.nabal.proxy.FacebookProxy.java

Source

/*
 * Copyright (C) 2013-2014 Tan Jung
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package illab.nabal.proxy;

import illab.nabal.agent.Operation;
import illab.nabal.agent.dto.FacebookAlbum;
import illab.nabal.agent.dto.FacebookEvent;
import illab.nabal.agent.dto.FacebookLikableObject;
import illab.nabal.agent.dto.FacebookMessage;
import illab.nabal.agent.dto.FacebookMessageThread;
import illab.nabal.agent.dto.FacebookNote;
import illab.nabal.agent.dto.FacebookPhoto;
import illab.nabal.agent.dto.FacebookPost;
import illab.nabal.agent.dto.FacebookProfile;
import illab.nabal.exception.NetworkException;
import illab.nabal.exception.SystemException;
import illab.nabal.settings.SnsProperties;
import illab.nabal.settings.SocialNetwork;
import illab.nabal.settings.SystemProperties;
import illab.nabal.util.ParameterHelper;
import illab.nabal.util.StringHelper;
import illab.nabal.util.Util;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import org.apache.http.NameValuePair;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONArray;
import org.json.JSONObject;

import android.content.Context;
import android.text.TextUtils;
import android.util.Log;

/**
 * Implementations of Facebook APIs.
 * 
 * @version 1.0, 02/10/14
 * @author <a href="mailto:tanito.jung@gmail.com">Tan Jung</a>
 */
class FacebookProxy extends AbstractProxy implements FacebookDelegate {
    private final static String TAG = "FacebookProxy";

    /**
     * Dummy integer parameter for unspecified limit.
     */
    private final static int UNSPECIFIED_LIMIT = 0;

    /**
     * Dummy integer parameter for unspecified offset.
     */
    private final static int UNSPECIFIED_OFFSET = -1;

    /**
     * Dummy integer parameter for unspecified UNIX time.
     */
    private final static long UNSPECIFIED_UNIX_TIME = 0L;

    /**
     * Constant for me.
     */
    private final static String ME = "me";

    /**
     * Constant for friend requested date.
     */
    private final static String FRIEND_REQUEST_DATE = "friend_requested_date";

    /**
     * Constant for RSVP status.
     */
    private final static String RSVP_STATUS = "rsvp_status";

    /**
     * Default fields for profile query. 
     */
    private final static String[] DEFAULT_PROFILE_FIELDS_ = { "id", "name", "username", "gender", "picture", "link",
            "age_range" };

    /**
     * Facebook network context.
     */
    private final FacebookContext mFacebookContext;

    /**
     * Constructor for Facebook covert op proxy.
     * 
     * @param context
      * @param systemProperties
      * @param snsProperties
     */
    FacebookProxy(Context context, SystemProperties systemProperties, SnsProperties snsProperties) {
        mContext = context;
        mFacebookContext = (FacebookContext) (mNetworkContext = new FacebookContext(mContext,
                new NetworkSessionListener(), systemProperties, snsProperties));
    }

    /**
     * Constructor for Facebook proxy.
     * 
     * @param alias
     * @param context
      * @param systemProperties
      * @param snsProperties
     */
    FacebookProxy(String alias, Context context, SystemProperties systemProperties, SnsProperties snsProperties) {
        mContext = context;
        mFacebookContext = (FacebookContext) (mNetworkContext = new FacebookContext(alias, mContext,
                new NetworkSessionListener(), systemProperties, snsProperties));
    }

    /**
      * Inject a Facebook session.
     * 
     * @param accessToken
     */
    void injectSession(String accessToken) {
        mFacebookContext.injectSession(accessToken);
    }

    /* START OF ***************** API implementations ***********************/

    @Override
    public void getMyProfile(final Operation<FacebookDelegate, FacebookProfile> operation,
            final String[] additionalFields, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getProfile(ME, additionalFields, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getProfile(final Operation<FacebookDelegate, FacebookProfile> operation,
            final String userIdentifier, final String[] additionalFields, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getProfile(userIdentifier, additionalFields, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getMyFriendList(final Operation<FacebookDelegate, List<FacebookProfile>> operation, final int limit,
            final int offset, final String[] additionalFields, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getFriendList(ME, limit, offset, additionalFields, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getFriendList(final Operation<FacebookDelegate, List<FacebookProfile>> operation,
            final String userIdentifier, final int limit, final int offset, final String[] additionalFields,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getFriendList(userIdentifier, limit, offset, additionalFields, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getMutualFriendList(final Operation<FacebookDelegate, List<FacebookProfile>> operation,
            final String userIdentifier, final int limit, final int offset, final String[] additionalFields,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation
                            .onResult(getMutualFriendList(userIdentifier, limit, offset, additionalFields, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void hackFriendList(final Operation<FacebookDelegate, List<FacebookProfile>> operation,
            final String userUid) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(hackFriendList(userUid));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getSubscribedTo(final Operation<FacebookDelegate, List<FacebookProfile>> operation,
            final String userIdentifier, final int limit, final String after, final String before,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getSubscribedTo(userIdentifier, limit, after, before, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getSubscribers(final Operation<FacebookDelegate, List<FacebookProfile>> operation,
            final String userIdentifier, final int limit, final String after, final String before,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getSubscribers(userIdentifier, limit, after, before, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getFriendRequestsForMe(final Operation<FacebookDelegate, List<FacebookProfile>> operation,
            final boolean getOnlyUnread, final int limit, final int offset, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getFriendRequestsForMe(getOnlyUnread, limit, offset, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getMyEvents(final Operation<FacebookDelegate, List<FacebookEvent>> operation,
            final FacebookEvent.Filter filter, final int limit, final int offset, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getEvents(ME, filter, limit, offset, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getEvents(final Operation<FacebookDelegate, List<FacebookEvent>> operation,
            final String userIdentifier, final FacebookEvent.Filter filter, final int limit, final int offset,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getEvents(userIdentifier, filter, limit, offset, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getEventParticipants(final Operation<FacebookDelegate, List<FacebookProfile>> operation,
            final String eventId, final FacebookEvent.ParticipantStatus participantStatus, final int limit,
            final int offset, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getEventParticipants(eventId, participantStatus, limit, offset, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getHomeTimeline(final Operation<FacebookDelegate, List<FacebookPost>> operation, final long since,
            final long until, final int limit, final Locale locale, final boolean isUnixTime) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getHomeTimeline(since, until, limit, locale, isUnixTime));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getMyTimeline(final Operation<FacebookDelegate, List<FacebookPost>> operation, final long since,
            final long until, final int limit, final boolean textOnly, final Locale locale,
            final boolean isUnixTime) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getTimeline(ME, since, until, limit, textOnly, locale, isUnixTime));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getTimeline(final Operation<FacebookDelegate, List<FacebookPost>> operation,
            final String ownerIdentifier, final long since, final long until, final int limit,
            final boolean textOnly, final Locale locale, final boolean isUnixTime) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(
                            getTimeline(ownerIdentifier, since, until, limit, textOnly, locale, isUnixTime));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getMyLastestFeedItem(final Operation<FacebookDelegate, FacebookPost> operation,
            final boolean textOnly, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    // get my feed limited to 1
                    List<FacebookPost> postList = getTimeline(ME, UNSPECIFIED_UNIX_TIME, UNSPECIFIED_UNIX_TIME, 1,
                            textOnly, locale, false);
                    if (postList != null) {
                        operation.onResult(postList.get(0));
                    }
                    // throw an Exception if no result
                    else {
                        throw new NetworkException("No post retrieved.");
                    }
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getLastestFeedItem(final Operation<FacebookDelegate, FacebookPost> operation,
            final String ownerIdentifier, final boolean textOnly, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    // get feed limited to 1
                    List<FacebookPost> postList = getTimeline(ownerIdentifier, UNSPECIFIED_UNIX_TIME,
                            UNSPECIFIED_UNIX_TIME, 1, textOnly, locale, false);
                    if (postList != null) {
                        operation.onResult(postList.get(0));
                    }
                    // throw an exception if no result
                    else {
                        throw new NetworkException("No post retrieved.");
                    }
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getPost(final Operation<FacebookDelegate, FacebookPost> operation, final String postId,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getPost(postId, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getAlbums(final Operation<FacebookDelegate, List<FacebookAlbum>> operation,
            final String ownerIdentifier, final int limit, final String after, final String before,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getAlbums(ownerIdentifier, limit, after, before, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getPhotos(final Operation<FacebookDelegate, List<FacebookPhoto>> operation,
            final String ownerIdentifier, final long since, final long until, final int limit, final Locale locale,
            final boolean isUnixTime) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(
                            getPhotos(ownerIdentifier, since, until, limit, null, null, locale, isUnixTime));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getPhotos(final Operation<FacebookDelegate, List<FacebookPhoto>> operation,
            final String ownerIdentifier, final int limit, final String after, final String before,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getPhotos(ownerIdentifier, UNSPECIFIED_UNIX_TIME, UNSPECIFIED_UNIX_TIME,
                            limit, after, before, locale, false));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getPhoto(final Operation<FacebookDelegate, FacebookPhoto> operation, final String photoId,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getPhoto(photoId, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getMyMessageThreads(final Operation<FacebookDelegate, List<FacebookMessageThread>> operation,
            final int howManyMessages, final long since, final long until, final int limit, final Locale locale,
            final boolean isUnixTime) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(
                            getMyMessageThreads(howManyMessages, since, until, limit, locale, isUnixTime));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getMessageThread(final Operation<FacebookDelegate, FacebookMessageThread> operation,
            final String messageThreadId, final Locale locale, final boolean isUnixTime) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getMessageThread(messageThreadId, locale, isUnixTime));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getMessages(final Operation<FacebookDelegate, List<FacebookMessage>> operation,
            final String messageThreadId, final long since, final long until, final int limit, final Locale locale,
            final boolean isUnixTime) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getMessages(messageThreadId, since, until, limit, locale, isUnixTime));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getMyNotes(final Operation<FacebookDelegate, List<FacebookNote>> operation, final int limit,
            final String after, final String before, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getNotes(ME, limit, after, before, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getNotes(final Operation<FacebookDelegate, List<FacebookNote>> operation,
            final String userIdentifier, final int limit, final String after, final String before,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getNotes(userIdentifier, limit, after, before, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getNote(final Operation<FacebookDelegate, FacebookNote> operation, final String noteId,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getNote(noteId, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getComments(final Operation<FacebookDelegate, List<FacebookPost>> operation,
            final String parentPostId, final int limit, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getComments(parentPostId, limit, null, null, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getNextPageComments(final Operation<FacebookDelegate, List<FacebookPost>> operation,
            final List<FacebookPost> commentList) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    // check comment list
                    if (commentList == null || commentList.isEmpty() == true
                            || StringHelper.isEmpty(commentList.get(0).getParentPostId()) == true) {
                        throw new SystemException("Invalid comment list.");
                    }
                    // extract required values from comment list
                    String parentPostId = commentList.get(0).getParentPostId();
                    int limit = commentList.size();
                    String after = commentList.get(0).getAfter();
                    Locale locale = commentList.get(0).getLocale();
                    // set after cursor to navigate forward to next page
                    operation.onResult(getComments(parentPostId, limit, after, null, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getPreviousPageComments(final Operation<FacebookDelegate, List<FacebookPost>> operation,
            final List<FacebookPost> commentList) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    // check comment list
                    if (commentList == null || commentList.isEmpty() == true
                            || StringHelper.isEmpty(commentList.get(0).getParentPostId()) == true) {
                        throw new SystemException("Invalid comment list.");
                    }
                    // extract required values from comment list
                    String parentPostId = commentList.get(0).getParentPostId();
                    int limit = commentList.size();
                    String before = commentList.get(0).getBefore();
                    Locale locale = commentList.get(0).getLocale();
                    // set before cursor to navigate forward to next page
                    operation.onResult(getComments(parentPostId, limit, null, before, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getComments(final Operation<FacebookDelegate, List<FacebookPost>> operation,
            final String parentPostId, final int limit, final String after, final String before,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getComments(parentPostId, limit, after, before, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getLikes(final Operation<FacebookDelegate, List<FacebookProfile>> operation, final String postId,
            final int limit, final String after, final String before, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getLikes(postId, limit, after, before, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getMyLikedObjects(final Operation<FacebookDelegate, List<FacebookLikableObject>> operation,
            final int limit, final int offset, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getLikedObjects(ME, limit, offset, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getLikedObjects(final Operation<FacebookDelegate, List<FacebookLikableObject>> operation,
            final String ownerIdentifier, final int limit, final int offset, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getLikedObjects(ownerIdentifier, limit, offset, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void postStatus(final Operation<FacebookDelegate, FacebookPost> operation, final String message,
            final String placeId, final String[] tags, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    // check message
                    if (StringHelper.isEmpty(message) == true) {
                        throw new SystemException("Invalid message.");
                    }
                    // generate tag CSV 
                    String tagString = null;
                    if (tags != null && tags.length > 0) {
                        tagString = TextUtils.join(",", tags);
                    }
                    operation.onResult(publishPost(ME, message, null, null, null, null, null, null, placeId,
                            tagString, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void postLink(final Operation<FacebookDelegate, FacebookPost> operation, final String message,
            final String picture, final String link, final String name, final String caption,
            final String description, final String source, final String placeId, final String[] tags,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    // check link URL
                    if (StringHelper.isEmpty(link) == true) {
                        throw new SystemException("Invalid link URL.");
                    }
                    // generate tag CSV 
                    String tagString = null;
                    if (tags != null && tags.length > 0) {
                        tagString = TextUtils.join(",", tags);
                    }
                    operation.onResult(publishPost(ME, message, picture, link, name, caption, description, source,
                            placeId, tagString, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void postPhoto(final Operation<FacebookDelegate, FacebookPost> operation,
            final String absolutePathOfPhoto, final String albumId, final String message, final String placeId,
            final String[] tags, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(postPhoto(absolutePathOfPhoto, albumId, message, placeId, tags, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void postVideo(final Operation<FacebookDelegate, FacebookPost> operation,
            final String absolutePathOfVideo, final String title, final String description, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(postVideo(absolutePathOfVideo, title, description, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void postStatus(final Operation<FacebookDelegate, FacebookPost> operation, final String ownerIdentifier,
            final String message, final String placeId, final String[] tags, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    // check message
                    if (StringHelper.isEmpty(message) == true) {
                        throw new SystemException("Invalid message.");
                    }
                    // generate tag CSV 
                    String tagString = null;
                    if (tags != null && tags.length > 0) {
                        tagString = TextUtils.join(",", tags);
                    }
                    operation.onResult(publishPost(ownerIdentifier, message, null, null, null, null, null, null,
                            placeId, tagString, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void postLink(final Operation<FacebookDelegate, FacebookPost> operation, final String ownerIdentifier,
            final String message, final String picture, final String link, final String name, final String caption,
            final String description, final String source, final String placeId, final String[] tags,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    // check link URL
                    if (StringHelper.isEmpty(link) == true) {
                        throw new SystemException("Invalid link URL.");
                    }
                    // generate tag CSV 
                    String tagString = null;
                    if (tags != null && tags.length > 0) {
                        tagString = TextUtils.join(",", tags);
                    }
                    operation.onResult(publishPost(ownerIdentifier, message, picture, link, name, caption,
                            description, source, placeId, tagString, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void postNote(final Operation<FacebookDelegate, FacebookPost> operation, final String subject,
            final String message, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(postNote(subject, message, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void postComment(final Operation<FacebookDelegate, FacebookPost> operation, final String parentPostId,
            final String message, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(postComment(parentPostId, message, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void createEvent(final Operation<FacebookDelegate, FacebookEvent> operation,
            final String ownerIdentifier, final String name, final String description, final String startTime,
            final String endTime, final String locationName, final FacebookEvent.Privacy privacy,
            final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(createEvent(ownerIdentifier, name, description, startTime, endTime,
                            locationName, null, privacy, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void createEventByPlaceId(final Operation<FacebookDelegate, FacebookEvent> operation,
            final String ownerIdentifier, final String name, final String description, final String startTime,
            final String endTime, final String placeId, final FacebookEvent.Privacy privacy, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(createEvent(ownerIdentifier, name, description, startTime, endTime, null,
                            placeId, privacy, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void attend(final Operation<FacebookDelegate, Boolean> operation, final String eventId) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(setRsvp(eventId, "attending"));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void maybe(final Operation<FacebookDelegate, Boolean> operation, final String eventId) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(setRsvp(eventId, "maybe"));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void decline(final Operation<FacebookDelegate, Boolean> operation, final String eventId) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(setRsvp(eventId, "declined"));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void like(final Operation<FacebookDelegate, Boolean> operation, final String objectId) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(like(objectId));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void unlike(final Operation<FacebookDelegate, Boolean> operation, final String objectId) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(unlike(objectId));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void delete(final Operation<FacebookDelegate, Boolean> operation, final String objectId) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(delete(objectId));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getFacebookObject(final Operation<FacebookDelegate, JSONObject> operation,
            final String facebookObjectId, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getFacebookObject(facebookObjectId, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getGrantedPermissions(final Operation<FacebookDelegate, JSONObject> operation) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getGrantedPermissions());
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getPageAccounts(final Operation<FacebookDelegate, JSONObject> operation, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getPageAccounts(locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getMyGroups(final Operation<FacebookDelegate, JSONObject> operation, final int limit,
            final int offset, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getGroups(ME, limit, offset, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    @Override
    public void getGroups(final Operation<FacebookDelegate, JSONObject> operation, final String userIdentifier,
            final int limit, final int offset, final Locale locale) {
        requestJob(new ProxyJob() {
            public void onFail(Exception e) {
                operation.onFail(e);
            }

            public void run() {
                try {
                    operation.onResult(getGroups(userIdentifier, limit, offset, locale));
                } catch (Exception e) {
                    onFail(e);
                }
            }
        });
    }

    /**
     * Get a Facebook profile corresponding to given user name.
     * 
     * @param userIdentifier
     * @param additionalFields
     * @return FacebookProfile
     * @throws Exception
     */
    private FacebookProfile getProfile(String userIdentifier, String[] additionalFields, Locale locale)
            throws Exception {

        // throw an exception if user identifier is invalid
        if (StringHelper.isEmpty(userIdentifier) == true) {
            throw new SystemException("Invalid user identifier.");
        }

        String apiUri = "/" + userIdentifier;

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", getProfileFields(additionalFields)),
                new BasicNameValuePair("locale", localeParam.toString()));

        return populateProfileBean(additionalFields, localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get friend list from an user who authorized this Facebook app.
     * 
     * @param userIdentifier
     * @param limit
     * @param offset
     * @param additionalFields
     * @param locale
     * @return list of FacebookProfile objects
     * @throws Exception
     */
    private List<FacebookProfile> getFriendList(String userIdentifier, int limit, int offset,
            String[] additionalFields, Locale locale) throws Exception {

        // throw an exception if user identifier is invalid
        if (StringHelper.isEmpty(userIdentifier) == true) {
            throw new SystemException("Invalid user identifier.");
        }

        String apiUri = "/" + userIdentifier + "/friends";

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", getProfileFields(additionalFields)),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageOffsetParams(params, limit, offset);

        return populateProfileList(additionalFields, localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get mutual friends between the owner of the current access token and
     * given user identifier.
     * 
     * @param userIdentifier
     * @param limit
     * @param offset
     * @param additionalFields
     * @param locale
     * @return list of FacebookProfile objects
     * @throws Exception
     */
    private List<FacebookProfile> getMutualFriendList(String userIdentifier, int limit, int offset,
            String[] additionalFields, Locale locale) throws Exception {

        // throw an exception if user identifier is invalid
        if (StringHelper.isEmpty(userIdentifier) == true) {
            throw new SystemException("Invalid user identifier.");
        }

        String apiUri = "/me/mutualfriends/" + userIdentifier;

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", getProfileFields(additionalFields)),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageOffsetParams(params, limit, offset);

        return populateProfileList(additionalFields, localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Hack and retrieve an user's friends if the user allowed to view his/her friends.
     * 
     * @param userUid
     * @return list of FacebookProfile objects
     * @throws Exception
     */
    private List<FacebookProfile> hackFriendList(String userUid) throws Exception {

        //https://www.facebook.com/ajax/typeahead_friends.php?u=404105&__a=1

        // throw an exception if user identifier is invalid
        if (StringHelper.isEmpty(userUid) == true) {
            throw new SystemException("Invalid user UID.");
        }

        String apiFullUrl = "https://www.facebook.com/ajax/typeahead_friends.php?__a=1&u=" + userUid;

        String responseString = getResponseString(new HttpGet(apiFullUrl), false);
        JSONObject responseJson = new JSONObject(responseString.replace("for (;;);", ""));

        // if error occurred
        JSONObject payloadJson = responseJson.optJSONObject("payload");
        if (responseJson.has("error") == true || payloadJson == null || payloadJson.optJSONArray("friends") == null
                || payloadJson.getJSONArray("friends").length() == 0) {
            throw new NetworkException("Error occurred while retrieving friend list.", responseString);
        }

        List<FacebookProfile> profileList = new ArrayList<FacebookProfile>();

        // iterate result
        for (int i = 0; i < payloadJson.getJSONArray("friends").length(); i++) {
            JSONObject profileJson = payloadJson.getJSONArray("friends").getJSONObject(i);
            FacebookProfile facebookProfile = new FacebookProfile();
            facebookProfile.setLocale(Locale.US);
            facebookProfile.setSnsUid(SocialNetwork.FACEBOOK);
            facebookProfile.setProfileJson(profileJson);
            facebookProfile.setUid(profileJson.optString("i"));
            facebookProfile.setFullName(profileJson.optString("t"));
            facebookProfile.setProfilePageUrl(profileJson.optString("u"));
            profileList.add(facebookProfile);
        }

        return profileList;
    }

    /**
     * Get user profiles that the owner of the current access token subscribed to. 
     * 
     * @param userIdentifier
     * @param limit
     * @param after
     * @param before
     * @param locale
     * @return list of FacebookProfile
     * @throws Exception
     */
    private List<FacebookProfile> getSubscribedTo(String userIdentifier, int limit, String after, String before,
            Locale locale) throws Exception {

        // throw an exception if user identifier is invalid
        if (StringHelper.isEmpty(userIdentifier) == true) {
            throw new SystemException("Invalid user identifier.");
        }

        String apiUri = "/" + userIdentifier + "/subscribedto";

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", getProfileFields(null)),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageCursorParams(params, limit, after, before);

        return populateProfileList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get user profiles that subscribed to the timeline owned by the owner of 
     * the current access token.
     * 
     * @param userIdentifier
     * @param limit
     * @param after
     * @param before
     * @param locale
     * @return list of FacebookProfile
     * @throws Exception
     */
    private List<FacebookProfile> getSubscribers(String userIdentifier, int limit, String after, String before,
            Locale locale) throws Exception {

        // throw an exception if user identifier is invalid
        if (StringHelper.isEmpty(userIdentifier) == true) {
            throw new SystemException("Invalid user identifier.");
        }

        String apiUri = "/" + userIdentifier + "/subscribers";

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", getProfileFields(null)),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageCursorParams(params, limit, after, before);

        return populateProfileList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get friend requests for the owner of the current access token.
     * 
     * @param getOnlyUnread
     * @param limit
     * @param offset
     * @param locale
     * @return list of FacebookProfile
     * @throws Exception
     */
    private List<FacebookProfile> getFriendRequestsForMe(boolean getOnlyUnread, int limit, int offset,
            Locale locale) throws Exception {

        String apiUri = "/me/friendrequests";

        // fields parameters - expand from field to get details 
        String[] fields = { "to", "from.fields(" + getProfileFields(null) + ")", "message", "unread",
                "created_time" };

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", TextUtils.join(",", fields)),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageOffsetParams(params, limit, offset);

        return populateProfileList(null, getOnlyUnread, localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get events.
     * 
     * @param ownerIdentifier
     * @param filter
     * @param limit
     * @param offset
     * @param locale
     * @return list of FacebookEvent objects
     * @throws Exception
     */
    private List<FacebookEvent> getEvents(String ownerIdentifier, FacebookEvent.Filter filter, int limit,
            int offset, Locale locale) throws Exception {

        // throw an exception if owner identifier is invalid
        if (StringHelper.isEmpty(ownerIdentifier) == true) {
            throw new SystemException("Invalid owner identifier.");
        }

        String uriEndpoint = "";

        // set filter if needed
        if (filter != null) {
            uriEndpoint = "/" + filter.getFilterString();
        }

        String apiUri = "/" + ownerIdentifier + "/events" + uriEndpoint;

        String[] fields = { "id", "owner", "name", "description", "cover", "start_time", "end_time", "timezone",
                "location,venue", "privacy", "updated_time" };

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", TextUtils.join(",", fields)),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageOffsetParams(params, limit, offset);

        return populateEventList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get participants of the event corresponding to given event ID.
     * 
     * @param eventId
     * @param participantStatus
     * @param limit
     * @param offset
     * @param locale
     * @return list of FacebookProfile objects
     * @throws Exception
     */
    private List<FacebookProfile> getEventParticipants(String eventId,
            FacebookEvent.ParticipantStatus participantStatus, int limit, int offset, Locale locale)
            throws Exception {

        // throw an exception if event ID is invalid
        if (StringHelper.isEmpty(eventId) == true) {
            throw new SystemException("Invalid event ID.");
        }

        String uriEndpoint = FacebookEvent.ParticipantStatus.INVITED.getUriEndpointString();

        // set participant status if needed
        if (participantStatus != null) {
            uriEndpoint = participantStatus.getUriEndpointString();
        }

        String apiUri = "/" + eventId + "/" + uriEndpoint;

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", getProfileFields(null) + "," + RSVP_STATUS),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageOffsetParams(params, limit, offset);

        return populateProfileList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get home timeline.
     * 
     * @param since
     * @param until
     * @param limit
     * @param locale
     * @param isUnixTime
     * @return list of FacebookPost objects
     * @throws Exception
     */
    private List<FacebookPost> getHomeTimeline(long since, long until, int limit, Locale locale, boolean isUnixTime)
            throws Exception {

        String apiUri = "/me/home";

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setTimeBasedPageParams(params, since, until, limit, isUnixTime);

        return populatePostList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get timeline owned by given owner indentifier.
     * 
     * @param ownerIdentifier
     * @param since
     * @param until
     * @param limit
     * @param textOnly
     * @param locale
     * @return list of FacebookPost objects
     * @throws Exception
     */
    private List<FacebookPost> getTimeline(String ownerIdentifier, long since, long until, int limit,
            boolean textOnly, Locale locale, boolean isUnixTime) throws Exception {

        // throw an exception if owner identifier is invalid
        if (StringHelper.isEmpty(ownerIdentifier) == true) {
            throw new SystemException("Invalid owner identifier.");
        }

        String uriEndpoint = "feed";

        // get text status updates only if set so
        if (textOnly == true) {
            uriEndpoint = "statuses";
        }

        String apiUri = "/" + ownerIdentifier + "/" + uriEndpoint;

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setTimeBasedPageParams(params, since, until, limit, isUnixTime);

        return populatePostList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get a post corresponding to given post ID.
     *
     * @param postId
     * @param locale
     * @return FacebookPost
     * @throws Exception
     */
    private FacebookPost getPost(String postId, Locale locale) throws Exception {

        // throw an exception if post ID is invalid
        if (StringHelper.isEmpty(postId) == true) {
            throw new SystemException("Invalid post ID.");
        }

        String apiUri = "/" + postId;

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        return populatePostBean(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get albums owned by given owner identifier.
     * 
     * @param ownerIdentifier
     * @param limit
     * @param after
     * @param before
     * @param locale
     * @return list of FacebookAlbum objects
     * @throws Exception
     */
    private List<FacebookAlbum> getAlbums(String ownerIdentifier, int limit, String after, String before,
            Locale locale) throws Exception {

        // throw an exception if owner identifier is invalid
        if (StringHelper.isEmpty(ownerIdentifier) == true) {
            throw new SystemException("Invalid owner identifier.");
        }

        String apiUri = "/" + ownerIdentifier + "/albums";

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageCursorParams(params, limit, after, before);

        return populateAlbumList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get photos from given owner identifier.
     * 
     * @param ownerIdentifier
     * @param since
     * @param until
     * @param limit
     * @param after
     * @param before
     * @param locale
     * @param isUnixTime
     * @return list of FacebookPhoto objects
     * @throws Exception
     */
    private List<FacebookPhoto> getPhotos(String ownerIdentifier, long since, long until, int limit, String after,
            String before, Locale locale, boolean isUnixTime) throws Exception {

        // throw an exception if owner identifier is invalid
        if (StringHelper.isEmpty(ownerIdentifier) == true) {
            throw new SystemException("Invalid owner identifier.");
        }

        String apiUri = "/" + ownerIdentifier + "/photos";

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set cursor page parameters if needed
        setPageCursorParams(params, limit, after, before);

        // set time-based page parameters if needed
        setTimeBasedPageParams(params, since, until, limit, isUnixTime);

        return populatePhotoList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get a Facebook photo object.
     * 
     * @param photoId
     * @param locale
     * @return FacebookPhoto
     * @throws Exception
     */
    private FacebookPhoto getPhoto(String photoId, Locale locale) throws Exception {

        // throw an exception if photo ID is invalid
        if (StringHelper.isEmpty(photoId) == true) {
            throw new SystemException("Invalid photo ID.");
        }

        String apiUri = "/" + photoId;

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        return populatePhotoBean(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get my message threads.
     * 
     * @param howManyMessages
     * @param since
     * @param until
     * @param limit
     * @param locale
     * @param isUnixTime
     * @return list of FacebookMessageThread objects
     * @throws Exception
     */
    private List<FacebookMessageThread> getMyMessageThreads(int howManyMessages, long since, long until, int limit,
            Locale locale, boolean isUnixTime) throws Exception {

        String apiUri = "/me/inbox";

        // validate how many messages per thread
        int messageLimit = howManyMessages;
        if (messageLimit < 1) {
            messageLimit = 1;
        } else if (messageLimit > 30) {
            messageLimit = 30;
        }

        // fields parameters
        String[] fields = { "id", "to", "unread", "unseen", "updated_time",
                "comments.limit(" + messageLimit + ")" };

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", TextUtils.join(",", fields)),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setTimeBasedPageParams(params, since, until, limit, isUnixTime);

        return populateMessageThreadList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get a message thread with last 30 messages in it.
     * 
     * @param messageThreadId
     * @param locale
     * @param isUnixTime
     * @return FacebookMessageThread
     * @throws Exception
     */
    private FacebookMessageThread getMessageThread(String messageThreadId, Locale locale, boolean isUnixTime)
            throws Exception {

        // throw an exception if message threaad ID is invalid
        if (StringHelper.isEmpty(messageThreadId) == true) {
            throw new SystemException("Invalid message thread ID.");
        }

        String apiUri = "/" + messageThreadId;

        // fields parameters
        String[] fields = { "id", "to", "unread", "unseen", "updated_time", "comments.limit(30)" };

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", TextUtils.join(",", fields)),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set date format parameter
        setDateFormatParam(params, isUnixTime);

        return populateMessageThreadBean(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get 30 messages from message thread corresponding to given message thread ID.
     * 
     * @param messageThreadId
     * @param since
     * @param until
     * @param limit
     * @param locale
     * @param isUnixTime
     * @return list of FacebookMessage objects
     * @throws Exception
     */
    private List<FacebookMessage> getMessages(String messageThreadId, long since, long until, int limit,
            Locale locale, boolean isUnixTime) throws Exception {

        // throw an exception if message threaad ID is invalid
        if (StringHelper.isEmpty(messageThreadId) == true) {
            throw new SystemException("Invalid message thread ID.");
        }

        String apiUri = "/" + messageThreadId + "/comments";

        // validate limit
        int messageLimit = limit;
        if (messageLimit < 1) {
            messageLimit = 1;
        } else if (messageLimit > 30) {
            messageLimit = 30;
        }

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setTimeBasedPageParams(params, since, until, messageLimit, isUnixTime);

        return populateMessageList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get notes owned by given user identifier.
     * 
     * @param userIdentifier
     * @param limit
     * @param after
     * @param before
     * @param locale
     * @return list of FacebookNote objects
     * @throws Exception
     */
    private List<FacebookNote> getNotes(String userIdentifier, int limit, String after, String before,
            Locale locale) throws Exception {

        // throw an exception if user identifier is invalid
        if (StringHelper.isEmpty(userIdentifier) == true) {
            throw new SystemException("Invalid user identifier.");
        }

        String apiUri = "/" + userIdentifier + "/notes";

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageCursorParams(params, limit, after, before);

        return populateNoteList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get a note corresponding to given note ID.
     * 
     * @param noteId
     * @param locale
     * @return FacebookNote
     * @throws Exception
     */
    private FacebookNote getNote(String noteId, Locale locale) throws Exception {

        // throw an exception if note ID is invalid
        if (StringHelper.isEmpty(noteId) == true) {
            throw new SystemException("Invalid note ID.");
        }

        String apiUri = "/" + noteId;

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        return populateNoteBean(localeParam, getResponseJson(getHttpGet(apiUri, params)), null, null);
    }

    /**
     * Get comments attached to a post corresponding to given parent post ID.
     * 
     * @param parentPostId
     * @param limit
     * @param after
     * @param before
     * @param locale
     * @return list of FacebookPost objects
     * @throws Exception
     */
    private List<FacebookPost> getComments(String parentPostId, int limit, String after, String before,
            Locale locale) throws Exception {

        // throw an exception if parent post ID is invalid
        if (StringHelper.isEmpty(parentPostId) == true) {
            throw new SystemException("Invalid parent post ID.");
        }

        String apiUri = "/" + parentPostId + "/comments";

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageCursorParams(params, limit, after, before);

        return populatePostList(parentPostId, localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get users who liked the post/comment corresponding to given post ID.
     * 
     * @param postId
     * @param limit
     * @param after
     * @param before
     * @param locale
     * @return list of FacebookProfile objects
     * @throws Exception
     */
    private List<FacebookProfile> getLikes(String postId, int limit, String after, String before, Locale locale)
            throws Exception {

        // throw an exception if post ID is invalid
        if (StringHelper.isEmpty(postId) == true) {
            throw new SystemException("Invalid post ID.");
        }

        String apiUri = "/" + postId + "/likes";

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", "id,username,name"),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageCursorParams(params, limit, after, before);

        return populateProfileList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Get Facebook objects that are liked by given owner identifier.
     * 
     * @param ownerIdentifier
     * @param limit
     * @param offset
     * @param locale
     * @return list of FacebookLikableObject objects
     * @throws Exception
     */
    private List<FacebookLikableObject> getLikedObjects(String ownerIdentifier, int limit, int offset,
            Locale locale) throws Exception {

        String apiUri = "/" + ownerIdentifier + "/likes";

        // fields parameters
        String[] fields = { "id", "category", "name", "description", "link", "can_post", "talking_about_count",
                "website", "cover", "created_time" };

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        // check locale and set to system default if null
        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("fields", TextUtils.join(",", fields)),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageOffsetParams(params, limit, offset);

        return populateLikableList(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Publish a post.
     * 
     * @param ownerIdentifier
     * @param message
     * @param picture
     * @param link
     * @param name
     * @param caption
     * @param description
     * @param source
     * @param placeId
     * @param tags
     * @param locale
     * @return FacebookPost
     * @throws Exception
     */
    private FacebookPost publishPost(String ownerIdentifier, String message, String picture, String link,
            String name, String caption, String description, String source, String placeId, String tags,
            Locale locale) throws Exception {

        String apiUri = "/" + ownerIdentifier + "/feed";

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("message", message), new BasicNameValuePair("picture", picture),
                new BasicNameValuePair("link", link), new BasicNameValuePair("name", name),
                new BasicNameValuePair("caption", caption), new BasicNameValuePair("description", description),
                new BasicNameValuePair("source", source), new BasicNameValuePair("place", placeId),
                new BasicNameValuePair("tags", tags));

        // execute post
        JSONObject responseJson = getResponseJson(getHttpPost(apiUri, params));

        // check returned JSON
        if (responseJson.has("id") == false) {
            NetworkException ne = new NetworkException("Invalid response.");
            ne.setResponseJson(responseJson);
            throw ne;
        }

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        // fetch and return the post object that just has been created
        return getPost(responseJson.getString("id"), localeParam);
    }

    /**
     * Post a photo.
     * 
     * @param absolutePathOfPhoto
     * @param albumId
     * @param message
     * @param placeId
     * @return FacebookPost
     * @throws Exception
     */
    private FacebookPost postPhoto(String absolutePathOfPhoto, String albumId, String message, String placeId,
            String[] tags, Locale locale) throws Exception {

        String objectIdentifier = ME;

        // check album ID
        if (StringHelper.isEmpty(albumId) == false) {
            objectIdentifier = albumId;
        }

        String apiUri = "/" + objectIdentifier + "/photos";

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("message", StringHelper.nvl(message)),
                new BasicNameValuePair("place", placeId));

        // add tags parameter if needed
        if (tags != null && tags.length > 0) {
            //[{"tag_uid":"1234"}, {"tag_uid":"12345"}]
            String tagString = "[";
            int count = 0;
            for (String userIdentifier : tags) {
                if (count > 0) {
                    tagString += ",";
                }
                tagString += "{\"tag_uid\":\"" + userIdentifier + "\"}";
                count++;
            }
            tagString += "]";
            Log.w(TAG, "## tag string : " + tagString);
            params.add(new BasicNameValuePair("tags", tagString));
        }

        JSONObject responseJson = getResponseJson(
                getMultipartHttpPost(apiUri, params, "source", absolutePathOfPhoto));

        // check returned JSON
        if (responseJson.has("id") == false && responseJson.has("post_id") == false) {
            NetworkException ne = new NetworkException("Invalid response.");
            ne.setResponseJson(responseJson);
            throw ne;
        }

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        // fetch the photo object that just has been created
        FacebookPost photoObject = getPost(responseJson.getString("id"), localeParam);

        FacebookPost returningPost = null;

        // if the photo has been successfully posted to timeline
        if (responseJson.has("post_id") == true) {

            // fetch the post object that just has been created
            FacebookPost timelinePost = getPost(responseJson.getString("post_id"), localeParam);

            // set the photo caption as the text content of the post if exists
            timelinePost.setTextContent(photoObject.getStringFromRawJson("name"));

            // set the location data of the photo to the post object if exists
            if (photoObject.getPlaceJson() != null) {
                timelinePost.setPlaceId(photoObject.getPlaceJson().optString("id"));
                timelinePost.setPlaceName(photoObject.getPlaceJson().optString("name"));
                if (photoObject.getPlaceJson().has("location") == true) {
                    timelinePost.setPlaceLatitude(Float.parseFloat(
                            photoObject.getPlaceJson().getJSONObject("location").optString("latitude")));
                    timelinePost.setPlaceLongitude(Float.parseFloat(
                            photoObject.getPlaceJson().getJSONObject("location").optString("longitude")));
                }
            }

            // return the post object that just has been created
            returningPost = timelinePost;
        }

        // if the photo has been uploaded succssuflly but NOT been posted to timeline
        else {
            // set the photo caption as the text content if exists
            photoObject.setTextContent(photoObject.getStringFromRawJson("name"));

            // return the photo object that just has been created
            returningPost = photoObject;
        }

        return returningPost;
    }

    /**
     * Post a video.
     * 
     * @param absolutePathOfVideo
     * @param title
     * @param description
     * @param locale
     * @return FacebookPost
     * @throws Exception
     */
    private FacebookPost postVideo(String absolutePathOfVideo, String title, String description, Locale locale)
            throws Exception {

        String apiUri = "/me/videos";

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("title", StringHelper.nvl(title)),
                new BasicNameValuePair("description", StringHelper.nvl(description)));

        JSONObject responseJson = getResponseJson(
                getMultipartHttpPost(apiUri, params, "source", absolutePathOfVideo));

        // check returned JSON
        if (responseJson.has("id") == false) {
            NetworkException ne = new NetworkException("Invalid response.");
            ne.setResponseJson(responseJson);
            throw ne;
        }

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        // extract video post ID
        FacebookProfile myProfile = getProfile(ME, null, localeParam);
        String postId = myProfile.getUid() + "_" + responseJson.getString("id");

        // set post ID only because it takes a few seconds for Facebook system
        // to generate the video post -- so the video post cannot be accessible 
        // at this moment
        FacebookPost videoPost = new FacebookPost();
        videoPost.setSnsUid(SocialNetwork.FACEBOOK);
        videoPost.setPostId(postId);
        videoPost.setPosterUid(myProfile.getUid());
        videoPost.setPosterUserName(myProfile.getUserName());
        videoPost.setPosterFullName(myProfile.getFullName());

        // return the post object that just has been created
        return videoPost;
    }

    /**
     * Post a note.
     * 
     * @param subject
     * @param message
     * @param locale
     * @return FacebookPost
     * @throws Exception
     */
    private FacebookPost postNote(String subject, String message, Locale locale) throws Exception {

        // check subject
        if (StringHelper.isEmpty(subject) == true) {
            throw new SystemException("Invalid note subject.");
        }

        // check content
        if (StringHelper.isEmpty(message) == true) {
            throw new SystemException("Invalid note content.");
        }

        String apiUri = "/me/notes";

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("subject", subject), new BasicNameValuePair("message", message));

        JSONObject responseJson = getResponseJson(getHttpPost(apiUri, params));

        // check returned JSON
        if (responseJson.has("id") == false) {
            NetworkException ne = new NetworkException("Invalid response.");
            ne.setResponseJson(responseJson);
            throw ne;
        }

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        // extract note post ID
        FacebookProfile myProfile = getProfile(ME, null, localeParam);
        String postId = myProfile.getUid() + "_" + responseJson.getString("id");

        return getPost(postId, localeParam);
    }

    /**
     * Post a comment to a post corresponding to given post ID.
     * 
     * @param parentPostId
     * @param message
     * @param locale
     * @return FacebookPost
     * @throws Exception
     */
    private FacebookPost postComment(String parentPostId, String message, Locale locale) throws Exception {

        // throw an exception if parent post ID is invalid
        if (StringHelper.isEmpty(parentPostId) == true) {
            throw new SystemException("Invalid parent post ID.");
        }

        // check message
        if (StringHelper.isEmpty(message) == true) {
            throw new SystemException("Invalid message.");
        }

        String apiUri = "/" + parentPostId + "/comments";

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("message", message));

        // execute post
        JSONObject responseJson = getResponseJson(getHttpPost(apiUri, params));

        // check returned JSON
        if (responseJson.has("id") == false) {
            NetworkException ne = new NetworkException("Invalid response.");
            ne.setResponseJson(responseJson);
            throw ne;
        }

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        // fetch and return the comment object that just has been created
        return getPost(responseJson.getString("id"), localeParam);
    }

    /**
     * Create an event.
     * 
     * @param ownerIdentifier
     * @param name
     * @param description
     * @param startTime
     * @param endTime
     * @param locale
     * @return FacebookEvent
     * @throws Exception
     */
    private FacebookEvent createEvent(String ownerIdentifier, String name, String description, String startTime,
            String endTime, String locationName, String placeId, FacebookEvent.Privacy privacy, Locale locale)
            throws Exception {

        // throw an exception if owner identifier is invalid
        if (StringHelper.isEmpty(ownerIdentifier) == true) {
            throw new SystemException("Invalid owner identifier.");
        }

        // check name
        if (StringHelper.isEmpty(name) == true) {
            throw new SystemException("Invalid event name.");
        }

        // check description
        if (StringHelper.isEmpty(description) == true) {
            throw new SystemException("Invalid event description.");
        }

        // check start time
        if (StringHelper.isEmpty(startTime) == true) {
            throw new SystemException("Invalid event start time.");
        }

        String apiUri = "/" + ownerIdentifier + "/events";

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("name", name), new BasicNameValuePair("description", description),
                new BasicNameValuePair("start_time", startTime), new BasicNameValuePair("end_time", endTime));

        // set end time if needed
        if (StringHelper.isEmpty(endTime) == false) {
            params.add(new BasicNameValuePair("end_time", endTime));
        }

        // set location name if needed
        if (StringHelper.isEmpty(locationName) == false) {
            params.add(new BasicNameValuePair("location", locationName));
        }

        // set palce ID if needed
        if (StringHelper.isEmpty(placeId) == false) {
            params.add(new BasicNameValuePair("location_id", placeId));
        }

        // set privacy if needed
        if (privacy != null) {
            params.add(new BasicNameValuePair("privacy_type", privacy.getPrivacyString()));
        }

        // execute post
        JSONObject responseJson = getResponseJson(getHttpPost(apiUri, params));

        // check returned JSON
        if (responseJson.has("id") == false) {
            NetworkException ne = new NetworkException("Invalid response.");
            ne.setResponseJson(responseJson);
            throw ne;
        }

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        // reset URI and parameters to fetch the event object that just has been created
        apiUri = "/" + responseJson.getString("id");
        params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        return populateEventBean(localeParam, getResponseJson(getHttpGet(apiUri, params)));
    }

    /**
     * Set RSVP to an Facebook event ojbect.
     * 
     * @param eventId
     * @param rsvp
     * @return Boolean
     * @throws Exception
     */
    private Boolean setRsvp(String eventId, String rsvp) throws Exception {

        // throw an exception if event ID is invalid
        if (StringHelper.isEmpty(eventId) == true) {
            throw new SystemException("Invalid event ID.");
        }

        // throw an exception if RSVP is invalid
        if (StringHelper.isEmpty(rsvp) == true) {
            throw new SystemException("Invalid RSVP.");
        }

        String apiUri = "/" + eventId + "/" + rsvp;

        return Boolean.valueOf(getResponseString(
                getHttpPost(apiUri, new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()))));
    }

    /**
     * Like a Facebook object such as status, link, comment, video or photo.
     * 
     * @param objectId
     * @return Boolean
     * @throws Exception
     */
    private Boolean like(String objectId) throws Exception {

        // throw an exception if object ID is invalid
        if (StringHelper.isEmpty(objectId) == true) {
            throw new SystemException("Invalid object ID.");
        }

        String apiUri = "/" + objectId + "/likes";

        return Boolean.valueOf(getResponseString(
                getHttpPost(apiUri, new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()))));
    }

    /**
     * Unike a Facebook object such as status, link, comment, video or photo.
     * 
     * @param objectId
     * @return JSONObject
     * @throws Exception
     */
    private Boolean unlike(String objectId) throws Exception {

        // throw an exception if object ID is invalid
        if (StringHelper.isEmpty(objectId) == true) {
            throw new SystemException("Invalid object ID.");
        }

        String apiUri = "/" + objectId + "/likes" + "?access_token=" + mFacebookContext.getAccessToken();

        return Boolean.valueOf(getResponseString(getHttpDelete(apiUri)));
    }

    /**
     * Delete a Facebook object corresponding to given Facebook object ID.
     * 
     * @param objectId
     * @return Boolean
     * @throws Exception
     */
    private Boolean delete(String objectId) throws Exception {

        // throw an exception if object ID is invalid
        if (StringHelper.isEmpty(objectId) == true) {
            throw new SystemException("Invalid object ID.");
        }

        String apiUri = "/" + objectId + "?access_token=" + mFacebookContext.getAccessToken();

        return Boolean.valueOf(getResponseString(getHttpDelete(apiUri)));
    }

    /**
     * Return a raw JSON object representing a Facebook object.
     * 
     * @param facebookObjectId
     * @param locale
     * @return JSONObject
     * @throws Exception
     */
    private JSONObject getFacebookObject(String facebookObjectId, Locale locale) throws Exception {

        // throw an exception if Facebook object ID is invalid
        if (StringHelper.isEmpty(facebookObjectId) == true) {
            throw new SystemException("Invalid Facebook object ID.");
        }

        String apiUri = "/" + facebookObjectId;

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        return getResponseJson(getHttpGet(apiUri, params));
    }

    /**
     * Get permissions granted to the owner of the current access token.
     * 
     * @return JSONObject
     * @throws Exception
     */
    private JSONObject getGrantedPermissions() throws Exception {

        String apiUri = "/me/permissions";

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("limit", "100"));

        return getResponseJson(getHttpGet(apiUri, params));
    }

    /**
     * Get page accounts attached to the owner of the current access token.
     * 
     * @param locale
     * @return JSONObject
     * @throws Exception
     */
    private JSONObject getPageAccounts(Locale locale) throws Exception {

        String apiUri = "/me/accounts";

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        return getResponseJson(getHttpGet(apiUri, params));
    }

    /**
     * Get groups.
     * 
     * @param userIdentifier
     * @param locale
     * @return JSONObject
     * @throws Exception
     */
    private JSONObject getGroups(String userIdentifier, int limit, int offset, Locale locale) throws Exception {

        // throw an exception if user identifier is invalid
        if (StringHelper.isEmpty(userIdentifier) == true) {
            throw new SystemException("Invalid user identifier.");
        }

        String apiUri = "/" + userIdentifier + "/groups";

        // set system locale if none given
        Locale localeParam = locale;
        if (localeParam == null)
            localeParam = Util.getSystemLocale();

        List<NameValuePair> params = ParameterHelper.addAllParams(
                new BasicNameValuePair("access_token", mFacebookContext.getAccessToken()),
                new BasicNameValuePair("locale", localeParam.toString()));

        // set page parameters
        setPageOffsetParams(params, limit, offset);

        return getResponseJson(getHttpGet(apiUri, params));
    }

    /* END OF ****************** API implementations ************************/

    /**
     * Set limit and offset parameters for Facebook profile list.
     * 
     * @param params
     * @param limit
     * @param offset
     * @return params
     */
    private List<NameValuePair> setPageOffsetParams(List<NameValuePair> params, int limit, int offset) {

        // if limit is given
        if (limit > UNSPECIFIED_LIMIT) {
            params.add(new BasicNameValuePair("limit", "" + limit));
        }

        // if offset is given
        if (offset > UNSPECIFIED_OFFSET) {
            params.add(new BasicNameValuePair("offset", "" + offset));
        }

        return params;
    }

    /**
     * Set date format for Facebook data.
     * 
     * @param params
     * @param isUnixTime true if you want all date data in UNIX-time-formatted
     * @return params
     */
    private List<NameValuePair> setDateFormatParam(List<NameValuePair> params, boolean isUnixTime) {

        // add date_format parameter if needed
        if (isUnixTime == true) {
            params.add(new BasicNameValuePair("date_format", "U"));
        }

        return params;
    }

    /**
     * Set since, until, limit and date_format parameters for Facebook feed pagination.
     * 
     * @param params
     * @param since
     * @param until
     * @param limit
     * @param isUnixTime
     * @return params
     */
    private List<NameValuePair> setTimeBasedPageParams(List<NameValuePair> params, long since, long until,
            int limit, boolean isUnixTime) {

        // if since is given
        if (since > UNSPECIFIED_UNIX_TIME) {
            params.add(new BasicNameValuePair("since", "" + since));
        }

        // if until is given
        if (until > UNSPECIFIED_UNIX_TIME) {
            params.add(new BasicNameValuePair("until", "" + until));
        }

        // if limit is given
        if (limit > UNSPECIFIED_LIMIT) {
            params.add(new BasicNameValuePair("limit", "" + limit));
        }

        // lastly set date_format paramter
        return setDateFormatParam(params, isUnixTime);
    }

    /**
     * Set limit, after and before parameters for Facebook comment data pagination.
     * 
     * @param params
     * @param limit
     * @param after
     * @param before
     * @return params
     */
    private List<NameValuePair> setPageCursorParams(List<NameValuePair> params, int limit, String after,
            String before) {

        // if limit is given
        if (limit > UNSPECIFIED_LIMIT) {
            params.add(new BasicNameValuePair("limit", "" + limit));
        }

        // if after is given
        if (StringHelper.isEmpty(after) == false) {
            params.add(new BasicNameValuePair("after", after));
        }

        // if before is given
        if (StringHelper.isEmpty(before) == false) {
            params.add(new BasicNameValuePair("before", before));

        }

        return params;
    }

    /**
     * Get fields parameter for Facebook profile data fields.
     * 
     * @param additionalFields
     * @return fields CSV
     */
    private String getProfileFields(String[] additionalFields) {

        String[] fieldsParams = DEFAULT_PROFILE_FIELDS_;

        if (additionalFields != null && additionalFields.length > 0) {
            fieldsParams = Util.concatArrays(fieldsParams, additionalFields);
        }

        return TextUtils.join(",", fieldsParams);
    }

    /**
     * Get FacebookProfile list from JSON array.
     * 
     * @param locale
     * @param responseJson
     * @return list of FacebookProfile objects
     * @throws Exception
     */
    private List<FacebookProfile> populateProfileList(Locale locale, JSONObject responseJson) throws Exception {
        return populateProfileList((String[]) null, locale, responseJson);
    }

    /**
     * Get FacebookProfile list from JSON array.
     * 
     * @param additionalFields
     * @param locale
     * @param responseJson
     * @return list of FacebookProfile objects
     * @throws Exception
     */
    private List<FacebookProfile> populateProfileList(String[] additionalFields, Locale locale,
            JSONObject responseJson) throws Exception {
        return populateProfileList(additionalFields, false, locale, responseJson);
    }

    /**
     * Get FacebookProfile list from JSON array.
     * 
     * @param additionalFields
     * @param getOnlyUnread
     * @param locale
     * @param responseJson
     * @return list of FacebookProfile objects
     * @throws Exception
     */
    private List<FacebookProfile> populateProfileList(String[] additionalFields, boolean getOnlyUnread,
            Locale locale, JSONObject responseJson) throws Exception {

        JSONArray profileJsonArray = responseJson.optJSONArray("data");

        // profile list must not be empty
        if (profileJsonArray == null || profileJsonArray.length() == 0) {
            throw new NetworkException("Profile list is empty.", responseJson.toString());
        }

        // extract cursor data if exsists
        String after = null;
        String before = null;
        if (responseJson.has("paging") == true && responseJson.getJSONObject("paging").has("cursors") == true) {
            JSONObject cursorJson = responseJson.getJSONObject("paging").getJSONObject("cursors");
            after = cursorJson.optString("after");
            before = cursorJson.optString("before");
        }

        // iterate profile list and populate FacebookProfile beans
        List<FacebookProfile> profileList = new ArrayList<FacebookProfile>();
        for (int i = 0; i < profileJsonArray.length(); i++) {
            JSONObject profileJson = profileJsonArray.getJSONObject(i);

            // if this list is friend request list
            if (profileJson.has("to") == true && profileJson.has("from") == true) {

                // skip this profile this list is only to fetch unread friend requests
                if (getOnlyUnread == true && profileJson.optBoolean("unread") == false) {
                    continue;
                }

                // extract the date this friend request made
                String friendRequestedDate = profileJson.optString("created_time");

                // extract a profile JSON from this friend requst JSON
                profileJson = profileJson.getJSONObject("from");

                // inject request date to profile JSON
                profileJson.put(FRIEND_REQUEST_DATE, friendRequestedDate);
            }

            // add profile to list
            profileList.add(populateProfileBean(additionalFields, locale, profileJson, after, before));
        }

        // throw an exception in case friend request list has no entry
        if (responseJson.has("summary") == true && profileList.isEmpty() == true) {
            throw new NetworkException("Friend request list is empty.", responseJson.toString());
        }

        return profileList;
    }

    /**
     * Set Facebook profile data to a FacebookProfile bean.
     * 
     * @param additionalFields
     * @param locale
     * @param profileJson
     * @return a FacebookProfile bean
     * @throws Exception
     */
    private FacebookProfile populateProfileBean(String[] additionalFields, Locale locale, JSONObject profileJson)
            throws Exception {
        return populateProfileBean(additionalFields, locale, profileJson, null, null);
    }

    /**
     * Set Facebook profile data to a FacebookProfile bean.
     * 
     * @param additionalFields
     * @param locale
     * @param profileJson
     * @param after
     * @param before
     * @return a FacebookProfile bean
     * @throws Exception
     */
    private FacebookProfile populateProfileBean(String[] additionalFields, Locale locale, JSONObject profileJson,
            String after, String before) throws Exception {

        // throw an exception if JSON is invalid
        if (profileJson == null || profileJson.has("id") == false) {
            if (profileJson == null) {
                throw new NetworkException("Profile JSON is null and cannot populate a Profile bean.");
            } else {
                throw new NetworkException("Invalid profile JSON.", profileJson.toString());
            }
        }

        // set profile data
        FacebookProfile facebookProfile = new FacebookProfile();
        facebookProfile.setLocale(locale);
        facebookProfile.setSnsUid(SocialNetwork.FACEBOOK);
        facebookProfile.setProfileJson(profileJson);
        facebookProfile.setUid(profileJson.optString("id"));
        facebookProfile.setUserName(profileJson.optString("username"));
        facebookProfile.setFullName(profileJson.optString("name"));
        facebookProfile.setProfilePageUrl(profileJson.optString("link"));
        facebookProfile.setGender(profileJson.optString("gender"));

        // profile photo
        if (profileJson.has("picture") == true && profileJson.getJSONObject("picture").has("data") == true) {

            // small profile photo
            String smallPhotoUrl = profileJson.getJSONObject("picture").getJSONObject("data").optString("url");
            facebookProfile.setSmallProfilePhotoUrl(smallPhotoUrl);

            // large profile photo for default dummy value 
            // (note that this URL can go wrong anytime)
            facebookProfile.setLargeProfilePhotoUrl(smallPhotoUrl.replaceAll("_q", "_n"));
        }

        // set additional fields for debug use
        facebookProfile.setAdditionalFields(additionalFields);

        // set the current permission settings for debug use
        facebookProfile.setGrantedPermissionsAsCsv(mFacebookContext.getPermissionScopeAsCsv());

        // set cursors
        facebookProfile.setAfter(after);
        facebookProfile.setBefore(before);

        // set friend requested date if this profile is a friend requester
        if (profileJson.has(FRIEND_REQUEST_DATE) == true) {
            facebookProfile.setFriendRequestedDate(profileJson.getString(FRIEND_REQUEST_DATE));
        }

        // set RSVP status if this profile is an event participant
        if (profileJson.has(RSVP_STATUS) == true) {
            facebookProfile.setRsvpStatus(profileJson.getString(RSVP_STATUS));
        }

        return facebookProfile;
    }

    /**
     * Get FacebookPost list from JSON array.
     * 
     * @param locale
     * @param responseJson
     * @return list of FacebookPost objects
     * @throws Exception
     */
    private List<FacebookPost> populatePostList(Locale locale, JSONObject responseJson) throws Exception {
        return populatePostList(null, locale, responseJson);
    }

    /**
     * Get FacebookPost list from JSON array.
     * 
     * @param parentPostId
     * @param locale
     * @param responseJson
     * @return list of FacebookPost objects
     * @throws Exception
     */
    private List<FacebookPost> populatePostList(String parentPostId, Locale locale, JSONObject responseJson)
            throws Exception {

        JSONArray postJsonArray = responseJson.optJSONArray("data");

        // post list must not be empty
        if (postJsonArray == null || postJsonArray.length() == 0) {
            throw new NetworkException("Post list is empty.", responseJson.toString());
        }

        // extract cursor data if exsists
        String after = null;
        String before = null;
        if (responseJson.has("paging") == true && responseJson.getJSONObject("paging").has("cursors") == true) {
            JSONObject cursorJson = responseJson.getJSONObject("paging").getJSONObject("cursors");
            after = cursorJson.optString("after");
            before = cursorJson.optString("before");
        }

        return populatePostList(locale, postJsonArray, parentPostId, after, before);
    }

    /**
     * Get FacebookPost list from JSON array.
     * 
     * @param locale
     * @param postJsonArray
     * @param parentPostId
     * @param after
     * @param before
     * @return list of FacebookPost objects
     * @throws Exception
     */
    private List<FacebookPost> populatePostList(Locale locale, JSONArray postJsonArray, String parentPostId,
            String after, String before) throws Exception {

        // iterate post list and populate FacebookPost beans
        List<FacebookPost> postList = new ArrayList<FacebookPost>();
        for (int i = 0; i < postJsonArray.length(); i++) {
            postList.add(populatePostBean(locale, postJsonArray.getJSONObject(i), parentPostId, after, before));
        }

        return postList;
    }

    /**
     * Set Facebook post data to a FacebookPost bean.
     * 
     * @param locale
     * @param postJson
     * @return a FacebookPost bean
     * @throws Exception
     */
    private FacebookPost populatePostBean(Locale locale, JSONObject postJson) throws Exception {
        return populatePostBean(locale, postJson, null, null, null);
    }

    /**
     * Set Facebook post data to a FacebookPost bean.
     * 
     * @param locale
     * @param postJson
     * @param parentPostId
     * @param after
     * @param before
     * @return a FacebookPost bean
     * @throws Exception
     */
    private FacebookPost populatePostBean(Locale locale, JSONObject postJson, String parentPostId, String after,
            String before) throws Exception {

        // throw an exception if JSON is invalid
        if (postJson == null || postJson.has("id") == false) {
            if (postJson == null) {
                throw new NetworkException("Post JSON is null and cannot populate a Post bean.");
            } else {
                throw new NetworkException("Invalid post JSON.", postJson.toString());
            }
        }

        // set post data
        FacebookPost facebookPost = new FacebookPost();
        facebookPost.setLocale(locale);
        facebookPost.setSnsUid(SocialNetwork.FACEBOOK);
        facebookPost.setPostJson(postJson);
        facebookPost.setPostId(postJson.optString("id"));
        facebookPost.setPosterUid(postJson.getJSONObject("from").optString("id"));
        facebookPost.setPosterFullName(postJson.getJSONObject("from").optString("name"));
        facebookPost.setTextContent(postJson.optString("message"));
        facebookPost.setCreatedDate(postJson.optString("created_time"));

        facebookPost.setThumbnailPhotoUrl(postJson.optString("picture"));
        facebookPost.setLinkUrl(postJson.optString("link"));
        facebookPost.setLinkName(postJson.optString("name"));
        facebookPost.setLinkCaption(postJson.optString("caption"));
        facebookPost.setLinkDescription(postJson.optString("description"));
        facebookPost.setObjectId(postJson.optString("object_id"));
        facebookPost.setSystemMessage(postJson.optString("story"));
        facebookPost.setIconUrl(postJson.optString("icon"));
        facebookPost.setUpdatedDate(postJson.optString("updated_time"));

        // set post type
        facebookPost.setType(postJson.optString("type"));

        // set post privacy
        if (facebookPost.getPrivacyJson() != null) {
            facebookPost.setPrivacy(facebookPost.getPrivacyJson().optString("value"));
        }

        // set like count
        if (postJson.has("likes") == true) {
            facebookPost.setLikeCount(postJson.getJSONObject("likes").optInt("count"));
        }

        // if the current post is a comment
        else if (postJson.has("like_count") == true) {
            facebookPost.setType(FacebookPost.Type.COMMENT.getTypeString());
            facebookPost.setParentPostId(parentPostId);
            facebookPost.setLikeCount(postJson.getInt("like_count"));
        }

        // if current post is status (retrieved from '[user_id]/statuses' endpoint)
        if (postJson.has("created_time") == false && postJson.has("updated_time") == true
                && facebookPost.getType() == null) {

            // convert status ID to post ID (post Id = [user UID]_[status ID])
            if (facebookPost.getPostId().indexOf("_") == -1) {
                facebookPost.setPostId(facebookPost.getPosterUid() + "_" + facebookPost.getPostId());
            }

            // set type manually
            if (facebookPost.getType() == null) {
                facebookPost.setType(FacebookPost.Type.STATUS.getTypeString());
            }

            // set created time manually since status object has no created_time field
            facebookPost.setCreatedDate(postJson.optString("updated_time"));
        }

        // set share count
        if (postJson.has("shares") == true) {
            facebookPost.setShareCount(postJson.getJSONObject("shares").optInt("count"));
        }

        // set place info
        if (facebookPost.getPlaceJson() != null) {
            facebookPost.setPlaceId(facebookPost.getPlaceJson().optString("id"));
            facebookPost.setPlaceName(facebookPost.getPlaceJson().optString("name"));
            if (facebookPost.getPlaceJson().has("location") == true) {
                facebookPost.setPlaceLatitude(Float
                        .parseFloat(facebookPost.getPlaceJson().getJSONObject("location").optString("latitude")));
                facebookPost.setPlaceLongitude(Float
                        .parseFloat(facebookPost.getPlaceJson().getJSONObject("location").optString("longitude")));
            }
        }

        // set comment list if exists
        if (facebookPost.getCommentJson() != null && facebookPost.getCommentJson().has("data") == true) {
            List<FacebookPost> commentList = populatePostList(facebookPost.getPostId(), locale,
                    facebookPost.getCommentJson());
            facebookPost.setCommentList(commentList);
        }

        // set cursors
        facebookPost.setAfter(after);
        facebookPost.setBefore(before);

        return facebookPost;
    }

    /**
     * Get FacebookAlbum list from JSON array.
     * 
     * @param locale
     * @param responseJson
     * @return list of FacebookAlbum objects
     * @throws Exception
     */
    private List<FacebookAlbum> populateAlbumList(Locale locale, JSONObject responseJson) throws Exception {

        JSONArray albumJsonArray = responseJson.optJSONArray("data");

        // album list must not be empty
        if (albumJsonArray == null || albumJsonArray.length() == 0) {
            throw new NetworkException("Album list is empty.", responseJson.toString());
        }

        // extract cursor data if exsists
        String after = null;
        String before = null;
        if (responseJson.has("paging") == true && responseJson.getJSONObject("paging").has("cursors") == true) {
            JSONObject cursorJson = responseJson.getJSONObject("paging").getJSONObject("cursors");
            after = cursorJson.optString("after");
            before = cursorJson.optString("before");
        }

        // iterate album list and populate FacebookAlbum beans
        List<FacebookAlbum> albumList = new ArrayList<FacebookAlbum>();
        for (int i = 0; i < albumJsonArray.length(); i++) {
            albumList.add(populateAlbumBean(locale, albumJsonArray.getJSONObject(i), after, before));
        }

        return albumList;
    }

    /**
     * Set Facebook album data to a FacebookAlbum bean.
     * 
     * @param locale
     * @param albumJson
     * @param after
     * @param before
     * @return a FacebookAlbum bean
     * @throws Exception
     */
    private FacebookAlbum populateAlbumBean(Locale locale, JSONObject albumJson, String after, String before)
            throws Exception {

        // throw an exception if JSON is invalid
        if (albumJson == null || albumJson.has("id") == false) {
            if (albumJson == null) {
                throw new NetworkException("Album JSON is null and cannot populate a album bean.");
            } else {
                throw new NetworkException("Invalid album JSON.", albumJson.toString());
            }
        }

        // set album data
        FacebookAlbum facebookAlbum = new FacebookAlbum();
        facebookAlbum.setLocale(locale);
        facebookAlbum.setAlbumJson(albumJson);
        facebookAlbum.setId(albumJson.getString("id"));
        facebookAlbum.setOwnerId(albumJson.getJSONObject("from").optString("id"));
        facebookAlbum.setOwnerFullName(albumJson.getJSONObject("from").optString("name"));
        facebookAlbum.setName(albumJson.optString("name"));
        facebookAlbum.setLink(albumJson.optString("link"));
        facebookAlbum.setCoverPhotoId(albumJson.optString("cover_photo"));
        facebookAlbum.setPrivacy(albumJson.optString("privacy"));
        facebookAlbum.setCount(albumJson.optInt("count"));
        facebookAlbum.setType(albumJson.optString("type"));
        facebookAlbum.setCreatedDate(albumJson.optString("created_time"));
        facebookAlbum.setUpdatedDate(albumJson.optString("updated_time"));
        facebookAlbum.setCanUpload(albumJson.optBoolean("can_upload"));

        // set cursors
        facebookAlbum.setAfter(after);
        facebookAlbum.setBefore(before);

        return facebookAlbum;
    }

    /**
     * Get FacebookPhoto list from JSON array.
     * 
     * @param locale
     * @param responseJson
     * @return list of FacebookPhoto objects
     * @throws Exception
     */
    private List<FacebookPhoto> populatePhotoList(Locale locale, JSONObject responseJson) throws Exception {

        JSONArray photoJsonArray = responseJson.optJSONArray("data");

        // photo list must not be empty
        if (photoJsonArray == null || photoJsonArray.length() == 0) {
            throw new NetworkException("Photo list is empty.", responseJson.toString());
        }

        // extract cursor data if exsists
        String after = null;
        String before = null;
        if (responseJson.has("paging") == true && responseJson.getJSONObject("paging").has("cursors") == true) {
            JSONObject cursorJson = responseJson.getJSONObject("paging").getJSONObject("cursors");
            after = cursorJson.optString("after");
            before = cursorJson.optString("before");
        }

        // iterate photo list and populate FacebookPhoto beans
        List<FacebookPhoto> photoList = new ArrayList<FacebookPhoto>();
        for (int i = 0; i < photoJsonArray.length(); i++) {
            photoList.add(populatePhotoBean(locale, photoJsonArray.getJSONObject(i), after, before));
        }

        return photoList;
    }

    /**
     * Set Facebook photo data to a FacebookPhoto bean.
     * 
     * @param locale
     * @param photoJson
     * @return a FacebookPhoto bean
     * @throws Exception
     */
    private FacebookPhoto populatePhotoBean(Locale locale, JSONObject photoJson) throws Exception {
        return populatePhotoBean(locale, photoJson, null, null);
    }

    /**
     * Set Facebook photo data to a FacebookPhoto bean.
     * 
     * @param locale
     * @param photoJson
     * @param after
     * @param before
     * @return a FacebookPhoto bean
     * @throws Exception
     */
    private FacebookPhoto populatePhotoBean(Locale locale, JSONObject photoJson, String after, String before)
            throws Exception {

        // throw an exception if JSON is invalid
        if (photoJson == null || photoJson.has("id") == false) {
            if (photoJson == null) {
                throw new NetworkException("Photo JSON is null and cannot populate a photo bean.");
            } else {
                throw new NetworkException("Invalid photo JSON.", photoJson.toString());
            }
        }

        // set photo data
        FacebookPhoto facebookPhoto = new FacebookPhoto();
        facebookPhoto.setLocale(locale);
        facebookPhoto.setPhotoJson(photoJson);
        facebookPhoto.setId(photoJson.getString("id"));
        facebookPhoto.setOwnerId(photoJson.getJSONObject("from").optString("id"));
        facebookPhoto.setOwnerFullName(photoJson.getJSONObject("from").optString("name"));
        facebookPhoto.setName(photoJson.optString("name"));
        facebookPhoto.setThumbnailUrl(photoJson.optString("picture"));
        facebookPhoto.setOriginalPhotoUrl(photoJson.optString("source"));
        facebookPhoto.setOriginalPhotoHeight(photoJson.optString("height"));
        facebookPhoto.setOriginalPhotoWidth(photoJson.optString("width"));
        facebookPhoto.setLink(photoJson.optString("link"));
        facebookPhoto.setIconUrl(photoJson.optString("icon"));
        facebookPhoto.setCreatedDate(photoJson.optString("created_time"));
        facebookPhoto.setUpdatedDate(photoJson.optString("updated_time"));

        // set like list if exists
        if (facebookPhoto.getLikeJson() != null && facebookPhoto.getLikeJson().has("data") == true) {
            List<FacebookProfile> likeList = populateProfileList(locale, facebookPhoto.getLikeJson());
            facebookPhoto.setLikeList(likeList);
        }

        // set comment list if exists
        if (facebookPhoto.getCommentJson() != null && facebookPhoto.getCommentJson().has("data") == true) {
            List<FacebookPost> commentList = populatePostList(facebookPhoto.getId(), locale,
                    facebookPhoto.getCommentJson());
            facebookPhoto.setCommentList(commentList);
        }

        // set cursors
        facebookPhoto.setAfter(after);
        facebookPhoto.setBefore(before);

        return facebookPhoto;
    }

    /**
     * Get FacebookNote list from JSON array.
     * 
     * @param locale
     * @param responseJson
     * @return list of FacebookNote objects
     * @throws Exception
     */
    private List<FacebookNote> populateNoteList(Locale locale, JSONObject responseJson) throws Exception {

        JSONArray noteJsonArray = responseJson.optJSONArray("data");

        // note list must not be empty
        if (noteJsonArray == null || noteJsonArray.length() == 0) {
            throw new NetworkException("Note list is empty.", responseJson.toString());
        }

        // extract cursor data if exsists
        String after = null;
        String before = null;
        if (responseJson.has("paging") == true && responseJson.getJSONObject("paging").has("cursors") == true) {
            JSONObject cursorJson = responseJson.getJSONObject("paging").getJSONObject("cursors");
            after = cursorJson.optString("after");
            before = cursorJson.optString("before");
        }

        // iterate note list and populate FacebookNote beans
        List<FacebookNote> noteList = new ArrayList<FacebookNote>();
        for (int i = 0; i < noteJsonArray.length(); i++) {
            noteList.add(populateNoteBean(locale, noteJsonArray.getJSONObject(i), after, before));
        }

        return noteList;
    }

    /**
     * Set Facebook note data to a FacebookLikableObject bean.
     * 
     * @param locale
     * @param noteJson
     * @param after
     * @param before
     * @return a FacebookNote bean
     * @throws Exception
     */
    private FacebookNote populateNoteBean(Locale locale, JSONObject noteJson, String after, String before)
            throws Exception {

        // throw an exception if JSON is invalid
        if (noteJson == null || noteJson.has("id") == false) {
            if (noteJson == null) {
                throw new NetworkException("Note JSON is null and " + "cannot populate a note bean.");
            } else {
                throw new NetworkException("Invalid note JSON.", noteJson.toString());
            }
        }

        // set note data
        FacebookNote facebookNote = new FacebookNote();
        facebookNote.setLocale(locale);
        facebookNote.setNoteJson(noteJson);
        facebookNote.setId(noteJson.getString("id"));
        facebookNote.setAuthorUid(noteJson.getJSONObject("from").optString("id"));
        facebookNote.setAuthorFullName(noteJson.getJSONObject("from").optString("name"));
        facebookNote.setSubject(noteJson.optString("subject"));
        facebookNote.setHtmlContent(noteJson.optString("message"));
        facebookNote.setIconUrl(noteJson.optString("icon"));
        facebookNote.setCreatedDate(noteJson.optString("created_time"));
        facebookNote.setUpdatedDate(noteJson.optString("updated_time"));

        // set cursors
        facebookNote.setAfter(after);
        facebookNote.setBefore(before);

        return facebookNote;
    }

    /**
     * Get FacebookLikableObject list from JSON array.
     * 
     * @param locale
     * @param responseJson
     * @return list of FacebookLikableObject objects
     * @throws Exception
     */
    private List<FacebookLikableObject> populateLikableList(Locale locale, JSONObject responseJson)
            throws Exception {

        JSONArray likableJsonArray = responseJson.optJSONArray("data");

        // likable object list must not be empty
        if (likableJsonArray == null || likableJsonArray.length() == 0) {
            throw new NetworkException("Likable object list is empty.", responseJson.toString());
        }

        // extract cursor data if exsists
        String after = null;
        String before = null;
        if (responseJson.has("paging") == true && responseJson.getJSONObject("paging").has("cursors") == true) {
            JSONObject cursorJson = responseJson.getJSONObject("paging").getJSONObject("cursors");
            after = cursorJson.optString("after");
            before = cursorJson.optString("before");
        }

        // iterate likable object list and populate FacebookLikableObject beans
        List<FacebookLikableObject> likableList = new ArrayList<FacebookLikableObject>();
        for (int i = 0; i < likableJsonArray.length(); i++) {
            likableList.add(populateLikableBean(locale, likableJsonArray.getJSONObject(i), after, before));
        }

        return likableList;
    }

    /**
     * Set Facebook likable object data to a FacebookLikableObject bean.
     * 
     * @param locale
     * @param objectJson
     * @param after
     * @param before
     * @return a FacebookLikableObject bean
     * @throws Exception
     */
    private FacebookLikableObject populateLikableBean(Locale locale, JSONObject objectJson, String after,
            String before) throws Exception {

        // throw an exception if JSON is invalid
        if (objectJson == null || objectJson.has("id") == false) {
            if (objectJson == null) {
                throw new NetworkException(
                        "Likable object JSON is null and " + "cannot populate a likable object bean.");
            } else {
                throw new NetworkException("Invalid likable object JSON.", objectJson.toString());
            }
        }

        // set likable object data
        FacebookLikableObject facebookLikableObject = new FacebookLikableObject();
        facebookLikableObject.setLocale(locale);
        facebookLikableObject.setLikableJson(objectJson);
        facebookLikableObject.setId(objectJson.getString("id"));
        facebookLikableObject.setCategory(objectJson.optString("category"));
        facebookLikableObject.setName(objectJson.optString("name"));
        facebookLikableObject.setLink(objectJson.optString("link"));
        facebookLikableObject.setDescription(objectJson.optString("description"));

        // set cover photo if exists
        if (objectJson.has("cover") == true) {
            facebookLikableObject.setCoverPhotoId(objectJson.getJSONObject("cover").optString("cover_id"));
            facebookLikableObject.setCoverPhotoUrl(objectJson.getJSONObject("cover").optString("source"));
        }

        facebookLikableObject.setWebSiteUrl(objectJson.optString("website"));
        facebookLikableObject.setTalkingAboutCount(objectJson.optInt("talking_about_count"));
        facebookLikableObject.setCanPost(objectJson.optBoolean("can_post"));
        facebookLikableObject.setCreatedDate(objectJson.optString("created_time"));

        // set cursors
        facebookLikableObject.setAfter(after);
        facebookLikableObject.setBefore(before);

        return facebookLikableObject;
    }

    /**
     * Get FacebookMessageThread list from JSON array.
     * 
     * @param locale
     * @param responseJson
     * @return list of FacebookMessageThread objects
     * @throws Exception
     */
    private List<FacebookMessageThread> populateMessageThreadList(Locale locale, JSONObject responseJson)
            throws Exception {

        JSONArray messageThreadJsonArray = responseJson.optJSONArray("data");

        // message thread list must not be empty
        if (messageThreadJsonArray == null || messageThreadJsonArray.length() == 0) {
            throw new NetworkException("Message thread list is empty.", responseJson.toString());
        }

        // iterate message thread list and populate FacebookMessageThread beans
        List<FacebookMessageThread> messageThreadList = new ArrayList<FacebookMessageThread>();
        for (int i = 0; i < messageThreadJsonArray.length(); i++) {
            messageThreadList.add(populateMessageThreadBean(locale, messageThreadJsonArray.getJSONObject(i)));
        }

        return messageThreadList;
    }

    /**
     * Set Facebook message thread data to a FacebookMessageThread bean.
     * 
     * 
     * @param locale
     * @param messageThreadJson
     * @return a FacebookMessage bean
     * @throws Exception
     */
    private FacebookMessageThread populateMessageThreadBean(Locale locale, JSONObject messageThreadJson)
            throws Exception {

        // throw an exception if JSON is invalid
        if (messageThreadJson == null || messageThreadJson.has("id") == false) {
            if (messageThreadJson == null) {
                throw new NetworkException(
                        "Message thread JSON is null and " + "cannot populate a message thread bean.");
            } else {
                throw new NetworkException("Invalid message thread JSON.", messageThreadJson.toString());
            }
        }

        // set message thread data
        FacebookMessageThread facebookMessageThread = new FacebookMessageThread();
        facebookMessageThread.setLocale(locale);
        facebookMessageThread.setMessageThreadJson(messageThreadJson);
        facebookMessageThread.setId(messageThreadJson.getString("id"));
        facebookMessageThread.setUnread(messageThreadJson.optInt("unread"));
        facebookMessageThread.setUnseen(messageThreadJson.optInt("unseen"));
        facebookMessageThread.setUpdatedDate(messageThreadJson.optString("updated_time"));

        // set conversation participants 
        String toUserUids = null;
        String toUserFullNames = null;
        JSONArray toJsonArray = messageThreadJson.getJSONObject("to").getJSONArray("data");
        for (int i = 0; i < toJsonArray.length(); i++) {
            JSONObject toJson = toJsonArray.getJSONObject(i);
            if (i > 0 && toJson.optString("id") != null) {
                toUserUids += "," + toJson.optString("id");
            } else {
                toUserUids = toJson.optString("id");
            }
            if (i > 0 && toJson.optString("name") != null) {
                toUserFullNames += "," + toJson.optString("name");
            } else {
                toUserFullNames = toJson.optString("name");
            }
        }
        if (toUserUids.length() > 0) {
            facebookMessageThread.setToUserUids(toUserUids.split(","));
        }
        if (toUserFullNames.length() > 0) {
            facebookMessageThread.setToUserFullNames(toUserFullNames.split(","));
        }

        // set message list
        facebookMessageThread
                .setMessageList(populateMessageList(locale, messageThreadJson.getJSONObject("comments")));

        // set last message and its sender information
        List<FacebookMessage> messageList = facebookMessageThread.getMessageList();
        facebookMessageThread.setLastMessage(messageList.get(messageList.size() - 1).getMessage());
        facebookMessageThread.setLastSenderUid(messageList.get(messageList.size() - 1).getSenderUid());
        facebookMessageThread.setLastSenderFullName(messageList.get(messageList.size() - 1).getSenderFullName());

        return facebookMessageThread;
    }

    /**
     * Get FacebookMessage list from JSON array.
     * 
     * @param locale
     * @param responseJson
     * @return list of FacebookMessage objects
     * @throws Exception
     */
    private List<FacebookMessage> populateMessageList(Locale locale, JSONObject responseJson) throws Exception {

        JSONArray messageJsonArray = responseJson.optJSONArray("data");

        // message list must not be empty
        if (messageJsonArray == null || messageJsonArray.length() == 0) {
            throw new NetworkException("Message list is empty.", responseJson.toString());
        }

        // iterate message list and populate FacebookMessage beans
        List<FacebookMessage> messageList = new ArrayList<FacebookMessage>();
        for (int i = 0; i < messageJsonArray.length(); i++) {
            messageList.add(populateMessageBean(locale, messageJsonArray.getJSONObject(i)));
        }

        return messageList;
    }

    /**
     * Set Facebook message data to a FacebookMessage bean.
     * 
     * @param locale
     * @param messageJson
     * @return a FacebookMessage bean
     * @throws Exception
     */
    private FacebookMessage populateMessageBean(Locale locale, JSONObject messageJson) throws Exception {

        // throw an exception if JSON is invalid
        if (messageJson == null || messageJson.has("id") == false) {
            if (messageJson == null) {
                throw new NetworkException("Message JSON is null and " + "cannot populate a message bean.");
            } else {
                throw new NetworkException("Invalid message JSON.", messageJson.toString());
            }
        }

        // set message data
        FacebookMessage facebookMessage = new FacebookMessage();
        facebookMessage.setLocale(locale);
        facebookMessage.setMessageJson(messageJson);
        facebookMessage.setId(messageJson.getString("id"));
        facebookMessage.setSenderUid(messageJson.getJSONObject("from").optString("id"));
        facebookMessage.setSenderFullName(messageJson.getJSONObject("from").optString("name"));
        facebookMessage.setMessage(messageJson.optString("message"));
        facebookMessage.setCreatedDate(messageJson.optString("created_time"));

        return facebookMessage;
    }

    /**
     * Get FacebookEvent list from JSON array.
     * 
     * @param locale
     * @param responseJson
     * @return list of FacebookEvent objects
     * @throws Exception
     */
    private List<FacebookEvent> populateEventList(Locale locale, JSONObject responseJson) throws Exception {

        JSONArray eventJsonArray = responseJson.optJSONArray("data");

        // event list must not be empty
        if (eventJsonArray == null || eventJsonArray.length() == 0) {
            throw new NetworkException("Event list is empty.", responseJson.toString());
        }

        // iterate event list and populate FacebookEvent beans
        List<FacebookEvent> eventList = new ArrayList<FacebookEvent>();
        for (int i = 0; i < eventJsonArray.length(); i++) {
            eventList.add(populateEventBean(locale, eventJsonArray.getJSONObject(i)));
        }

        return eventList;
    }

    /**
     * Set Facebook event data to a FacebookEvent bean.
     * 
     * @param locale
     * @param eventJson
     * @return a FacebookEvent bean
     * @throws Exception
     */
    private FacebookEvent populateEventBean(Locale locale, JSONObject eventJson) throws Exception {

        // throw an exception if JSON is invalid
        if (eventJson == null || eventJson.has("id") == false) {
            if (eventJson == null) {
                throw new NetworkException("Event JSON is null and " + "cannot populate an event bean.");
            } else {
                throw new NetworkException("Invalid event JSON.", eventJson.toString());
            }
        }

        // set event data
        FacebookEvent facebookEvent = new FacebookEvent();
        facebookEvent.setLocale(locale);
        facebookEvent.setEventJson(eventJson);
        facebookEvent.setId(eventJson.getString("id"));
        facebookEvent.setOwnerUid(eventJson.getJSONObject("owner").optString("id"));
        facebookEvent.setOwnerFullName(eventJson.getJSONObject("owner").optString("name"));
        facebookEvent.setName(eventJson.optString("name"));
        facebookEvent.setDescription(eventJson.optString("description"));

        // set cover photo
        if (eventJson.has("cover") == true) {
            facebookEvent.setCoverPhotoId(eventJson.getJSONObject("cover").optString("cover_id"));
            facebookEvent.setCoverPhotoUrl(eventJson.getJSONObject("cover").optString("source"));
        }

        facebookEvent.setStartTime(eventJson.optString("start_time"));
        facebookEvent.setEndTime(eventJson.optString("end_time"));

        // set timezone
        if (eventJson.optString("timezone") != null) {
            facebookEvent.setTimezone(TimeZone.getTimeZone(eventJson.optString("timezone")));
            facebookEvent.setTimezoneString(eventJson.optString("timezone"));
        }

        // set location
        facebookEvent.setLocationName(eventJson.optString("location"));
        if (eventJson.has("venue") == true) {
            facebookEvent.setLocationId(eventJson.getJSONObject("venue").optString("id"));
            String latitude = eventJson.getJSONObject("venue").optString("latitude");
            if (StringHelper.isEmpty(latitude) == false) {
                facebookEvent.setLocationLatitude(Float.parseFloat(latitude));
            }

            String longitude = eventJson.getJSONObject("venue").optString("longitude");
            if (StringHelper.isEmpty(longitude) == false) {
                facebookEvent.setLocationLongitude(Float.parseFloat(longitude));
            }
        }

        facebookEvent.setPrivacy(eventJson.optString("privacy"));
        facebookEvent.setUpdatedDate(eventJson.optString("updated_time"));

        return facebookEvent;
    }

}