Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
14792 manas 1
/**
2
 * Copyright 2010-present Facebook.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *    http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
 
17
package com.facebook.widget;
18
 
19
import android.content.Context;
20
import android.graphics.Bitmap;
21
import android.view.LayoutInflater;
22
import android.view.View;
23
import android.view.ViewGroup;
24
import android.view.ViewStub;
25
import android.widget.*;
26
import com.facebook.FacebookException;
27
import com.facebook.android.R;
28
import com.facebook.internal.ImageDownloader;
29
import com.facebook.internal.ImageRequest;
30
import com.facebook.internal.ImageResponse;
31
import com.facebook.model.GraphObject;
32
import org.json.JSONObject;
33
 
34
import java.net.URI;
35
import java.net.URISyntaxException;
36
import java.text.Collator;
37
import java.util.*;
38
 
39
class GraphObjectAdapter<T extends GraphObject> extends BaseAdapter implements SectionIndexer {
40
    private static final int DISPLAY_SECTIONS_THRESHOLD = 1;
41
    private static final int HEADER_VIEW_TYPE = 0;
42
    private static final int GRAPH_OBJECT_VIEW_TYPE = 1;
43
    private static final int ACTIVITY_CIRCLE_VIEW_TYPE = 2;
44
    private static final int MAX_PREFETCHED_PICTURES = 20;
45
 
46
    private static final String ID = "id";
47
    private static final String NAME = "name";
48
    private static final String PICTURE = "picture";
49
 
50
    private final Map<String, ImageRequest> pendingRequests = new HashMap<String, ImageRequest>();
51
    private final LayoutInflater inflater;
52
    private List<String> sectionKeys = new ArrayList<String>();
53
    private Map<String, ArrayList<T>> graphObjectsBySection = new HashMap<String, ArrayList<T>>();
54
    private Map<String, T> graphObjectsById = new HashMap<String, T>();
55
    private boolean displaySections;
56
    private List<String> sortFields;
57
    private String groupByField;
58
    private boolean showPicture;
59
    private boolean showCheckbox;
60
    private Filter<T> filter;
61
    private DataNeededListener dataNeededListener;
62
    private GraphObjectCursor<T> cursor;
63
    private Context context;
64
    private Map<String, ImageResponse> prefetchedPictureCache = new HashMap<String, ImageResponse>();
65
    private ArrayList<String> prefetchedProfilePictureIds = new ArrayList<String>();
66
    private OnErrorListener onErrorListener;
67
 
68
    public interface DataNeededListener {
69
        public void onDataNeeded();
70
    }
71
 
72
    public interface OnErrorListener {
73
        void onError(GraphObjectAdapter<?> adapter, FacebookException error);
74
    }
75
 
76
    public static class SectionAndItem<T extends GraphObject> {
77
        public String sectionKey;
78
        public T graphObject;
79
 
80
        public enum Type {
81
            GRAPH_OBJECT,
82
            SECTION_HEADER,
83
            ACTIVITY_CIRCLE
84
        }
85
 
86
        public SectionAndItem(String sectionKey, T graphObject) {
87
            this.sectionKey = sectionKey;
88
            this.graphObject = graphObject;
89
        }
90
 
91
        public Type getType() {
92
            if (sectionKey == null) {
93
                return Type.ACTIVITY_CIRCLE;
94
            } else if (graphObject == null) {
95
                return Type.SECTION_HEADER;
96
            } else {
97
                return Type.GRAPH_OBJECT;
98
            }
99
        }
100
    }
101
 
102
    interface Filter<T> {
103
        boolean includeItem(T graphObject);
104
    }
105
 
106
    public GraphObjectAdapter(Context context) {
107
        this.context = context;
108
        this.inflater = LayoutInflater.from(context);
109
    }
110
 
111
    public List<String> getSortFields() {
112
        return sortFields;
113
    }
114
 
115
    public void setSortFields(List<String> sortFields) {
116
        this.sortFields = sortFields;
117
    }
118
 
119
    public String getGroupByField() {
120
        return groupByField;
121
    }
122
 
123
    public void setGroupByField(String groupByField) {
124
        this.groupByField = groupByField;
125
    }
126
 
127
    public boolean getShowPicture() {
128
        return showPicture;
129
    }
130
 
131
    public void setShowPicture(boolean showPicture) {
132
        this.showPicture = showPicture;
133
    }
134
 
135
    public boolean getShowCheckbox() {
136
        return showCheckbox;
137
    }
138
 
139
    public void setShowCheckbox(boolean showCheckbox) {
140
        this.showCheckbox = showCheckbox;
141
    }
142
 
143
    public DataNeededListener getDataNeededListener() {
144
        return dataNeededListener;
145
    }
146
 
147
    public void setDataNeededListener(DataNeededListener dataNeededListener) {
148
        this.dataNeededListener = dataNeededListener;
149
    }
150
 
151
    public OnErrorListener getOnErrorListener() {
152
        return onErrorListener;
153
    }
154
 
155
    public void setOnErrorListener(OnErrorListener onErrorListener) {
156
        this.onErrorListener = onErrorListener;
157
    }
158
 
159
    public GraphObjectCursor<T> getCursor() {
160
        return cursor;
161
    }
162
 
163
    public boolean changeCursor(GraphObjectCursor<T> cursor) {
164
        if (this.cursor == cursor) {
165
            return false;
166
        }
167
        if (this.cursor != null) {
168
            this.cursor.close();
169
        }
170
        this.cursor = cursor;
171
 
172
        rebuildAndNotify();
173
        return true;
174
    }
175
 
176
    public void rebuildAndNotify() {
177
        rebuildSections();
178
        notifyDataSetChanged();
179
    }
180
 
181
    public void prioritizeViewRange(int firstVisibleItem, int lastVisibleItem, int prefetchBuffer) {
182
        if ((lastVisibleItem < firstVisibleItem) || (sectionKeys.size() == 0)) {
183
            return;
184
        }
185
 
186
        // We want to prioritize requests for items which are visible but do not have pictures
187
        // loaded yet. We also want to pre-fetch pictures for items which are not yet visible
188
        // but are within a buffer on either side of the visible items, on the assumption that
189
        // they will be visible soon. For these latter items, we'll store the images in memory
190
        // in the hopes we can immediately populate their image view when needed.
191
 
192
        // Prioritize the requests in reverse order since each call to prioritizeRequest will just
193
        // move it to the front of the queue. And we want the earliest ones in the range to be at
194
        // the front of the queue, so all else being equal, the list will appear to populate from
195
        // the top down.
196
        for (int i = lastVisibleItem; i >= 0; i--) {
197
            SectionAndItem<T> sectionAndItem = getSectionAndItem(i);
198
            if (sectionAndItem.graphObject != null) {
199
                String id = getIdOfGraphObject(sectionAndItem.graphObject);
200
                ImageRequest request = pendingRequests.get(id);
201
                if (request != null) {
202
                    ImageDownloader.prioritizeRequest(request);
203
                }
204
            }
205
        }
206
 
207
        // For items which are not visible, but within the buffer on either side, we want to
208
        // fetch those items and store them in a small in-memory cache of bitmaps.
209
        int start = Math.max(0, firstVisibleItem - prefetchBuffer);
210
        int end = Math.min(lastVisibleItem + prefetchBuffer, getCount() - 1);
211
        ArrayList<T> graphObjectsToPrefetchPicturesFor = new ArrayList<T>();
212
        // Add the IDs before and after the visible range.
213
        for (int i = start; i < firstVisibleItem; ++i) {
214
            SectionAndItem<T> sectionAndItem = getSectionAndItem(i);
215
            if (sectionAndItem.graphObject != null) {
216
                graphObjectsToPrefetchPicturesFor.add(sectionAndItem.graphObject);
217
            }
218
        }
219
        for (int i = lastVisibleItem + 1; i <= end; ++i) {
220
            SectionAndItem<T> sectionAndItem = getSectionAndItem(i);
221
            if (sectionAndItem.graphObject != null) {
222
                graphObjectsToPrefetchPicturesFor.add(sectionAndItem.graphObject);
223
            }
224
        }
225
        for (T graphObject : graphObjectsToPrefetchPicturesFor) {
226
            URI uri = getPictureUriOfGraphObject(graphObject);
227
            final String id = getIdOfGraphObject(graphObject);
228
 
229
            // This URL already have been requested for pre-fetching, but we want to act in an LRU manner, so move
230
            // it to the end of the list regardless.
231
            boolean alreadyPrefetching = prefetchedProfilePictureIds.remove(id);
232
            prefetchedProfilePictureIds.add(id);
233
 
234
            // If we've already requested it for pre-fetching, no need to do so again.
235
            if (!alreadyPrefetching) {
236
                downloadProfilePicture(id, uri, null);
237
            }
238
        }
239
    }
240
 
241
    protected String getSectionKeyOfGraphObject(T graphObject) {
242
        String result = null;
243
 
244
        if (groupByField != null) {
245
            result = (String) graphObject.getProperty(groupByField);
246
            if (result != null && result.length() > 0) {
247
                result = result.substring(0, 1).toUpperCase();
248
            }
249
        }
250
 
251
        return (result != null) ? result : "";
252
    }
253
 
254
    protected CharSequence getTitleOfGraphObject(T graphObject) {
255
        return (String) graphObject.getProperty(NAME);
256
    }
257
 
258
    protected CharSequence getSubTitleOfGraphObject(T graphObject) {
259
        return null;
260
    }
261
 
262
    protected URI getPictureUriOfGraphObject(T graphObject) {
263
        String uri = null;
264
        Object o = graphObject.getProperty(PICTURE);
265
        if (o instanceof String) {
266
            uri = (String) o;
267
        } else if (o instanceof JSONObject) {
268
            ItemPicture itemPicture = GraphObject.Factory.create((JSONObject) o).cast(ItemPicture.class);
269
            ItemPictureData data = itemPicture.getData();
270
            if (data != null) {
271
                uri = data.getUrl();
272
            }
273
        }
274
 
275
        if (uri != null) {
276
            try {
277
                return new URI(uri);
278
            } catch (URISyntaxException e) {
279
            }
280
        }
281
        return null;
282
    }
283
 
284
    protected View getSectionHeaderView(String sectionHeader, View convertView, ViewGroup parent) {
285
        TextView result = (TextView) convertView;
286
 
287
        if (result == null) {
288
            result = (TextView) inflater.inflate(R.layout.com_facebook_picker_list_section_header, null);
289
        }
290
 
291
        result.setText(sectionHeader);
292
 
293
        return result;
294
    }
295
 
296
    protected View getGraphObjectView(T graphObject, View convertView, ViewGroup parent) {
297
        View result = convertView;
298
 
299
        if (result == null) {
300
            result = createGraphObjectView(graphObject);
301
        }
302
 
303
        populateGraphObjectView(result, graphObject);
304
        return result;
305
    }
306
 
307
    private View getActivityCircleView(View convertView, ViewGroup parent) {
308
        View result = convertView;
309
 
310
        if (result == null) {
311
            result = inflater.inflate(R.layout.com_facebook_picker_activity_circle_row, null);
312
        }
313
        ProgressBar activityCircle = (ProgressBar) result.findViewById(R.id.com_facebook_picker_row_activity_circle);
314
        activityCircle.setVisibility(View.VISIBLE);
315
 
316
        return result;
317
    }
318
 
319
    protected int getGraphObjectRowLayoutId(T graphObject) {
320
        return R.layout.com_facebook_picker_list_row;
321
    }
322
 
323
    protected int getDefaultPicture() {
324
        return R.drawable.com_facebook_profile_default_icon;
325
    }
326
 
327
    protected View createGraphObjectView(T graphObject) {
328
        View result = inflater.inflate(getGraphObjectRowLayoutId(graphObject), null);
329
 
330
        ViewStub checkboxStub = (ViewStub) result.findViewById(R.id.com_facebook_picker_checkbox_stub);
331
        if (checkboxStub != null) {
332
            if (!getShowCheckbox()) {
333
                checkboxStub.setVisibility(View.GONE);
334
            } else {
335
                CheckBox checkBox = (CheckBox) checkboxStub.inflate();
336
                updateCheckboxState(checkBox, false);
337
            }
338
        }
339
 
340
        ViewStub profilePicStub = (ViewStub) result.findViewById(R.id.com_facebook_picker_profile_pic_stub);
341
        if (!getShowPicture()) {
342
            profilePicStub.setVisibility(View.GONE);
343
        } else {
344
            ImageView imageView = (ImageView) profilePicStub.inflate();
345
            imageView.setVisibility(View.VISIBLE);
346
        }
347
 
348
        return result;
349
    }
350
 
351
    protected void populateGraphObjectView(View view, T graphObject) {
352
        String id = getIdOfGraphObject(graphObject);
353
        view.setTag(id);
354
 
355
        CharSequence title = getTitleOfGraphObject(graphObject);
356
        TextView titleView = (TextView) view.findViewById(R.id.com_facebook_picker_title);
357
        if (titleView != null) {
358
            titleView.setText(title, TextView.BufferType.SPANNABLE);
359
        }
360
 
361
        CharSequence subtitle = getSubTitleOfGraphObject(graphObject);
362
        TextView subtitleView = (TextView) view.findViewById(R.id.picker_subtitle);
363
        if (subtitleView != null) {
364
            if (subtitle != null) {
365
                subtitleView.setText(subtitle, TextView.BufferType.SPANNABLE);
366
                subtitleView.setVisibility(View.VISIBLE);
367
            } else {
368
                subtitleView.setVisibility(View.GONE);
369
            }
370
        }
371
 
372
        if (getShowCheckbox()) {
373
            CheckBox checkBox = (CheckBox) view.findViewById(R.id.com_facebook_picker_checkbox);
374
            updateCheckboxState(checkBox, isGraphObjectSelected(id));
375
        }
376
 
377
        if (getShowPicture()) {
378
            URI pictureURI = getPictureUriOfGraphObject(graphObject);
379
 
380
            if (pictureURI != null) {
381
                ImageView profilePic = (ImageView) view.findViewById(R.id.com_facebook_picker_image);
382
 
383
                // See if we have already pre-fetched this; if not, download it.
384
                if (prefetchedPictureCache.containsKey(id)) {
385
                    ImageResponse response = prefetchedPictureCache.get(id);
386
                    profilePic.setImageBitmap(response.getBitmap());
387
                    profilePic.setTag(response.getRequest().getImageUri());
388
                } else {
389
                    downloadProfilePicture(id, pictureURI, profilePic);
390
                }
391
            }
392
        }
393
    }
394
 
395
    /**
396
     * @throws FacebookException if the GraphObject doesn't have an ID.
397
     */
398
    String getIdOfGraphObject(T graphObject) {
399
        if (graphObject.asMap().containsKey(ID)) {
400
            Object obj = graphObject.getProperty(ID);
401
            if (obj instanceof String) {
402
                return (String) obj;
403
            }
404
        }
405
        throw new FacebookException("Received an object without an ID.");
406
    }
407
 
408
    boolean filterIncludesItem(T graphObject) {
409
        return filter == null || filter.includeItem(graphObject);
410
    }
411
 
412
    Filter<T> getFilter() {
413
        return filter;
414
    }
415
 
416
    void setFilter(Filter<T> filter) {
417
        this.filter = filter;
418
    }
419
 
420
    boolean isGraphObjectSelected(String graphObjectId) {
421
        return false;
422
    }
423
 
424
    void updateCheckboxState(CheckBox checkBox, boolean graphObjectSelected) {
425
        // Default is no-op
426
    }
427
 
428
    String getPictureFieldSpecifier() {
429
        // How big is our image?
430
        View view = createGraphObjectView(null);
431
        ImageView picture = (ImageView) view.findViewById(R.id.com_facebook_picker_image);
432
        if (picture == null) {
433
            return null;
434
        }
435
 
436
        // Note: these dimensions are in pixels, not dips
437
        ViewGroup.LayoutParams layoutParams = picture.getLayoutParams();
438
        return String.format(Locale.US, "picture.height(%d).width(%d)", layoutParams.height, layoutParams.width);
439
    }
440
 
441
 
442
    private boolean shouldShowActivityCircleCell() {
443
        // We show the "more data" activity circle cell if we have a listener to request more data,
444
        // we are expecting more data, and we have some data already (i.e., not on a fresh query).
445
        return (cursor != null) && cursor.areMoreObjectsAvailable() && (dataNeededListener != null) && !isEmpty();
446
    }
447
 
448
    private void rebuildSections() {
449
        sectionKeys = new ArrayList<String>();
450
        graphObjectsBySection = new HashMap<String, ArrayList<T>>();
451
        graphObjectsById = new HashMap<String, T>();
452
        displaySections = false;
453
 
454
        if (cursor == null || cursor.getCount() == 0) {
455
            return;
456
        }
457
 
458
        int objectsAdded = 0;
459
        cursor.moveToFirst();
460
        do {
461
            T graphObject = cursor.getGraphObject();
462
 
463
            if (!filterIncludesItem(graphObject)) {
464
                continue;
465
            }
466
 
467
            objectsAdded++;
468
 
469
            String sectionKeyOfItem = getSectionKeyOfGraphObject(graphObject);
470
            if (!graphObjectsBySection.containsKey(sectionKeyOfItem)) {
471
                sectionKeys.add(sectionKeyOfItem);
472
                graphObjectsBySection.put(sectionKeyOfItem, new ArrayList<T>());
473
            }
474
            List<T> section = graphObjectsBySection.get(sectionKeyOfItem);
475
            section.add(graphObject);
476
 
477
            graphObjectsById.put(getIdOfGraphObject(graphObject), graphObject);
478
        } while (cursor.moveToNext());
479
 
480
        if (sortFields != null) {
481
            final Collator collator = Collator.getInstance();
482
            for (List<T> section : graphObjectsBySection.values()) {
483
                Collections.sort(section, new Comparator<GraphObject>() {
484
                    @Override
485
                    public int compare(GraphObject a, GraphObject b) {
486
                        return compareGraphObjects(a, b, sortFields, collator);
487
                    }
488
                });
489
            }
490
        }
491
 
492
        Collections.sort(sectionKeys, Collator.getInstance());
493
 
494
        displaySections = sectionKeys.size() > 1 && objectsAdded > DISPLAY_SECTIONS_THRESHOLD;
495
    }
496
 
497
    SectionAndItem<T> getSectionAndItem(int position) {
498
        if (sectionKeys.size() == 0) {
499
            return null;
500
        }
501
        String sectionKey = null;
502
        T graphObject = null;
503
 
504
        if (!displaySections) {
505
            sectionKey = sectionKeys.get(0);
506
            List<T> section = graphObjectsBySection.get(sectionKey);
507
            if (position >= 0 && position < section.size()) {
508
                graphObject = graphObjectsBySection.get(sectionKey).get(position);
509
            } else {
510
                // We are off the end; we must be adding an activity circle to indicate more data is coming.
511
                assert dataNeededListener != null && cursor.areMoreObjectsAvailable();
512
                // We return null for both to indicate this.
513
                return new SectionAndItem<T>(null, null);
514
            }
515
        } else {
516
            // Count through the sections; the "0" position in each section is the header. We decrement
517
            // position each time we skip forward a certain number of elements, including the header.
518
            for (String key : sectionKeys) {
519
                // Decrement if we skip over the header
520
                if (position-- == 0) {
521
                    sectionKey = key;
522
                    break;
523
                }
524
 
525
                List<T> section = graphObjectsBySection.get(key);
526
                if (position < section.size()) {
527
                    // The position is somewhere in this section. Get the corresponding graph object.
528
                    sectionKey = key;
529
                    graphObject = section.get(position);
530
                    break;
531
                }
532
                // Decrement by as many items as we skipped over
533
                position -= section.size();
534
            }
535
        }
536
        if (sectionKey != null) {
537
            // Note: graphObject will be null if this represents a section header.
538
            return new SectionAndItem<T>(sectionKey, graphObject);
539
        } else {
540
            throw new IndexOutOfBoundsException("position");
541
        }
542
    }
543
 
544
    int getPosition(String sectionKey, T graphObject) {
545
        int position = 0;
546
        boolean found = false;
547
 
548
        // First find the section key and increment position one for each header we will render;
549
        // increment by the size of each section prior to the one we want.
550
        for (String key : sectionKeys) {
551
            if (displaySections) {
552
                position++;
553
            }
554
            if (key.equals(sectionKey)) {
555
                found = true;
556
                break;
557
            } else {
558
                position += graphObjectsBySection.get(key).size();
559
            }
560
        }
561
 
562
        if (!found) {
563
            return -1;
564
        } else if (graphObject == null) {
565
            // null represents the header for a section; we counted this header in position earlier,
566
            // so subtract it back out.
567
            return position - (displaySections ? 1 : 0);
568
        }
569
 
570
        // Now find index of this item within that section.
571
        for (T t : graphObjectsBySection.get(sectionKey)) {
572
            if (GraphObject.Factory.hasSameId(t, graphObject)) {
573
                return position;
574
            }
575
            position++;
576
        }
577
        return -1;
578
    }
579
 
580
    @Override
581
    public boolean isEmpty() {
582
        // We'll never populate sectionKeys unless we have at least one object.
583
        return sectionKeys.size() == 0;
584
    }
585
 
586
    @Override
587
    public int getCount() {
588
        if (sectionKeys.size() == 0) {
589
            return 0;
590
        }
591
 
592
        // If we are not displaying sections, we don't display a header; otherwise, we have one header per item in
593
        // addition to the actual items.
594
        int count = (displaySections) ? sectionKeys.size() : 0;
595
        for (List<T> section : graphObjectsBySection.values()) {
596
            count += section.size();
597
        }
598
 
599
        // If we should show a cell with an activity circle indicating more data is coming, add it to the count.
600
        if (shouldShowActivityCircleCell()) {
601
            ++count;
602
        }
603
 
604
        return count;
605
    }
606
 
607
    @Override
608
    public boolean areAllItemsEnabled() {
609
        return displaySections;
610
    }
611
 
612
    @Override
613
    public boolean hasStableIds() {
614
        return true;
615
    }
616
 
617
    @Override
618
    public boolean isEnabled(int position) {
619
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);
620
        return sectionAndItem.getType() == SectionAndItem.Type.GRAPH_OBJECT;
621
    }
622
 
623
    @Override
624
    public Object getItem(int position) {
625
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);
626
        return (sectionAndItem.getType() == SectionAndItem.Type.GRAPH_OBJECT) ? sectionAndItem.graphObject : null;
627
    }
628
 
629
    @Override
630
    public long getItemId(int position) {
631
        // We assume IDs that can be converted to longs. If this is not the case for certain types of
632
        // GraphObjects, subclasses should override this to return, e.g., position, and override hasStableIds
633
        // to return false.
634
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);
635
        if (sectionAndItem != null && sectionAndItem.graphObject != null) {
636
            String id = getIdOfGraphObject(sectionAndItem.graphObject);
637
            if (id != null) {
638
                try {
639
                    return Long.parseLong(id);
640
                } catch (NumberFormatException e) {
641
                    // NOOP
642
                }
643
            }
644
        }
645
        return 0;
646
    }
647
 
648
    @Override
649
    public int getViewTypeCount() {
650
        return 3;
651
    }
652
 
653
    @Override
654
    public int getItemViewType(int position) {
655
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);
656
        switch (sectionAndItem.getType()) {
657
            case SECTION_HEADER:
658
                return HEADER_VIEW_TYPE;
659
            case GRAPH_OBJECT:
660
                return GRAPH_OBJECT_VIEW_TYPE;
661
            case ACTIVITY_CIRCLE:
662
                return ACTIVITY_CIRCLE_VIEW_TYPE;
663
            default:
664
                throw new FacebookException("Unexpected type of section and item.");
665
        }
666
    }
667
 
668
    @Override
669
    public View getView(int position, View convertView, ViewGroup parent) {
670
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);
671
 
672
        switch (sectionAndItem.getType()) {
673
            case SECTION_HEADER:
674
                return getSectionHeaderView(sectionAndItem.sectionKey, convertView, parent);
675
            case GRAPH_OBJECT:
676
                return getGraphObjectView(sectionAndItem.graphObject, convertView, parent);
677
            case ACTIVITY_CIRCLE:
678
                // If we get a request for this view, it means we need more data.
679
                assert cursor.areMoreObjectsAvailable() && (dataNeededListener != null);
680
                dataNeededListener.onDataNeeded();
681
                return getActivityCircleView(convertView, parent);
682
            default:
683
                throw new FacebookException("Unexpected type of section and item.");
684
        }
685
    }
686
 
687
    @Override
688
    public Object[] getSections() {
689
        if (displaySections) {
690
            return sectionKeys.toArray();
691
        } else {
692
            return new Object[0];
693
        }
694
    }
695
 
696
    @Override
697
    public int getPositionForSection(int section) {
698
        if (displaySections) {
699
            section = Math.max(0, Math.min(section, sectionKeys.size() - 1));
700
            if (section < sectionKeys.size()) {
701
                return getPosition(sectionKeys.get(section), null);
702
            }
703
        }
704
        return 0;
705
    }
706
 
707
    @Override
708
    public int getSectionForPosition(int position) {
709
        SectionAndItem<T> sectionAndItem = getSectionAndItem(position);
710
        if (sectionAndItem != null &&
711
                sectionAndItem.getType() != SectionAndItem.Type.ACTIVITY_CIRCLE) {
712
            return Math.max(0, Math.min(sectionKeys.indexOf(sectionAndItem.sectionKey), sectionKeys.size() - 1));
713
        }
714
        return 0;
715
    }
716
 
717
    public List<T> getGraphObjectsById(Collection<String> ids) {
718
        Set<String> idSet = new HashSet<String>();
719
        idSet.addAll(ids);
720
 
721
        ArrayList<T> result = new ArrayList<T>(idSet.size());
722
        for (String id : idSet) {
723
            T graphObject = graphObjectsById.get(id);
724
            if (graphObject != null) {
725
                result.add(graphObject);
726
            }
727
        }
728
 
729
        return result;
730
    }
731
 
732
    private void downloadProfilePicture(final String profileId, URI pictureURI, final ImageView imageView) {
733
        if (pictureURI == null) {
734
            return;
735
        }
736
 
737
        // If we don't have an imageView, we are pre-fetching this image to store in-memory because we
738
        // think the user might scroll to its corresponding list row. If we do have an imageView, we
739
        // only want to queue a download if the view's tag isn't already set to the URL (which would mean
740
        // it's already got the correct picture).
741
        boolean prefetching = imageView == null;
742
        if (prefetching || !pictureURI.equals(imageView.getTag())) {
743
            if (!prefetching) {
744
                // Setting the tag to the profile ID indicates that we're currently downloading the
745
                // picture for this profile; we'll set it to the actual picture URL when complete.
746
                imageView.setTag(profileId);
747
                imageView.setImageResource(getDefaultPicture());
748
            }
749
 
750
            ImageRequest.Builder builder = new ImageRequest.Builder(context.getApplicationContext(), pictureURI)
751
                    .setCallerTag(this)
752
                    .setCallback(
753
                            new ImageRequest.Callback() {
754
                                @Override
755
                                public void onCompleted(ImageResponse response) {
756
                                    processImageResponse(response, profileId, imageView);
757
                                }
758
                            });
759
 
760
            ImageRequest newRequest = builder.build();
761
            pendingRequests.put(profileId, newRequest);
762
 
763
            ImageDownloader.downloadAsync(newRequest);
764
        }
765
    }
766
 
767
    private void callOnErrorListener(Exception exception) {
768
        if (onErrorListener != null) {
769
            if (!(exception instanceof FacebookException)) {
770
                exception = new FacebookException(exception);
771
            }
772
            onErrorListener.onError(this, (FacebookException) exception);
773
        }
774
    }
775
 
776
    private void processImageResponse(ImageResponse response, String graphObjectId, ImageView imageView) {
777
        pendingRequests.remove(graphObjectId);
778
        if (response.getError() != null) {
779
            callOnErrorListener(response.getError());
780
        }
781
 
782
        if (imageView == null) {
783
            // This was a pre-fetch request.
784
            if (response.getBitmap() != null) {
785
                // Is the cache too big?
786
                if (prefetchedPictureCache.size() >= MAX_PREFETCHED_PICTURES) {
787
                    // Find the oldest one and remove it.
788
                    String oldestId = prefetchedProfilePictureIds.remove(0);
789
                    prefetchedPictureCache.remove(oldestId);
790
                }
791
                prefetchedPictureCache.put(graphObjectId, response);
792
            }
793
        } else if (graphObjectId.equals(imageView.getTag())) {
794
            Exception error = response.getError();
795
            Bitmap bitmap = response.getBitmap();
796
            if (error == null && bitmap != null) {
797
                imageView.setImageBitmap(bitmap);
798
                imageView.setTag(response.getRequest().getImageUri());
799
            }
800
        }
801
    }
802
 
803
    private static int compareGraphObjects(GraphObject a, GraphObject b, Collection<String> sortFields,
804
            Collator collator) {
805
        for (String sortField : sortFields) {
806
            String sa = (String) a.getProperty(sortField);
807
            String sb = (String) b.getProperty(sortField);
808
 
809
            if (sa != null && sb != null) {
810
                int result = collator.compare(sa, sb);
811
                if (result != 0) {
812
                    return result;
813
                }
814
            } else if (!(sa == null && sb == null)) {
815
                return (sa == null) ? -1 : 1;
816
            }
817
        }
818
        return 0;
819
    }
820
 
821
 
822
    // Graph object type to navigate the JSON that sometimes comes back instead of a URL string
823
    private interface ItemPicture extends GraphObject {
824
        ItemPictureData getData();
825
    }
826
 
827
    // Graph object type to navigate the JSON that sometimes comes back instead of a URL string
828
    private interface ItemPictureData extends GraphObject {
829
        String getUrl();
830
    }
831
}