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.app.Activity;
20
import android.content.Context;
21
import android.content.res.TypedArray;
22
import android.graphics.drawable.Drawable;
23
import android.os.Bundle;
24
import android.support.v4.app.Fragment;
25
import android.support.v4.app.LoaderManager;
26
import android.support.v4.content.Loader;
27
import android.text.TextUtils;
28
import android.util.AttributeSet;
29
import android.view.LayoutInflater;
30
import android.view.View;
31
import android.view.ViewGroup;
32
import android.view.ViewStub;
33
import android.view.animation.AlphaAnimation;
34
import android.widget.*;
35
import com.facebook.FacebookException;
36
import com.facebook.Request;
37
import com.facebook.Session;
38
import com.facebook.SessionState;
39
import com.facebook.android.R;
40
import com.facebook.internal.SessionTracker;
41
import com.facebook.model.GraphObject;
42
 
43
import java.util.*;
44
 
45
/**
46
 * Provides functionality common to SDK UI elements that allow the user to pick one or more
47
 * graph objects (e.g., places, friends) from a list of possibilities. The UI is exposed as a
48
 * Fragment to allow to it to be included in an Activity along with other Fragments. The Fragments
49
 * can be configured by passing parameters as part of their Intent bundle, or (for certain
50
 * properties) by specifying attributes in their XML layout files.
51
 * <br/>
52
 * PickerFragments support callbacks that will be called in the event of an error, when the
53
 * underlying data has been changed, or when the set of selected graph objects changes.
54
 */
55
public abstract class PickerFragment<T extends GraphObject> extends Fragment {
56
    /**
57
     * The key for a boolean parameter in the fragment's Intent bundle to indicate whether the
58
     * picker should show pictures (if available) for the graph objects.
59
     */
60
    public static final String SHOW_PICTURES_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ShowPictures";
61
    /**
62
     * The key for a String parameter in the fragment's Intent bundle to indicate which extra fields
63
     * beyond the default fields should be retrieved for any graph objects in the results.
64
     */
65
    public static final String EXTRA_FIELDS_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ExtraFields";
66
    /**
67
     * The key for a boolean parameter in the fragment's Intent bundle to indicate whether the
68
     * picker should display a title bar with a Done button.
69
     */
70
    public static final String SHOW_TITLE_BAR_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ShowTitleBar";
71
    /**
72
     * The key for a String parameter in the fragment's Intent bundle to indicate the text to
73
     * display in the title bar.
74
     */
75
    public static final String TITLE_TEXT_BUNDLE_KEY = "com.facebook.widget.PickerFragment.TitleText";
76
    /**
77
     * The key for a String parameter in the fragment's Intent bundle to indicate the text to
78
     * display in the Done btuton.
79
     */
80
    public static final String DONE_BUTTON_TEXT_BUNDLE_KEY = "com.facebook.widget.PickerFragment.DoneButtonText";
81
 
82
    private static final String SELECTION_BUNDLE_KEY = "com.facebook.android.PickerFragment.Selection";
83
    private static final String ACTIVITY_CIRCLE_SHOW_KEY = "com.facebook.android.PickerFragment.ActivityCircleShown";
84
    private static final int PROFILE_PICTURE_PREFETCH_BUFFER = 5;
85
 
86
    private final int layout;
87
    private OnErrorListener onErrorListener;
88
    private OnDataChangedListener onDataChangedListener;
89
    private OnSelectionChangedListener onSelectionChangedListener;
90
    private OnDoneButtonClickedListener onDoneButtonClickedListener;
91
    private GraphObjectFilter<T> filter;
92
    private boolean showPictures = true;
93
    private boolean showTitleBar = true;
94
    private ListView listView;
95
    HashSet<String> extraFields = new HashSet<String>();
96
    GraphObjectAdapter<T> adapter;
97
    private final Class<T> graphObjectClass;
98
    private LoadingStrategy loadingStrategy;
99
    private SelectionStrategy selectionStrategy;
100
    private Set<String> selectionHint;
101
    private ProgressBar activityCircle;
102
    private SessionTracker sessionTracker;
103
    private String titleText;
104
    private String doneButtonText;
105
    private TextView titleTextView;
106
    private Button doneButton;
107
    private Drawable titleBarBackground;
108
    private Drawable doneButtonBackground;
109
    private boolean appEventsLogged;
110
 
111
    PickerFragment(Class<T> graphObjectClass, int layout, Bundle args) {
112
        this.graphObjectClass = graphObjectClass;
113
        this.layout = layout;
114
 
115
        setPickerFragmentSettingsFromBundle(args);
116
    }
117
 
118
    @Override
119
    public void onCreate(Bundle savedInstanceState) {
120
        super.onCreate(savedInstanceState);
121
 
122
        adapter = createAdapter();
123
        adapter.setFilter(new GraphObjectAdapter.Filter<T>() {
124
            @Override
125
            public boolean includeItem(T graphObject) {
126
                return filterIncludesItem(graphObject);
127
            }
128
        });
129
    }
130
 
131
    @Override
132
    public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) {
133
        super.onInflate(activity, attrs, savedInstanceState);
134
        TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.com_facebook_picker_fragment);
135
 
136
        setShowPictures(a.getBoolean(R.styleable.com_facebook_picker_fragment_show_pictures, showPictures));
137
        String extraFieldsString = a.getString(R.styleable.com_facebook_picker_fragment_extra_fields);
138
        if (extraFieldsString != null) {
139
            String[] strings = extraFieldsString.split(",");
140
            setExtraFields(Arrays.asList(strings));
141
        }
142
 
143
        showTitleBar = a.getBoolean(R.styleable.com_facebook_picker_fragment_show_title_bar, showTitleBar);
144
        titleText = a.getString(R.styleable.com_facebook_picker_fragment_title_text);
145
        doneButtonText = a.getString(R.styleable.com_facebook_picker_fragment_done_button_text);
146
        titleBarBackground = a.getDrawable(R.styleable.com_facebook_picker_fragment_title_bar_background);
147
        doneButtonBackground = a.getDrawable(R.styleable.com_facebook_picker_fragment_done_button_background);
148
 
149
        a.recycle();
150
    }
151
 
152
    @Override
153
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
154
        ViewGroup view = (ViewGroup) inflater.inflate(layout, container, false);
155
 
156
        listView = (ListView) view.findViewById(R.id.com_facebook_picker_list_view);
157
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
158
            @Override
159
            public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
160
                onListItemClick((ListView) parent, v, position);
161
            }
162
        });
163
        listView.setOnLongClickListener(new View.OnLongClickListener() {
164
            @Override
165
            public boolean onLongClick(View v) {
166
                // We don't actually do anything differently on long-clicks, but setting the listener
167
                // enables the selector transition that we have for visual consistency with the
168
                // Facebook app's pickers.
169
                return false;
170
            }
171
        });
172
        listView.setOnScrollListener(onScrollListener);
173
 
174
        activityCircle = (ProgressBar) view.findViewById(R.id.com_facebook_picker_activity_circle);
175
 
176
        setupViews(view);
177
 
178
        listView.setAdapter(adapter);
179
 
180
        return view;
181
    }
182
 
183
    @Override
184
    public void onActivityCreated(final Bundle savedInstanceState) {
185
        super.onActivityCreated(savedInstanceState);
186
 
187
        sessionTracker = new SessionTracker(getActivity(), new Session.StatusCallback() {
188
            @Override
189
            public void call(Session session, SessionState state, Exception exception) {
190
                if (!session.isOpened()) {
191
                    // When a session is closed, we want to clear out our data so it is not visible to subsequent users
192
                    clearResults();
193
                }
194
            }
195
        });
196
 
197
        setSettingsFromBundle(savedInstanceState);
198
 
199
        loadingStrategy = createLoadingStrategy();
200
        loadingStrategy.attach(adapter);
201
 
202
        selectionStrategy = createSelectionStrategy();
203
        selectionStrategy.readSelectionFromBundle(savedInstanceState, SELECTION_BUNDLE_KEY);
204
 
205
        // Should we display a title bar? (We need to do this after we've retrieved our bundle settings.)
206
        if (showTitleBar) {
207
            inflateTitleBar((ViewGroup) getView());
208
        }
209
 
210
        if (activityCircle != null && savedInstanceState != null) {
211
            boolean shown = savedInstanceState.getBoolean(ACTIVITY_CIRCLE_SHOW_KEY, false);
212
            if (shown) {
213
                displayActivityCircle();
214
            } else {
215
                // Should be hidden already, but just to be sure.
216
                hideActivityCircle();
217
            }
218
        }
219
    }
220
 
221
    @Override
222
    public void onDetach() {
223
        super.onDetach();
224
 
225
        listView.setOnScrollListener(null);
226
        listView.setAdapter(null);
227
 
228
        loadingStrategy.detach();
229
        sessionTracker.stopTracking();
230
    }
231
 
232
    @Override
233
    public void onSaveInstanceState(Bundle outState) {
234
        super.onSaveInstanceState(outState);
235
 
236
        saveSettingsToBundle(outState);
237
        selectionStrategy.saveSelectionToBundle(outState, SELECTION_BUNDLE_KEY);
238
        if (activityCircle != null) {
239
            outState.putBoolean(ACTIVITY_CIRCLE_SHOW_KEY, activityCircle.getVisibility() == View.VISIBLE);
240
        }
241
    }
242
 
243
    @Override
244
    public void onStop() {
245
        if (!appEventsLogged) {
246
            logAppEvents(false);
247
        }
248
        super.onStop();
249
    }
250
 
251
    @Override
252
    public void setArguments(Bundle args) {
253
        super.setArguments(args);
254
        setSettingsFromBundle(args);
255
    }
256
 
257
    /**
258
     * Gets the current OnDataChangedListener for this fragment, which will be called whenever
259
     * the underlying data being displaying in the picker has changed.
260
     *
261
     * @return the OnDataChangedListener, or null if there is none
262
     */
263
    public OnDataChangedListener getOnDataChangedListener() {
264
        return onDataChangedListener;
265
    }
266
 
267
    /**
268
     * Sets the current OnDataChangedListener for this fragment, which will be called whenever
269
     * the underlying data being displaying in the picker has changed.
270
     *
271
     * @param onDataChangedListener the OnDataChangedListener, or null if there is none
272
     */
273
    public void setOnDataChangedListener(OnDataChangedListener onDataChangedListener) {
274
        this.onDataChangedListener = onDataChangedListener;
275
    }
276
 
277
    /**
278
     * Gets the current OnSelectionChangedListener for this fragment, which will be called
279
     * whenever the user selects or unselects a graph object in the list.
280
     *
281
     * @return the OnSelectionChangedListener, or null if there is none
282
     */
283
    public OnSelectionChangedListener getOnSelectionChangedListener() {
284
        return onSelectionChangedListener;
285
    }
286
 
287
    /**
288
     * Sets the current OnSelectionChangedListener for this fragment, which will be called
289
     * whenever the user selects or unselects a graph object in the list.
290
     *
291
     * @param onSelectionChangedListener the OnSelectionChangedListener, or null if there is none
292
     */
293
    public void setOnSelectionChangedListener(
294
            OnSelectionChangedListener onSelectionChangedListener) {
295
        this.onSelectionChangedListener = onSelectionChangedListener;
296
    }
297
 
298
    /**
299
     * Gets the current OnDoneButtonClickedListener for this fragment, which will be called
300
     * when the user clicks the Done button.
301
     *
302
     * @return the OnDoneButtonClickedListener, or null if there is none
303
     */
304
    public OnDoneButtonClickedListener getOnDoneButtonClickedListener() {
305
        return onDoneButtonClickedListener;
306
    }
307
 
308
    /**
309
     * Sets the current OnDoneButtonClickedListener for this fragment, which will be called
310
     * when the user clicks the Done button. This will only be possible if the title bar is
311
     * being shown in this fragment.
312
     *
313
     * @param onDoneButtonClickedListener the OnDoneButtonClickedListener, or null if there is none
314
     */
315
    public void setOnDoneButtonClickedListener(OnDoneButtonClickedListener onDoneButtonClickedListener) {
316
        this.onDoneButtonClickedListener = onDoneButtonClickedListener;
317
    }
318
 
319
    /**
320
     * Gets the current OnErrorListener for this fragment, which will be called in the event
321
     * of network or other errors encountered while populating the graph objects in the list.
322
     *
323
     * @return the OnErrorListener, or null if there is none
324
     */
325
    public OnErrorListener getOnErrorListener() {
326
        return onErrorListener;
327
    }
328
 
329
    /**
330
     * Sets the current OnErrorListener for this fragment, which will be called in the event
331
     * of network or other errors encountered while populating the graph objects in the list.
332
     *
333
     * @param onErrorListener the OnErrorListener, or null if there is none
334
     */
335
    public void setOnErrorListener(OnErrorListener onErrorListener) {
336
        this.onErrorListener = onErrorListener;
337
    }
338
 
339
    /**
340
     * Gets the current filter for this fragment, which will be called for each graph object
341
     * returned from the service to determine if it should be displayed in the list.
342
     * If no filter is specified, all retrieved graph objects will be displayed.
343
     *
344
     * @return the GraphObjectFilter, or null if there is none
345
     */
346
    public GraphObjectFilter<T> getFilter() {
347
        return filter;
348
    }
349
 
350
    /**
351
     * Sets the current filter for this fragment, which will be called for each graph object
352
     * returned from the service to determine if it should be displayed in the list.
353
     * If no filter is specified, all retrieved graph objects will be displayed.
354
     *
355
     * @param filter the GraphObjectFilter, or null if there is none
356
     */
357
    public void setFilter(GraphObjectFilter<T> filter) {
358
        this.filter = filter;
359
    }
360
 
361
    /**
362
     * Gets the Session to use for any Facebook requests this fragment will make.
363
     *
364
     * @return the Session that will be used for any Facebook requests, or null if there is none
365
     */
366
    public Session getSession() {
367
        return sessionTracker.getSession();
368
    }
369
 
370
    /**
371
     * Sets the Session to use for any Facebook requests this fragment will make. If the
372
     * parameter is null, the fragment will use the current active session, if any.
373
     *
374
     * @param session the Session to use for Facebook requests, or null to use the active session
375
     */
376
    public void setSession(Session session) {
377
        sessionTracker.setSession(session);
378
    }
379
 
380
    /**
381
     * Gets whether to display pictures, if available, for displayed graph objects.
382
     *
383
     * @return true if pictures should be displayed, false if not
384
     */
385
    public boolean getShowPictures() {
386
        return showPictures;
387
    }
388
 
389
    /**
390
     * Sets whether to display pictures, if available, for displayed graph objects.
391
     *
392
     * @param showPictures true if pictures should be displayed, false if not
393
     */
394
    public void setShowPictures(boolean showPictures) {
395
        this.showPictures = showPictures;
396
    }
397
 
398
    /**
399
     * Gets the extra fields to request for the retrieved graph objects.
400
     *
401
     * @return the extra fields to request
402
     */
403
    public Set<String> getExtraFields() {
404
        return new HashSet<String>(extraFields);
405
    }
406
 
407
    /**
408
     * Sets the extra fields to request for the retrieved graph objects.
409
     *
410
     * @param fields the extra fields to request
411
     */
412
    public void setExtraFields(Collection<String> fields) {
413
        extraFields = new HashSet<String>();
414
        if (fields != null) {
415
            extraFields.addAll(fields);
416
        }
417
    }
418
 
419
    /**
420
     * Sets whether to show a title bar with a Done button. This must be
421
     * called prior to the Fragment going through its creation lifecycle to have an effect.
422
     *
423
     * @param showTitleBar true if a title bar should be displayed, false if not
424
     */
425
    public void setShowTitleBar(boolean showTitleBar) {
426
        this.showTitleBar = showTitleBar;
427
    }
428
 
429
    /**
430
     * Gets whether to show a title bar with a Done button. The default is true.
431
     *
432
     * @return true if a title bar will be shown, false if not.
433
     */
434
    public boolean getShowTitleBar() {
435
        return showTitleBar;
436
    }
437
 
438
    /**
439
     * Sets the text to show in the title bar, if a title bar is to be shown. This must be
440
     * called prior to the Fragment going through its creation lifecycle to have an effect, or
441
     * the default will be used.
442
     *
443
     * @param titleText the text to show in the title bar
444
     */
445
    public void setTitleText(String titleText) {
446
        this.titleText = titleText;
447
    }
448
 
449
    /**
450
     * Gets the text to show in the title bar, if a title bar is to be shown.
451
     *
452
     * @return the text to show in the title bar
453
     */
454
    public String getTitleText() {
455
        if (titleText == null) {
456
            titleText = getDefaultTitleText();
457
        }
458
        return titleText;
459
    }
460
 
461
    /**
462
     * Sets the text to show in the Done button, if a title bar is to be shown. This must be
463
     * called prior to the Fragment going through its creation lifecycle to have an effect, or
464
     * the default will be used.
465
     *
466
     * @param doneButtonText the text to show in the Done button
467
     */
468
    public void setDoneButtonText(String doneButtonText) {
469
        this.doneButtonText = doneButtonText;
470
    }
471
 
472
    /**
473
     * Gets the text to show in the Done button, if a title bar is to be shown.
474
     *
475
     * @return the text to show in the Done button
476
     */
477
    public String getDoneButtonText() {
478
        if (doneButtonText == null) {
479
            doneButtonText = getDefaultDoneButtonText();
480
        }
481
        return doneButtonText;
482
    }
483
 
484
    /**
485
     * Causes the picker to load data from the service and display it to the user.
486
     *
487
     * @param forceReload if true, data will be loaded even if there is already data being displayed (or loading);
488
     *                    if false, data will not be re-loaded if it is already displayed (or loading)
489
     */
490
    public void loadData(boolean forceReload) {
491
        loadData(forceReload, null);
492
    }
493
 
494
    /**
495
     * Causes the picker to load data from the service and display it to the user.
496
     *
497
     * @param forceReload if true, data will be loaded even if there is already data being displayed (or loading);
498
     *                    if false, data will not be re-loaded if it is already displayed (or loading)
499
     * @param selectIds ids to select, if they are present in the loaded data
500
     */
501
    public void loadData(boolean forceReload, Set<String> selectIds) {
502
        if (!forceReload && loadingStrategy.isDataPresentOrLoading()) {
503
            return;
504
        }
505
        selectionHint = selectIds;
506
        loadDataSkippingRoundTripIfCached();
507
    }
508
 
509
    /**
510
     * Updates the properties of the PickerFragment based on the contents of the supplied Bundle;
511
     * calling Activities may use this to pass additional configuration information to the
512
     * PickerFragment beyond what is specified in its XML layout.
513
     *
514
     * @param inState a Bundle containing keys corresponding to properties of the PickerFragment
515
     */
516
    public void setSettingsFromBundle(Bundle inState) {
517
        setPickerFragmentSettingsFromBundle(inState);
518
    }
519
 
520
    void setupViews(ViewGroup view) {
521
    }
522
 
523
    boolean filterIncludesItem(T graphObject) {
524
        if (filter != null) {
525
            return filter.includeItem(graphObject);
526
        }
527
        return true;
528
    }
529
 
530
    List<T> getSelectedGraphObjects() {
531
        return adapter.getGraphObjectsById(selectionStrategy.getSelectedIds());
532
    }
533
 
534
    void setSelectedGraphObjects(List<String> objectIds) {
535
        for(String objectId : objectIds) {
536
            if(!this.selectionStrategy.isSelected(objectId)) {
537
                this.selectionStrategy.toggleSelection(objectId);
538
            }
539
        }
540
    }
541
 
542
    void saveSettingsToBundle(Bundle outState) {
543
        outState.putBoolean(SHOW_PICTURES_BUNDLE_KEY, showPictures);
544
        if (!extraFields.isEmpty()) {
545
            outState.putString(EXTRA_FIELDS_BUNDLE_KEY, TextUtils.join(",", extraFields));
546
        }
547
        outState.putBoolean(SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar);
548
        outState.putString(TITLE_TEXT_BUNDLE_KEY, titleText);
549
        outState.putString(DONE_BUTTON_TEXT_BUNDLE_KEY, doneButtonText);
550
    }
551
 
552
    abstract Request getRequestForLoadData(Session session);
553
 
554
    abstract PickerFragmentAdapter<T> createAdapter();
555
 
556
    abstract LoadingStrategy createLoadingStrategy();
557
 
558
    abstract SelectionStrategy createSelectionStrategy();
559
 
560
    void onLoadingData() {
561
    }
562
 
563
    String getDefaultTitleText() {
564
        return null;
565
    }
566
 
567
    String getDefaultDoneButtonText() {
568
        return getString(R.string.com_facebook_picker_done_button_text);
569
    }
570
 
571
    void displayActivityCircle() {
572
        if (activityCircle != null) {
573
            layoutActivityCircle();
574
            activityCircle.setVisibility(View.VISIBLE);
575
        }
576
    }
577
 
578
    void layoutActivityCircle() {
579
        // If we've got no data, make the activity circle full-opacity. Otherwise we'll dim it to avoid
580
        //  cluttering the UI.
581
        float alpha = (!adapter.isEmpty()) ? .25f : 1.0f;
582
        setAlpha(activityCircle, alpha);
583
    }
584
 
585
    void hideActivityCircle() {
586
        if (activityCircle != null) {
587
            // We use an animation to dim the activity circle; need to clear this or it will remain visible.
588
            activityCircle.clearAnimation();
589
            activityCircle.setVisibility(View.INVISIBLE);
590
        }
591
    }
592
 
593
    void setSelectionStrategy(SelectionStrategy selectionStrategy) {
594
        if (selectionStrategy != this.selectionStrategy) {
595
            this.selectionStrategy = selectionStrategy;
596
            if (adapter != null) {
597
                // Adapter should cause a re-render.
598
                adapter.notifyDataSetChanged();
599
            }
600
        }
601
    }
602
 
603
    void logAppEvents(boolean doneButtonClicked) {
604
    }
605
 
606
    private static void setAlpha(View view, float alpha) {
607
        // Set the alpha appropriately (setAlpha is API >= 11, this technique works on all API levels).
608
        AlphaAnimation alphaAnimation = new AlphaAnimation(alpha, alpha);
609
        alphaAnimation.setDuration(0);
610
        alphaAnimation.setFillAfter(true);
611
        view.startAnimation(alphaAnimation);
612
    }
613
 
614
 
615
    private void setPickerFragmentSettingsFromBundle(Bundle inState) {
616
        // We do this in a separate non-overridable method so it is safe to call from the constructor.
617
        if (inState != null) {
618
            showPictures = inState.getBoolean(SHOW_PICTURES_BUNDLE_KEY, showPictures);
619
            String extraFieldsString = inState.getString(EXTRA_FIELDS_BUNDLE_KEY);
620
            if (extraFieldsString != null) {
621
                String[] strings = extraFieldsString.split(",");
622
                setExtraFields(Arrays.asList(strings));
623
            }
624
            showTitleBar = inState.getBoolean(SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar);
625
            String titleTextString = inState.getString(TITLE_TEXT_BUNDLE_KEY);
626
            if (titleTextString != null) {
627
                titleText = titleTextString;
628
                if (titleTextView != null) {
629
                    titleTextView.setText(titleText);
630
                }
631
            }
632
            String doneButtonTextString = inState.getString(DONE_BUTTON_TEXT_BUNDLE_KEY);
633
            if (doneButtonTextString != null) {
634
                doneButtonText = doneButtonTextString;
635
                if (doneButton != null) {
636
                    doneButton.setText(doneButtonText);
637
                }
638
            }
639
        }
640
    }
641
 
642
    private void inflateTitleBar(ViewGroup view) {
643
        ViewStub stub = (ViewStub) view.findViewById(R.id.com_facebook_picker_title_bar_stub);
644
        if (stub != null) {
645
            View titleBar = stub.inflate();
646
 
647
            final RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
648
                    RelativeLayout.LayoutParams.MATCH_PARENT,
649
                    RelativeLayout.LayoutParams.MATCH_PARENT);
650
            layoutParams.addRule(RelativeLayout.BELOW, R.id.com_facebook_picker_title_bar);
651
            listView.setLayoutParams(layoutParams);
652
 
653
            if (titleBarBackground != null) {
654
                titleBar.setBackgroundDrawable(titleBarBackground);
655
            }
656
 
657
            doneButton = (Button) view.findViewById(R.id.com_facebook_picker_done_button);
658
            if (doneButton != null) {
659
                doneButton.setOnClickListener(new View.OnClickListener() {
660
                    @Override
661
                    public void onClick(View v) {
662
                        logAppEvents(true);
663
                        appEventsLogged = true;
664
 
665
                        if (onDoneButtonClickedListener != null) {
666
                            onDoneButtonClickedListener.onDoneButtonClicked(PickerFragment.this);
667
                        }
668
                    }
669
                });
670
 
671
                if (getDoneButtonText() != null) {
672
                    doneButton.setText(getDoneButtonText());
673
                }
674
 
675
                if (doneButtonBackground != null) {
676
                    doneButton.setBackgroundDrawable(doneButtonBackground);
677
                }
678
            }
679
 
680
            titleTextView = (TextView) view.findViewById(R.id.com_facebook_picker_title);
681
            if (titleTextView != null) {
682
                if (getTitleText() != null) {
683
                    titleTextView.setText(getTitleText());
684
                }
685
            }
686
        }
687
    }
688
 
689
    private void onListItemClick(ListView listView, View v, int position) {
690
        @SuppressWarnings("unchecked")
691
        T graphObject = (T) listView.getItemAtPosition(position);
692
        String id = adapter.getIdOfGraphObject(graphObject);
693
        selectionStrategy.toggleSelection(id);
694
        adapter.notifyDataSetChanged();
695
 
696
        if (onSelectionChangedListener != null) {
697
            onSelectionChangedListener.onSelectionChanged(PickerFragment.this);
698
        }
699
    }
700
 
701
    private void loadDataSkippingRoundTripIfCached() {
702
        clearResults();
703
 
704
        Request request = getRequestForLoadData(getSession());
705
        if (request != null) {
706
            onLoadingData();
707
            loadingStrategy.startLoading(request);
708
        }
709
    }
710
 
711
    private void clearResults() {
712
        if (adapter != null) {
713
            boolean wasSelection = !selectionStrategy.isEmpty();
714
            boolean wasData = !adapter.isEmpty();
715
 
716
            loadingStrategy.clearResults();
717
            selectionStrategy.clear();
718
            adapter.notifyDataSetChanged();
719
 
720
            // Tell anyone who cares the data and selection has changed, if they have.
721
            if (wasData && onDataChangedListener != null) {
722
                onDataChangedListener.onDataChanged(PickerFragment.this);
723
            }
724
            if (wasSelection && onSelectionChangedListener != null) {
725
                onSelectionChangedListener.onSelectionChanged(PickerFragment.this);
726
            }
727
        }
728
    }
729
 
730
    void updateAdapter(SimpleGraphObjectCursor<T> data) {
731
        if (adapter != null) {
732
            // As we fetch additional results and add them to the table, we do not
733
            // want the items displayed jumping around seemingly at random, frustrating the user's
734
            // attempts at scrolling, etc. Since results may be added anywhere in
735
            // the table, we choose to try to keep the first visible row in a fixed
736
            // position (from the user's perspective). We try to keep it positioned at
737
            // the same offset from the top of the screen so adding new items seems
738
            // smoother, as opposed to having it "snap" to a multiple of row height
739
 
740
            // We use the second row, to give context above and below it and avoid
741
            // cases where the first row is only barely visible, thus providing little context.
742
            // The exception is where the very first row is visible, in which case we use that.
743
            View view = listView.getChildAt(1);
744
            int anchorPosition = listView.getFirstVisiblePosition();
745
            if (anchorPosition > 0) {
746
                anchorPosition++;
747
            }
748
            GraphObjectAdapter.SectionAndItem<T> anchorItem = adapter.getSectionAndItem(anchorPosition);
749
            final int top = (view != null &&
750
                    anchorItem.getType() != GraphObjectAdapter.SectionAndItem.Type.ACTIVITY_CIRCLE) ?
751
                    view.getTop() : 0;
752
 
753
            // Now actually add the results.
754
            boolean dataChanged = adapter.changeCursor(data);
755
 
756
            if (view != null && anchorItem != null) {
757
                // Put the item back in the same spot it was.
758
                final int newPositionOfItem = adapter.getPosition(anchorItem.sectionKey, anchorItem.graphObject);
759
                if (newPositionOfItem != -1) {
760
                    listView.setSelectionFromTop(newPositionOfItem, top);
761
                }
762
            }
763
 
764
            if (dataChanged && onDataChangedListener != null) {
765
                onDataChangedListener.onDataChanged(PickerFragment.this);
766
            }
767
            if (selectionHint != null && !selectionHint.isEmpty() && data != null) {
768
                data.moveToFirst();
769
                boolean changed = false;
770
                for (int i = 0; i < data.getCount(); i++) {
771
                    data.moveToPosition(i);
772
                    T graphObject = data.getGraphObject();
773
                    if (!graphObject.asMap().containsKey("id"))
774
                        continue;
775
                    Object obj = graphObject.getProperty("id");
776
                    if (!(obj instanceof String)) {
777
                        continue;
778
                    }
779
                    String id = (String) obj;
780
                    if (selectionHint.contains(id)) {
781
                        selectionStrategy.toggleSelection(id);
782
                        selectionHint.remove(id);
783
                        changed = true;
784
                    }
785
                    if (selectionHint.isEmpty()) {
786
                        break;
787
                    }
788
                }
789
                if (onSelectionChangedListener != null && changed) {
790
                    onSelectionChangedListener.onSelectionChanged(PickerFragment.this);
791
                }
792
            }
793
        }
794
    }
795
 
796
    private void reprioritizeDownloads() {
797
        int lastVisibleItem = listView.getLastVisiblePosition();
798
        if (lastVisibleItem >= 0) {
799
            int firstVisibleItem = listView.getFirstVisiblePosition();
800
            adapter.prioritizeViewRange(firstVisibleItem, lastVisibleItem, PROFILE_PICTURE_PREFETCH_BUFFER);
801
        }
802
    }
803
 
804
    private ListView.OnScrollListener onScrollListener = new ListView.OnScrollListener() {
805
        @Override
806
        public void onScrollStateChanged(AbsListView view, int scrollState) {
807
        }
808
 
809
        @Override
810
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
811
            reprioritizeDownloads();
812
        }
813
    };
814
 
815
    /**
816
     * Callback interface that will be called when a network or other error is encountered
817
     * while retrieving graph objects.
818
     */
819
    public interface OnErrorListener {
820
        /**
821
         * Called when a network or other error is encountered.
822
         *
823
         * @param error a FacebookException representing the error that was encountered.
824
         */
825
        void onError(PickerFragment<?> fragment, FacebookException error);
826
    }
827
 
828
    /**
829
     * Callback interface that will be called when the underlying data being displayed in the
830
     * picker has been updated.
831
     */
832
    public interface OnDataChangedListener {
833
        /**
834
         * Called when the set of data being displayed in the picker has changed.
835
         */
836
        void onDataChanged(PickerFragment<?> fragment);
837
    }
838
 
839
    /**
840
     * Callback interface that will be called when the user selects or unselects graph objects
841
     * in the picker.
842
     */
843
    public interface OnSelectionChangedListener {
844
        /**
845
         * Called when the user selects or unselects graph objects in the picker.
846
         */
847
        void onSelectionChanged(PickerFragment<?> fragment);
848
    }
849
 
850
    /**
851
     * Callback interface that will be called when the user clicks the Done button on the
852
     * title bar.
853
     */
854
    public interface OnDoneButtonClickedListener {
855
        /**
856
         * Called when the user clicks the Done button.
857
         */
858
        void onDoneButtonClicked(PickerFragment<?> fragment);
859
    }
860
 
861
    /**
862
     * Callback interface that will be called to determine if a graph object should be displayed.
863
     *
864
     * @param <T>
865
     */
866
    public interface GraphObjectFilter<T> {
867
        /**
868
         * Called to determine if a graph object should be displayed.
869
         *
870
         * @param graphObject the graph object
871
         * @return true to display the graph object, false to hide it
872
         */
873
        boolean includeItem(T graphObject);
874
    }
875
 
876
    abstract class LoadingStrategy {
877
        protected final static int CACHED_RESULT_REFRESH_DELAY = 2 * 1000;
878
 
879
        protected GraphObjectPagingLoader<T> loader;
880
        protected GraphObjectAdapter<T> adapter;
881
 
882
        public void attach(GraphObjectAdapter<T> adapter) {
883
            loader = (GraphObjectPagingLoader<T>) getLoaderManager().initLoader(0, null,
884
                    new LoaderManager.LoaderCallbacks<SimpleGraphObjectCursor<T>>() {
885
                        @Override
886
                        public Loader<SimpleGraphObjectCursor<T>> onCreateLoader(int id, Bundle args) {
887
                            return LoadingStrategy.this.onCreateLoader();
888
                        }
889
 
890
                        @Override
891
                        public void onLoadFinished(Loader<SimpleGraphObjectCursor<T>> loader,
892
                                SimpleGraphObjectCursor<T> data) {
893
                            if (loader != LoadingStrategy.this.loader) {
894
                                throw new FacebookException("Received callback for unknown loader.");
895
                            }
896
                            LoadingStrategy.this.onLoadFinished((GraphObjectPagingLoader<T>) loader, data);
897
                        }
898
 
899
                        @Override
900
                        public void onLoaderReset(Loader<SimpleGraphObjectCursor<T>> loader) {
901
                            if (loader != LoadingStrategy.this.loader) {
902
                                throw new FacebookException("Received callback for unknown loader.");
903
                            }
904
                            LoadingStrategy.this.onLoadReset((GraphObjectPagingLoader<T>) loader);
905
                        }
906
                    });
907
 
908
            loader.setOnErrorListener(new GraphObjectPagingLoader.OnErrorListener() {
909
                @Override
910
                public void onError(FacebookException error, GraphObjectPagingLoader<?> loader) {
911
                    hideActivityCircle();
912
                    if (onErrorListener != null) {
913
                        onErrorListener.onError(PickerFragment.this, error);
914
                    }
915
                }
916
            });
917
 
918
            this.adapter = adapter;
919
            // Tell the adapter about any data we might already have.
920
            this.adapter.changeCursor(loader.getCursor());
921
            this.adapter.setOnErrorListener(new GraphObjectAdapter.OnErrorListener() {
922
                @Override
923
                public void onError(GraphObjectAdapter<?> adapter, FacebookException error) {
924
                    if (onErrorListener != null) {
925
                        onErrorListener.onError(PickerFragment.this, error);
926
                    }
927
                }
928
            });
929
        }
930
 
931
        public void detach() {
932
            adapter.setDataNeededListener(null);
933
            adapter.setOnErrorListener(null);
934
            loader.setOnErrorListener(null);
935
 
936
            loader = null;
937
            adapter = null;
938
        }
939
 
940
        public void clearResults() {
941
            if (loader != null) {
942
                loader.clearResults();
943
            }
944
        }
945
 
946
        public void startLoading(Request request) {
947
            if (loader != null) {
948
                loader.startLoading(request, canSkipRoundTripIfCached());
949
                onStartLoading(loader, request);
950
            }
951
        }
952
 
953
        public boolean isDataPresentOrLoading() {
954
            return !adapter.isEmpty() || loader.isLoading();
955
        }
956
 
957
        protected GraphObjectPagingLoader<T> onCreateLoader() {
958
            return new GraphObjectPagingLoader<T>(getActivity(), graphObjectClass);
959
        }
960
 
961
        protected void onStartLoading(GraphObjectPagingLoader<T> loader, Request request) {
962
            displayActivityCircle();
963
        }
964
 
965
        protected void onLoadReset(GraphObjectPagingLoader<T> loader) {
966
            adapter.changeCursor(null);
967
        }
968
 
969
        protected void onLoadFinished(GraphObjectPagingLoader<T> loader, SimpleGraphObjectCursor<T> data) {
970
            updateAdapter(data);
971
        }
972
 
973
        protected boolean canSkipRoundTripIfCached() {
974
            return true;
975
        }
976
    }
977
 
978
    abstract class SelectionStrategy {
979
        abstract boolean isSelected(String id);
980
 
981
        abstract void toggleSelection(String id);
982
 
983
        abstract Collection<String> getSelectedIds();
984
 
985
        abstract void clear();
986
 
987
        abstract boolean isEmpty();
988
 
989
        abstract boolean shouldShowCheckBoxIfUnselected();
990
 
991
        abstract void saveSelectionToBundle(Bundle outBundle, String key);
992
 
993
        abstract void readSelectionFromBundle(Bundle inBundle, String key);
994
    }
995
 
996
    class SingleSelectionStrategy extends SelectionStrategy {
997
        private String selectedId;
998
 
999
        public Collection<String> getSelectedIds() {
1000
            return Arrays.asList(new String[]{selectedId});
1001
        }
1002
 
1003
        @Override
1004
        boolean isSelected(String id) {
1005
            return selectedId != null && id != null && selectedId.equals(id);
1006
        }
1007
 
1008
        @Override
1009
        void toggleSelection(String id) {
1010
            if (selectedId != null && selectedId.equals(id)) {
1011
                selectedId = null;
1012
            } else {
1013
                selectedId = id;
1014
            }
1015
        }
1016
 
1017
        @Override
1018
        void saveSelectionToBundle(Bundle outBundle, String key) {
1019
            if (!TextUtils.isEmpty(selectedId)) {
1020
                outBundle.putString(key, selectedId);
1021
            }
1022
        }
1023
 
1024
        @Override
1025
        void readSelectionFromBundle(Bundle inBundle, String key) {
1026
            if (inBundle != null) {
1027
                selectedId = inBundle.getString(key);
1028
            }
1029
        }
1030
 
1031
        @Override
1032
        public void clear() {
1033
            selectedId = null;
1034
        }
1035
 
1036
        @Override
1037
        boolean isEmpty() {
1038
            return selectedId == null;
1039
        }
1040
 
1041
        @Override
1042
        boolean shouldShowCheckBoxIfUnselected() {
1043
            return false;
1044
        }
1045
    }
1046
 
1047
    class MultiSelectionStrategy extends SelectionStrategy {
1048
        private Set<String> selectedIds = new HashSet<String>();
1049
 
1050
        public Collection<String> getSelectedIds() {
1051
            return selectedIds;
1052
        }
1053
 
1054
        @Override
1055
        boolean isSelected(String id) {
1056
            return id != null && selectedIds.contains(id);
1057
        }
1058
 
1059
        @Override
1060
        void toggleSelection(String id) {
1061
            if (id != null) {
1062
                if (selectedIds.contains(id)) {
1063
                    selectedIds.remove(id);
1064
                } else {
1065
                    selectedIds.add(id);
1066
                }
1067
            }
1068
        }
1069
 
1070
        @Override
1071
        void saveSelectionToBundle(Bundle outBundle, String key) {
1072
            if (!selectedIds.isEmpty()) {
1073
                String ids = TextUtils.join(",", selectedIds);
1074
                outBundle.putString(key, ids);
1075
            }
1076
        }
1077
 
1078
        @Override
1079
        void readSelectionFromBundle(Bundle inBundle, String key) {
1080
            if (inBundle != null) {
1081
                String ids = inBundle.getString(key);
1082
                if (ids != null) {
1083
                    String[] splitIds = TextUtils.split(ids, ",");
1084
                    selectedIds.clear();
1085
                    Collections.addAll(selectedIds, splitIds);
1086
                }
1087
            }
1088
        }
1089
 
1090
        @Override
1091
        public void clear() {
1092
            selectedIds.clear();
1093
        }
1094
 
1095
        @Override
1096
        boolean isEmpty() {
1097
            return selectedIds.isEmpty();
1098
        }
1099
 
1100
        @Override
1101
        boolean shouldShowCheckBoxIfUnselected() {
1102
            return true;
1103
        }
1104
    }
1105
 
1106
    abstract class PickerFragmentAdapter<U extends GraphObject> extends GraphObjectAdapter<T> {
1107
        public PickerFragmentAdapter(Context context) {
1108
            super(context);
1109
        }
1110
 
1111
        @Override
1112
        boolean isGraphObjectSelected(String graphObjectId) {
1113
            return selectionStrategy.isSelected(graphObjectId);
1114
        }
1115
 
1116
        @Override
1117
        void updateCheckboxState(CheckBox checkBox, boolean graphObjectSelected) {
1118
            checkBox.setChecked(graphObjectSelected);
1119
            int visible = (graphObjectSelected || selectionStrategy
1120
                    .shouldShowCheckBoxIfUnselected()) ? View.VISIBLE : View.GONE;
1121
            checkBox.setVisibility(visible);
1122
        }
1123
    }
1124
}