com.facebook.widget.GraphObjectAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.widget.GraphObjectAdapter.java

Source

/**
 * Copyright 2010-present Facebook.
 *
 * 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 com.facebook.widget;

import android.content.Context;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.*;
import com.facebook.FacebookException;
import com.facebook.android.R;
import com.facebook.internal.ImageDownloader;
import com.facebook.internal.ImageRequest;
import com.facebook.internal.ImageResponse;
import com.facebook.model.GraphObject;
import org.json.JSONObject;

import java.net.URI;
import java.net.URISyntaxException;
import java.text.Collator;
import java.util.*;

class GraphObjectAdapter<T extends GraphObject> extends BaseAdapter implements SectionIndexer {
    private static final int DISPLAY_SECTIONS_THRESHOLD = 1;
    private static final int HEADER_VIEW_TYPE = 0;
    private static final int GRAPH_OBJECT_VIEW_TYPE = 1;
    private static final int ACTIVITY_CIRCLE_VIEW_TYPE = 2;
    private static final int MAX_PREFETCHED_PICTURES = 20;

    private static final String ID = "id";
    private static final String NAME = "name";
    private static final String PICTURE = "picture";

    private final Map<String, ImageRequest> pendingRequests = new HashMap<String, ImageRequest>();
    private final LayoutInflater inflater;
    private List<String> sectionKeys = new ArrayList<String>();
    private Map<String, ArrayList<T>> graphObjectsBySection = new HashMap<String, ArrayList<T>>();
    private Map<String, T> graphObjectsById = new HashMap<String, T>();
    private boolean displaySections;
    private List<String> sortFields;
    private String groupByField;
    private boolean showPicture;
    private boolean showCheckbox;
    private Filter<T> filter;
    private DataNeededListener dataNeededListener;
    private GraphObjectCursor<T> cursor;
    private Context context;
    private Map<String, ImageResponse> prefetchedPictureCache = new HashMap<String, ImageResponse>();
    private ArrayList<String> prefetchedProfilePictureIds = new ArrayList<String>();
    private OnErrorListener onErrorListener;

    public interface DataNeededListener {
        void onDataNeeded();
    }

    public interface OnErrorListener {
        void onError(GraphObjectAdapter<?> adapter, FacebookException error);
    }

    public static class SectionAndItem<T extends GraphObject> {
        public String sectionKey;
        public T graphObject;

        public enum Type {
            GRAPH_OBJECT, SECTION_HEADER, ACTIVITY_CIRCLE
        }

        public SectionAndItem(String sectionKey, T graphObject) {
            this.sectionKey = sectionKey;
            this.graphObject = graphObject;
        }

        public Type getType() {
            if (sectionKey == null) {
                return Type.ACTIVITY_CIRCLE;
            } else if (graphObject == null) {
                return Type.SECTION_HEADER;
            } else {
                return Type.GRAPH_OBJECT;
            }
        }
    }

    interface Filter<T> {
        boolean includeItem(T graphObject);
    }

    public GraphObjectAdapter(Context context) {
        this.context = context;
        this.inflater = LayoutInflater.from(context);
    }

    public List<String> getSortFields() {
        return sortFields;
    }

    public void setSortFields(List<String> sortFields) {
        this.sortFields = sortFields;
    }

    public String getGroupByField() {
        return groupByField;
    }

    public void setGroupByField(String groupByField) {
        this.groupByField = groupByField;
    }

    public boolean getShowPicture() {
        return showPicture;
    }

    public void setShowPicture(boolean showPicture) {
        this.showPicture = showPicture;
    }

    public boolean getShowCheckbox() {
        return showCheckbox;
    }

    public void setShowCheckbox(boolean showCheckbox) {
        this.showCheckbox = showCheckbox;
    }

    public DataNeededListener getDataNeededListener() {
        return dataNeededListener;
    }

    public void setDataNeededListener(DataNeededListener dataNeededListener) {
        this.dataNeededListener = dataNeededListener;
    }

    public OnErrorListener getOnErrorListener() {
        return onErrorListener;
    }

    public void setOnErrorListener(OnErrorListener onErrorListener) {
        this.onErrorListener = onErrorListener;
    }

    public GraphObjectCursor<T> getCursor() {
        return cursor;
    }

    public boolean changeCursor(GraphObjectCursor<T> cursor) {
        if (this.cursor == cursor) {
            return false;
        }
        if (this.cursor != null) {
            this.cursor.close();
        }
        this.cursor = cursor;

        rebuildAndNotify();
        return true;
    }

    public void rebuildAndNotify() {
        rebuildSections();
        notifyDataSetChanged();
    }

    public void prioritizeViewRange(int firstVisibleItem, int lastVisibleItem, int prefetchBuffer) {
        if ((lastVisibleItem < firstVisibleItem) || (sectionKeys.size() == 0)) {
            return;
        }

        // We want to prioritize requests for items which are visible but do not have pictures
        // loaded yet. We also want to pre-fetch pictures for items which are not yet visible
        // but are within a buffer on either side of the visible items, on the assumption that
        // they will be visible soon. For these latter items, we'll store the images in memory
        // in the hopes we can immediately populate their image view when needed.

        // Prioritize the requests in reverse order since each call to prioritizeRequest will just
        // move it to the front of the queue. And we want the earliest ones in the range to be at
        // the front of the queue, so all else being equal, the list will appear to populate from
        // the top down.
        for (int i = lastVisibleItem; i >= 0; i--) {
            SectionAndItem<T> sectionAndItem = getSectionAndItem(i);
            if (sectionAndItem.graphObject != null) {
                String id = getIdOfGraphObject(sectionAndItem.graphObject);
                ImageRequest request = pendingRequests.get(id);
                if (request != null) {
                    ImageDownloader.prioritizeRequest(request);
                }
            }
        }

        // For items which are not visible, but within the buffer on either side, we want to
        // fetch those items and store them in a small in-memory cache of bitmaps.
        int start = Math.max(0, firstVisibleItem - prefetchBuffer);
        int end = Math.min(lastVisibleItem + prefetchBuffer, getCount() - 1);
        ArrayList<T> graphObjectsToPrefetchPicturesFor = new ArrayList<T>();
        // Add the IDs before and after the visible range.
        for (int i = start; i < firstVisibleItem; ++i) {
            SectionAndItem<T> sectionAndItem = getSectionAndItem(i);
            if (sectionAndItem.graphObject != null) {
                graphObjectsToPrefetchPicturesFor.add(sectionAndItem.graphObject);
            }
        }
        for (int i = lastVisibleItem + 1; i <= end; ++i) {
            SectionAndItem<T> sectionAndItem = getSectionAndItem(i);
            if (sectionAndItem.graphObject != null) {
                graphObjectsToPrefetchPicturesFor.add(sectionAndItem.graphObject);
            }
        }
        for (T graphObject : graphObjectsToPrefetchPicturesFor) {
            URI uri = getPictureUriOfGraphObject(graphObject);
            final String id = getIdOfGraphObject(graphObject);

            // This URL already have been requested for pre-fetching, but we want to act in an LRU manner, so move
            // it to the end of the list regardless.
            boolean alreadyPrefetching = prefetchedProfilePictureIds.remove(id);
            prefetchedProfilePictureIds.add(id);

            // If we've already requested it for pre-fetching, no need to do so again.
            if (!alreadyPrefetching) {
                downloadProfilePicture(id, uri, null);
            }
        }
    }

    protected String getSectionKeyOfGraphObject(T graphObject) {
        String result = null;

        if (groupByField != null) {
            result = (String) graphObject.getProperty(groupByField);
            if (result != null && result.length() > 0) {
                result = result.substring(0, 1).toUpperCase();
            }
        }

        return (result != null) ? result : "";
    }

    protected CharSequence getTitleOfGraphObject(T graphObject) {
        return (String) graphObject.getProperty(NAME);
    }

    protected CharSequence getSubTitleOfGraphObject(T graphObject) {
        return null;
    }

    protected URI getPictureUriOfGraphObject(T graphObject) {
        String uri = null;
        Object o = graphObject.getProperty(PICTURE);
        if (o instanceof String) {
            uri = (String) o;
        } else if (o instanceof JSONObject) {
            ItemPicture itemPicture = GraphObject.Factory.create((JSONObject) o).cast(ItemPicture.class);
            ItemPictureData data = itemPicture.getData();
            if (data != null) {
                uri = data.getUrl();
            }
        }

        if (uri != null) {
            try {
                return new URI(uri);
            } catch (URISyntaxException e) {
            }
        }
        return null;
    }

    protected View getSectionHeaderView(String sectionHeader, View convertView, ViewGroup parent) {
        TextView result = (TextView) convertView;

        if (result == null) {
            result = (TextView) inflater.inflate(R.layout.com_facebook_picker_list_section_header, null);
        }

        result.setText(sectionHeader);

        return result;
    }

    protected View getGraphObjectView(T graphObject, View convertView, ViewGroup parent) {
        View result = convertView;

        if (result == null) {
            result = createGraphObjectView(graphObject);
        }

        populateGraphObjectView(result, graphObject);
        return result;
    }

    private View getActivityCircleView(View convertView, ViewGroup parent) {
        View result = convertView;

        if (result == null) {
            result = inflater.inflate(R.layout.com_facebook_picker_activity_circle_row, null);
        }
        ProgressBar activityCircle = (ProgressBar) result
                .findViewById(R.id.com_facebook_picker_row_activity_circle);
        activityCircle.setVisibility(View.VISIBLE);

        return result;
    }

    protected int getGraphObjectRowLayoutId(T graphObject) {
        return R.layout.com_facebook_picker_list_row;
    }

    protected int getDefaultPicture() {
        return R.drawable.com_facebook_profile_default_icon;
    }

    protected View createGraphObjectView(T graphObject) {
        View result = inflater.inflate(getGraphObjectRowLayoutId(graphObject), null);

        ViewStub checkboxStub = (ViewStub) result.findViewById(R.id.com_facebook_picker_checkbox_stub);
        if (checkboxStub != null) {
            if (!getShowCheckbox()) {
                checkboxStub.setVisibility(View.GONE);
            } else {
                CheckBox checkBox = (CheckBox) checkboxStub.inflate();
                updateCheckboxState(checkBox, false);
            }
        }

        ViewStub profilePicStub = (ViewStub) result.findViewById(R.id.com_facebook_picker_profile_pic_stub);
        if (!getShowPicture()) {
            profilePicStub.setVisibility(View.GONE);
        } else {
            ImageView imageView = (ImageView) profilePicStub.inflate();
            imageView.setVisibility(View.VISIBLE);
        }

        return result;
    }

    protected void populateGraphObjectView(View view, T graphObject) {
        String id = getIdOfGraphObject(graphObject);
        view.setTag(id);

        CharSequence title = getTitleOfGraphObject(graphObject);
        TextView titleView = (TextView) view.findViewById(R.id.com_facebook_picker_title);
        if (titleView != null) {
            titleView.setText(title, TextView.BufferType.SPANNABLE);
        }

        CharSequence subtitle = getSubTitleOfGraphObject(graphObject);
        TextView subtitleView = (TextView) view.findViewById(R.id.picker_subtitle);
        if (subtitleView != null) {
            if (subtitle != null) {
                subtitleView.setText(subtitle, TextView.BufferType.SPANNABLE);
                subtitleView.setVisibility(View.VISIBLE);
            } else {
                subtitleView.setVisibility(View.GONE);
            }
        }

        if (getShowCheckbox()) {
            CheckBox checkBox = (CheckBox) view.findViewById(R.id.com_facebook_picker_checkbox);
            updateCheckboxState(checkBox, isGraphObjectSelected(id));
        }

        if (getShowPicture()) {
            URI pictureURI = getPictureUriOfGraphObject(graphObject);

            if (pictureURI != null) {
                ImageView profilePic = (ImageView) view.findViewById(R.id.com_facebook_picker_image);

                // See if we have already pre-fetched this; if not, download it.
                if (prefetchedPictureCache.containsKey(id)) {
                    ImageResponse response = prefetchedPictureCache.get(id);
                    profilePic.setImageBitmap(response.getBitmap());
                    profilePic.setTag(response.getRequest().getImageUri());
                } else {
                    downloadProfilePicture(id, pictureURI, profilePic);
                }
            }
        }
    }

    /**
     * @throws FacebookException if the GraphObject doesn't have an ID.
     */
    String getIdOfGraphObject(T graphObject) {
        if (graphObject.asMap().containsKey(ID)) {
            Object obj = graphObject.getProperty(ID);
            if (obj instanceof String) {
                return (String) obj;
            }
        }
        throw new FacebookException("Received an object without an ID.");
    }

    boolean filterIncludesItem(T graphObject) {
        return filter == null || filter.includeItem(graphObject);
    }

    Filter<T> getFilter() {
        return filter;
    }

    void setFilter(Filter<T> filter) {
        this.filter = filter;
    }

    boolean isGraphObjectSelected(String graphObjectId) {
        return false;
    }

    void updateCheckboxState(CheckBox checkBox, boolean graphObjectSelected) {
        // Default is no-op
    }

    String getPictureFieldSpecifier() {
        // How big is our image?
        View view = createGraphObjectView(null);
        ImageView picture = (ImageView) view.findViewById(R.id.com_facebook_picker_image);
        if (picture == null) {
            return null;
        }

        // Note: these dimensions are in pixels, not dips
        ViewGroup.LayoutParams layoutParams = picture.getLayoutParams();
        return String.format(Locale.US, "picture.height(%d).width(%d)", layoutParams.height, layoutParams.width);
    }

    private boolean shouldShowActivityCircleCell() {
        // We show the "more data" activity circle cell if we have a listener to request more data,
        // we are expecting more data, and we have some data already (i.e., not on a fresh query).
        return (cursor != null) && cursor.areMoreObjectsAvailable() && (dataNeededListener != null) && !isEmpty();
    }

    private void rebuildSections() {
        sectionKeys = new ArrayList<String>();
        graphObjectsBySection = new HashMap<String, ArrayList<T>>();
        graphObjectsById = new HashMap<String, T>();
        displaySections = false;

        if (cursor == null || cursor.getCount() == 0) {
            return;
        }

        int objectsAdded = 0;
        cursor.moveToFirst();
        do {
            T graphObject = cursor.getGraphObject();

            if (!filterIncludesItem(graphObject)) {
                continue;
            }

            objectsAdded++;

            String sectionKeyOfItem = getSectionKeyOfGraphObject(graphObject);
            if (!graphObjectsBySection.containsKey(sectionKeyOfItem)) {
                sectionKeys.add(sectionKeyOfItem);
                graphObjectsBySection.put(sectionKeyOfItem, new ArrayList<T>());
            }
            List<T> section = graphObjectsBySection.get(sectionKeyOfItem);
            section.add(graphObject);

            graphObjectsById.put(getIdOfGraphObject(graphObject), graphObject);
        } while (cursor.moveToNext());

        if (sortFields != null) {
            final Collator collator = Collator.getInstance();
            for (List<T> section : graphObjectsBySection.values()) {
                Collections.sort(section, new Comparator<GraphObject>() {
                    @Override
                    public int compare(GraphObject a, GraphObject b) {
                        return compareGraphObjects(a, b, sortFields, collator);
                    }
                });
            }
        }

        Collections.sort(sectionKeys, Collator.getInstance());

        displaySections = sectionKeys.size() > 1 && objectsAdded > DISPLAY_SECTIONS_THRESHOLD;
    }

    SectionAndItem<T> getSectionAndItem(int position) {
        if (sectionKeys.size() == 0) {
            return null;
        }
        String sectionKey = null;
        T graphObject = null;

        if (!displaySections) {
            sectionKey = sectionKeys.get(0);
            List<T> section = graphObjectsBySection.get(sectionKey);
            if (position >= 0 && position < section.size()) {
                graphObject = graphObjectsBySection.get(sectionKey).get(position);
            } else {
                // We are off the end; we must be adding an activity circle to indicate more data is coming.
                assert dataNeededListener != null && cursor.areMoreObjectsAvailable();
                // We return null for both to indicate this.
                return new SectionAndItem<T>(null, null);
            }
        } else {
            // Count through the sections; the "0" position in each section is the header. We decrement
            // position each time we skip forward a certain number of elements, including the header.
            for (String key : sectionKeys) {
                // Decrement if we skip over the header
                if (position-- == 0) {
                    sectionKey = key;
                    break;
                }

                List<T> section = graphObjectsBySection.get(key);
                if (position < section.size()) {
                    // The position is somewhere in this section. Get the corresponding graph object.
                    sectionKey = key;
                    graphObject = section.get(position);
                    break;
                }
                // Decrement by as many items as we skipped over
                position -= section.size();
            }
        }
        if (sectionKey != null) {
            // Note: graphObject will be null if this represents a section header.
            return new SectionAndItem<T>(sectionKey, graphObject);
        } else {
            throw new IndexOutOfBoundsException("position");
        }
    }

    int getPosition(String sectionKey, T graphObject) {
        int position = 0;
        boolean found = false;

        // First find the section key and increment position one for each header we will render;
        // increment by the size of each section prior to the one we want.
        for (String key : sectionKeys) {
            if (displaySections) {
                position++;
            }
            if (key.equals(sectionKey)) {
                found = true;
                break;
            } else {
                position += graphObjectsBySection.get(key).size();
            }
        }

        if (!found) {
            return -1;
        } else if (graphObject == null) {
            // null represents the header for a section; we counted this header in position earlier,
            // so subtract it back out.
            return position - (displaySections ? 1 : 0);
        }

        // Now find index of this item within that section.
        for (T t : graphObjectsBySection.get(sectionKey)) {
            if (GraphObject.Factory.hasSameId(t, graphObject)) {
                return position;
            }
            position++;
        }
        return -1;
    }

    @Override
    public boolean isEmpty() {
        // We'll never populate sectionKeys unless we have at least one object.
        return sectionKeys.size() == 0;
    }

    @Override
    public int getCount() {
        if (sectionKeys.size() == 0) {
            return 0;
        }

        // If we are not displaying sections, we don't display a header; otherwise, we have one header per item in
        // addition to the actual items.
        int count = (displaySections) ? sectionKeys.size() : 0;
        for (List<T> section : graphObjectsBySection.values()) {
            count += section.size();
        }

        // If we should show a cell with an activity circle indicating more data is coming, add it to the count.
        if (shouldShowActivityCircleCell()) {
            ++count;
        }

        return count;
    }

    @Override
    public boolean areAllItemsEnabled() {
        return displaySections;
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }

    @Override
    public boolean isEnabled(int position) {
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);
        return sectionAndItem.getType() == SectionAndItem.Type.GRAPH_OBJECT;
    }

    @Override
    public Object getItem(int position) {
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);
        return (sectionAndItem.getType() == SectionAndItem.Type.GRAPH_OBJECT) ? sectionAndItem.graphObject : null;
    }

    @Override
    public long getItemId(int position) {
        // We assume IDs that can be converted to longs. If this is not the case for certain types of
        // GraphObjects, subclasses should override this to return, e.g., position, and override hasStableIds
        // to return false.
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);
        if (sectionAndItem != null && sectionAndItem.graphObject != null) {
            String id = getIdOfGraphObject(sectionAndItem.graphObject);
            if (id != null) {
                try {
                    return Long.parseLong(id);
                } catch (NumberFormatException e) {
                    // NOOP
                }
            }
        }
        return 0;
    }

    @Override
    public int getViewTypeCount() {
        return 3;
    }

    @Override
    public int getItemViewType(int position) {
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);
        switch (sectionAndItem.getType()) {
        case SECTION_HEADER:
            return HEADER_VIEW_TYPE;
        case GRAPH_OBJECT:
            return GRAPH_OBJECT_VIEW_TYPE;
        case ACTIVITY_CIRCLE:
            return ACTIVITY_CIRCLE_VIEW_TYPE;
        default:
            throw new FacebookException("Unexpected type of section and item.");
        }
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);

        switch (sectionAndItem.getType()) {
        case SECTION_HEADER:
            return getSectionHeaderView(sectionAndItem.sectionKey, convertView, parent);
        case GRAPH_OBJECT:
            return getGraphObjectView(sectionAndItem.graphObject, convertView, parent);
        case ACTIVITY_CIRCLE:
            // If we get a request for this view, it means we need more data.
            assert cursor.areMoreObjectsAvailable() && (dataNeededListener != null);
            dataNeededListener.onDataNeeded();
            return getActivityCircleView(convertView, parent);
        default:
            throw new FacebookException("Unexpected type of section and item.");
        }
    }

    @Override
    public Object[] getSections() {
        if (displaySections) {
            return sectionKeys.toArray();
        } else {
            return new Object[0];
        }
    }

    @Override
    public int getPositionForSection(int section) {
        if (displaySections) {
            section = Math.max(0, Math.min(section, sectionKeys.size() - 1));
            if (section < sectionKeys.size()) {
                return getPosition(sectionKeys.get(section), null);
            }
        }
        return 0;
    }

    @Override
    public int getSectionForPosition(int position) {
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);
        if (sectionAndItem != null && sectionAndItem.getType() != SectionAndItem.Type.ACTIVITY_CIRCLE) {
            return Math.max(0, Math.min(sectionKeys.indexOf(sectionAndItem.sectionKey), sectionKeys.size() - 1));
        }
        return 0;
    }

    public List<T> getGraphObjectsById(Collection<String> ids) {
        Set<String> idSet = new HashSet<String>();
        idSet.addAll(ids);

        ArrayList<T> result = new ArrayList<T>(idSet.size());
        for (String id : idSet) {
            T graphObject = graphObjectsById.get(id);
            if (graphObject != null) {
                result.add(graphObject);
            }
        }

        return result;
    }

    private void downloadProfilePicture(final String profileId, URI pictureURI, final ImageView imageView) {
        if (pictureURI == null) {
            return;
        }

        // If we don't have an imageView, we are pre-fetching this image to store in-memory because we
        // think the user might scroll to its corresponding list row. If we do have an imageView, we
        // only want to queue a download if the view's tag isn't already set to the URL (which would mean
        // it's already got the correct picture).
        boolean prefetching = imageView == null;
        if (prefetching || !pictureURI.equals(imageView.getTag())) {
            if (!prefetching) {
                // Setting the tag to the profile ID indicates that we're currently downloading the
                // picture for this profile; we'll set it to the actual picture URL when complete.
                imageView.setTag(profileId);
                imageView.setImageResource(getDefaultPicture());
            }

            ImageRequest.Builder builder = new ImageRequest.Builder(context.getApplicationContext(), pictureURI)
                    .setCallerTag(this).setCallback(new ImageRequest.Callback() {
                        @Override
                        public void onCompleted(ImageResponse response) {
                            processImageResponse(response, profileId, imageView);
                        }
                    });

            ImageRequest newRequest = builder.build();
            pendingRequests.put(profileId, newRequest);

            ImageDownloader.downloadAsync(newRequest);
        }
    }

    private void callOnErrorListener(Exception exception) {
        if (onErrorListener != null) {
            if (!(exception instanceof FacebookException)) {
                exception = new FacebookException(exception);
            }
            onErrorListener.onError(this, (FacebookException) exception);
        }
    }

    private void processImageResponse(ImageResponse response, String graphObjectId, ImageView imageView) {
        pendingRequests.remove(graphObjectId);
        if (response.getError() != null) {
            callOnErrorListener(response.getError());
        }

        if (imageView == null) {
            // This was a pre-fetch request.
            if (response.getBitmap() != null) {
                // Is the cache too big?
                if (prefetchedPictureCache.size() >= MAX_PREFETCHED_PICTURES) {
                    // Find the oldest one and remove it.
                    String oldestId = prefetchedProfilePictureIds.remove(0);
                    prefetchedPictureCache.remove(oldestId);
                }
                prefetchedPictureCache.put(graphObjectId, response);
            }
        } else if (graphObjectId.equals(imageView.getTag())) {
            Exception error = response.getError();
            Bitmap bitmap = response.getBitmap();
            if (error == null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
                imageView.setTag(response.getRequest().getImageUri());
            }
        }
    }

    private static int compareGraphObjects(GraphObject a, GraphObject b, Collection<String> sortFields,
            Collator collator) {
        for (String sortField : sortFields) {
            String sa = (String) a.getProperty(sortField);
            String sb = (String) b.getProperty(sortField);

            if (sa != null && sb != null) {
                int result = collator.compare(sa, sb);
                if (result != 0) {
                    return result;
                }
            } else if (!(sa == null && sb == null)) {
                return (sa == null) ? -1 : 1;
            }
        }
        return 0;
    }

    // Graph object type to navigate the JSON that sometimes comes back instead of a URL string
    private interface ItemPicture extends GraphObject {
        ItemPictureData getData();
    }

    // Graph object type to navigate the JSON that sometimes comes back instead of a URL string
    private interface ItemPictureData extends GraphObject {
        String getUrl();
    }
}