/** * Copyright 2012 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.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.text.TextUtils; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.view.animation.AlphaAnimation; import android.widget.*; import com.facebook.*; import com.facebook.android.R; import com.facebook.model.GraphObject; import com.facebook.internal.SessionTracker; import java.util.*; /** * Provides functionality common to SDK UI elements that allow the user to pick one or more * graph objects (e.g., places, friends) from a list of possibilities. The UI is exposed as a * Fragment to allow to it to be included in an Activity along with other Fragments. The Fragments * can be configured by passing parameters as part of their Intent bundle, or (for certain * properties) by specifying attributes in their XML layout files. * <br/> * PickerFragments support callbacks that will be called in the event of an error, when the * underlying data has been changed, or when the set of selected graph objects changes. */ public abstract class PickerFragment<T extends GraphObject> extends Fragment { /** * The key for a boolean parameter in the fragment's Intent bundle to indicate whether the * picker should show pictures (if available) for the graph objects. */ public static final String SHOW_PICTURES_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ShowPictures"; /** * The key for a String parameter in the fragment's Intent bundle to indicate which extra fields * beyond the default fields should be retrieved for any graph objects in the results. */ public static final String EXTRA_FIELDS_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ExtraFields"; /** * The key for a boolean parameter in the fragment's Intent bundle to indicate whether the * picker should display a title bar with a Done button. */ public static final String SHOW_TITLE_BAR_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ShowTitleBar"; /** * The key for a String parameter in the fragment's Intent bundle to indicate the text to * display in the title bar. */ public static final String TITLE_TEXT_BUNDLE_KEY = "com.facebook.widget.PickerFragment.TitleText"; /** * The key for a String parameter in the fragment's Intent bundle to indicate the text to * display in the Done btuton. */ public static final String DONE_BUTTON_TEXT_BUNDLE_KEY = "com.facebook.widget.PickerFragment.DoneButtonText"; private static final String SELECTION_BUNDLE_KEY = "com.facebook.android.PickerFragment.Selection"; private static final String ACTIVITY_CIRCLE_SHOW_KEY = "com.facebook.android.PickerFragment.ActivityCircleShown"; private static final int PROFILE_PICTURE_PREFETCH_BUFFER = 5; private final int layout; private OnErrorListener onErrorListener; private OnDataChangedListener onDataChangedListener; private OnSelectionChangedListener onSelectionChangedListener; private OnDoneButtonClickedListener onDoneButtonClickedListener; private GraphObjectFilter<T> filter; private boolean showPictures = true; private boolean showTitleBar = true; private ListView listView; HashSet<String> extraFields = new HashSet<String>(); GraphObjectAdapter<T> adapter; private final Class<T> graphObjectClass; private LoadingStrategy loadingStrategy; private SelectionStrategy selectionStrategy; private ProgressBar activityCircle; private SessionTracker sessionTracker; private String titleText; private String doneButtonText; private TextView titleTextView; private Button doneButton; private Drawable titleBarBackground; private Drawable doneButtonBackground; PickerFragment(Class<T> graphObjectClass, int layout, Bundle args) { this.graphObjectClass = graphObjectClass; this.layout = layout; setPickerFragmentSettingsFromBundle(args); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); adapter = createAdapter(); adapter.setFilter(new GraphObjectAdapter.Filter<T>() { @Override public boolean includeItem(T graphObject) { return filterIncludesItem(graphObject); } }); } @Override public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) { super.onInflate(activity, attrs, savedInstanceState); TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.com_facebook_picker_fragment); setShowPictures(a.getBoolean(R.styleable.com_facebook_picker_fragment_show_pictures, showPictures)); String extraFieldsString = a.getString(R.styleable.com_facebook_picker_fragment_extra_fields); if (extraFieldsString != null) { String[] strings = extraFieldsString.split(","); setExtraFields(Arrays.asList(strings)); } showTitleBar = a.getBoolean(R.styleable.com_facebook_picker_fragment_show_title_bar, showTitleBar); titleText = a.getString(R.styleable.com_facebook_picker_fragment_title_text); doneButtonText = a.getString(R.styleable.com_facebook_picker_fragment_done_button_text); titleBarBackground = a.getDrawable(R.styleable.com_facebook_picker_fragment_title_bar_background); doneButtonBackground = a.getDrawable(R.styleable.com_facebook_picker_fragment_done_button_background); a.recycle(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ViewGroup view = (ViewGroup) inflater.inflate(layout, container, false); listView = (ListView) view.findViewById(R.id.com_facebook_picker_list_view); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View v, int position, long id) { onListItemClick((ListView) parent, v, position); } }); listView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { // We don't actually do anything differently on long-clicks, but setting the listener // enables the selector transition that we have for visual consistency with the // Facebook app's pickers. return false; } }); listView.setOnScrollListener(onScrollListener); listView.setAdapter(adapter); activityCircle = (ProgressBar) view.findViewById(R.id.com_facebook_picker_activity_circle); return view; } @Override public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); sessionTracker = new SessionTracker(getActivity(), new Session.StatusCallback() { @Override public void call(Session session, SessionState state, Exception exception) { if (!session.isOpened()) { // When a session is closed, we want to clear out our data so it is not visible to subsequent users clearResults(); } } }); setSettingsFromBundle(savedInstanceState); loadingStrategy = createLoadingStrategy(); loadingStrategy.attach(adapter); selectionStrategy = createSelectionStrategy(); selectionStrategy.readSelectionFromBundle(savedInstanceState, SELECTION_BUNDLE_KEY); // Should we display a title bar? (We need to do this after we've retrieved our bundle settings.) if (showTitleBar) { inflateTitleBar((ViewGroup) getView()); } if (activityCircle != null && savedInstanceState != null) { boolean shown = savedInstanceState.getBoolean(ACTIVITY_CIRCLE_SHOW_KEY, false); if (shown) { displayActivityCircle(); } else { // Should be hidden already, but just to be sure. hideActivityCircle(); } } } @Override public void onDetach() { super.onDetach(); listView.setOnScrollListener(null); listView.setAdapter(null); loadingStrategy.detach(); sessionTracker.stopTracking(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); saveSettingsToBundle(outState); selectionStrategy.saveSelectionToBundle(outState, SELECTION_BUNDLE_KEY); if (activityCircle != null) { outState.putBoolean(ACTIVITY_CIRCLE_SHOW_KEY, activityCircle.getVisibility() == View.VISIBLE); } } @Override public void setArguments(Bundle args) { super.setArguments(args); setSettingsFromBundle(args); } /** * Gets the current OnDataChangedListener for this fragment, which will be called whenever * the underlying data being displaying in the picker has changed. * * @return the OnDataChangedListener, or null if there is none */ public OnDataChangedListener getOnDataChangedListener() { return onDataChangedListener; } /** * Sets the current OnDataChangedListener for this fragment, which will be called whenever * the underlying data being displaying in the picker has changed. * * @param onDataChangedListener the OnDataChangedListener, or null if there is none */ public void setOnDataChangedListener(OnDataChangedListener onDataChangedListener) { this.onDataChangedListener = onDataChangedListener; } /** * Gets the current OnSelectionChangedListener for this fragment, which will be called * whenever the user selects or unselects a graph object in the list. * * @return the OnSelectionChangedListener, or null if there is none */ public OnSelectionChangedListener getOnSelectionChangedListener() { return onSelectionChangedListener; } /** * Sets the current OnSelectionChangedListener for this fragment, which will be called * whenever the user selects or unselects a graph object in the list. * * @param onSelectionChangedListener the OnSelectionChangedListener, or null if there is none */ public void setOnSelectionChangedListener( OnSelectionChangedListener onSelectionChangedListener) { this.onSelectionChangedListener = onSelectionChangedListener; } /** * Gets the current OnDoneButtonClickedListener for this fragment, which will be called * when the user clicks the Done button. * * @return the OnDoneButtonClickedListener, or null if there is none */ public OnDoneButtonClickedListener getOnDoneButtonClickedListener() { return onDoneButtonClickedListener; } /** * Sets the current OnDoneButtonClickedListener for this fragment, which will be called * when the user clicks the Done button. This will only be possible if the title bar is * being shown in this fragment. * * @param onDoneButtonClickedListener the OnDoneButtonClickedListener, or null if there is none */ public void setOnDoneButtonClickedListener(OnDoneButtonClickedListener onDoneButtonClickedListener) { this.onDoneButtonClickedListener = onDoneButtonClickedListener; } /** * Gets the current OnErrorListener for this fragment, which will be called in the event * of network or other errors encountered while populating the graph objects in the list. * * @return the OnErrorListener, or null if there is none */ public OnErrorListener getOnErrorListener() { return onErrorListener; } /** * Sets the current OnErrorListener for this fragment, which will be called in the event * of network or other errors encountered while populating the graph objects in the list. * * @param onErrorListener the OnErrorListener, or null if there is none */ public void setOnErrorListener(OnErrorListener onErrorListener) { this.onErrorListener = onErrorListener; } /** * Gets the current filter for this fragment, which will be called for each graph object * returned from the service to determine if it should be displayed in the list. * If no filter is specified, all retrieved graph objects will be displayed. * * @return the GraphObjectFilter, or null if there is none */ public GraphObjectFilter<T> getFilter() { return filter; } /** * Sets the current filter for this fragment, which will be called for each graph object * returned from the service to determine if it should be displayed in the list. * If no filter is specified, all retrieved graph objects will be displayed. * * @param filter the GraphObjectFilter, or null if there is none */ public void setFilter(GraphObjectFilter<T> filter) { this.filter = filter; } /** * Gets the Session to use for any Facebook requests this fragment will make. * * @return the Session that will be used for any Facebook requests, or null if there is none */ public Session getSession() { return sessionTracker.getSession(); } /** * Sets the Session to use for any Facebook requests this fragment will make. If the * parameter is null, the fragment will use the current active session, if any. * * @param session the Session to use for Facebook requests, or null to use the active session */ public void setSession(Session session) { sessionTracker.setSession(session); } /** * Gets whether to display pictures, if available, for displayed graph objects. * * @return true if pictures should be displayed, false if not */ public boolean getShowPictures() { return showPictures; } /** * Sets whether to display pictures, if available, for displayed graph objects. * * @param showPictures true if pictures should be displayed, false if not */ public void setShowPictures(boolean showPictures) { this.showPictures = showPictures; } /** * Gets the extra fields to request for the retrieved graph objects. * * @return the extra fields to request */ public Set<String> getExtraFields() { return new HashSet<String>(extraFields); } /** * Sets the extra fields to request for the retrieved graph objects. * * @param fields the extra fields to request */ public void setExtraFields(Collection<String> fields) { extraFields = new HashSet<String>(); if (fields != null) { extraFields.addAll(fields); } } /** * Sets whether to show a title bar with a Done button. This must be * called prior to the Fragment going through its creation lifecycle to have an effect. * * @param showTitleBar true if a title bar should be displayed, false if not */ public void setShowTitleBar(boolean showTitleBar) { this.showTitleBar = showTitleBar; } /** * Gets whether to show a title bar with a Done button. The default is true. * * @return true if a title bar will be shown, false if not. */ public boolean getShowTitleBar() { return showTitleBar; } /** * Sets the text to show in the title bar, if a title bar is to be shown. This must be * called prior to the Fragment going through its creation lifecycle to have an effect, or * the default will be used. * * @param titleText the text to show in the title bar */ public void setTitleText(String titleText) { this.titleText = titleText; } /** * Gets the text to show in the title bar, if a title bar is to be shown. * * @return the text to show in the title bar */ public String getTitleText() { if (titleText == null) { titleText = getDefaultTitleText(); } return titleText; } /** * Sets the text to show in the Done button, if a title bar is to be shown. This must be * called prior to the Fragment going through its creation lifecycle to have an effect, or * the default will be used. * * @param doneButtonText the text to show in the Done button */ public void setDoneButtonText(String doneButtonText) { this.doneButtonText = doneButtonText; } /** * Gets the text to show in the Done button, if a title bar is to be shown. * * @return the text to show in the Done button */ public String getDoneButtonText() { if (doneButtonText == null) { doneButtonText = getDefaultDoneButtonText(); } return doneButtonText; } /** * Causes the picker to load data from the service and display it to the user. * * @param forceReload if true, data will be loaded even if there is already data being displayed (or loading); * if false, data will not be re-loaded if it is already displayed (or loading) */ public void loadData(boolean forceReload) { if (!forceReload && loadingStrategy.isDataPresentOrLoading()) { return; } loadDataSkippingRoundTripIfCached(); } /** * Updates the properties of the PickerFragment based on the contents of the supplied Bundle; * calling Activities may use this to pass additional configuration information to the * PickerFragment beyond what is specified in its XML layout. * * @param inState a Bundle containing keys corresponding to properties of the PickerFragment */ public void setSettingsFromBundle(Bundle inState) { setPickerFragmentSettingsFromBundle(inState); } boolean filterIncludesItem(T graphObject) { if (filter != null) { return filter.includeItem(graphObject); } return true; } List<T> getSelectedGraphObjects() { return adapter.getGraphObjectsById(selectionStrategy.getSelectedIds()); } void saveSettingsToBundle(Bundle outState) { outState.putBoolean(SHOW_PICTURES_BUNDLE_KEY, showPictures); if (!extraFields.isEmpty()) { outState.putString(EXTRA_FIELDS_BUNDLE_KEY, TextUtils.join(",", extraFields)); } outState.putBoolean(SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar); outState.putString(TITLE_TEXT_BUNDLE_KEY, titleText); outState.putString(DONE_BUTTON_TEXT_BUNDLE_KEY, doneButtonText); } abstract Request getRequestForLoadData(Session session); abstract PickerFragmentAdapter<T> createAdapter(); abstract LoadingStrategy createLoadingStrategy(); abstract SelectionStrategy createSelectionStrategy(); void onLoadingData() { } String getDefaultTitleText() { return null; } String getDefaultDoneButtonText() { return getString(R.string.com_facebook_picker_done_button_text); } void displayActivityCircle() { if (activityCircle != null) { layoutActivityCircle(); activityCircle.setVisibility(View.VISIBLE); } } void layoutActivityCircle() { // If we've got no data, make the activity circle full-opacity. Otherwise we'll dim it to avoid // cluttering the UI. float alpha = (!adapter.isEmpty()) ? .25f : 1.0f; setAlpha(activityCircle, alpha); } void hideActivityCircle() { if (activityCircle != null) { // We use an animation to dim the activity circle; need to clear this or it will remain visible. activityCircle.clearAnimation(); activityCircle.setVisibility(View.INVISIBLE); } } void setSelectionStrategy(SelectionStrategy selectionStrategy) { if (selectionStrategy != this.selectionStrategy) { this.selectionStrategy = selectionStrategy; if (adapter != null) { // Adapter should cause a re-render. adapter.notifyDataSetChanged(); } } } private static void setAlpha(View view, float alpha) { // Set the alpha appropriately (setAlpha is API >= 11, this technique works on all API levels). AlphaAnimation alphaAnimation = new AlphaAnimation(alpha, alpha); alphaAnimation.setDuration(0); alphaAnimation.setFillAfter(true); view.startAnimation(alphaAnimation); } private void setPickerFragmentSettingsFromBundle(Bundle inState) { // We do this in a separate non-overridable method so it is safe to call from the constructor. if (inState != null) { showPictures = inState.getBoolean(SHOW_PICTURES_BUNDLE_KEY, showPictures); String extraFieldsString = inState.getString(EXTRA_FIELDS_BUNDLE_KEY); if (extraFieldsString != null) { String[] strings = extraFieldsString.split(","); setExtraFields(Arrays.asList(strings)); } showTitleBar = inState.getBoolean(SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar); String titleTextString = inState.getString(TITLE_TEXT_BUNDLE_KEY); if (titleTextString != null) { titleText = titleTextString; if (titleTextView != null) { titleTextView.setText(titleText); } } String doneButtonTextString = inState.getString(DONE_BUTTON_TEXT_BUNDLE_KEY); if (doneButtonTextString != null) { doneButtonText = doneButtonTextString; if (doneButton != null) { doneButton.setText(doneButtonText); } } } } private void inflateTitleBar(ViewGroup view) { ViewStub stub = (ViewStub) view.findViewById(R.id.com_facebook_picker_title_bar_stub); if (stub != null) { View titleBar = stub.inflate(); final RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.FILL_PARENT, RelativeLayout.LayoutParams.FILL_PARENT); layoutParams.addRule(RelativeLayout.BELOW, R.id.com_facebook_picker_title_bar); listView.setLayoutParams(layoutParams); if (titleBarBackground != null) { titleBar.setBackgroundDrawable(titleBarBackground); } doneButton = (Button) view.findViewById(R.id.com_facebook_picker_done_button); if (doneButton != null) { doneButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (onDoneButtonClickedListener != null) { onDoneButtonClickedListener.onDoneButtonClicked(PickerFragment.this); } } }); if (getDoneButtonText() != null) { doneButton.setText(getDoneButtonText()); } if (doneButtonBackground != null) { doneButton.setBackgroundDrawable(doneButtonBackground); } } titleTextView = (TextView) view.findViewById(R.id.com_facebook_picker_title); if (titleTextView != null) { if (getTitleText() != null) { titleTextView.setText(getTitleText()); } } } } private void onListItemClick(ListView listView, View v, int position) { @SuppressWarnings("unchecked") T graphObject = (T) listView.getItemAtPosition(position); String id = adapter.getIdOfGraphObject(graphObject); selectionStrategy.toggleSelection(id); adapter.notifyDataSetChanged(); if (onSelectionChangedListener != null) { onSelectionChangedListener.onSelectionChanged(PickerFragment.this); } } private void loadDataSkippingRoundTripIfCached() { clearResults(); Request request = getRequestForLoadData(getSession()); if (request != null) { onLoadingData(); loadingStrategy.startLoading(request); } } private void clearResults() { if (adapter != null) { boolean wasSelection = !selectionStrategy.isEmpty(); boolean wasData = !adapter.isEmpty(); loadingStrategy.clearResults(); selectionStrategy.clear(); adapter.notifyDataSetChanged(); // Tell anyone who cares the data and selection has changed, if they have. if (wasData && onDataChangedListener != null) { onDataChangedListener.onDataChanged(PickerFragment.this); } if (wasSelection && onSelectionChangedListener != null) { onSelectionChangedListener.onSelectionChanged(PickerFragment.this); } } } void updateAdapter(SimpleGraphObjectCursor<T> data) { if (adapter != null) { // As we fetch additional results and add them to the table, we do not // want the items displayed jumping around seemingly at random, frustrating the user's // attempts at scrolling, etc. Since results may be added anywhere in // the table, we choose to try to keep the first visible row in a fixed // position (from the user's perspective). We try to keep it positioned at // the same offset from the top of the screen so adding new items seems // smoother, as opposed to having it "snap" to a multiple of row height // We use the second row, to give context above and below it and avoid // cases where the first row is only barely visible, thus providing little context. // The exception is where the very first row is visible, in which case we use that. View view = listView.getChildAt(1); int anchorPosition = listView.getFirstVisiblePosition(); if (anchorPosition > 0) { anchorPosition++; } GraphObjectAdapter.SectionAndItem<T> anchorItem = adapter.getSectionAndItem(anchorPosition); final int top = (view != null && anchorItem.getType() != GraphObjectAdapter.SectionAndItem.Type.ACTIVITY_CIRCLE) ? view.getTop() : 0; // Now actually add the results. boolean dataChanged = adapter.changeCursor(data); if (view != null && anchorItem != null) { // Put the item back in the same spot it was. final int newPositionOfItem = adapter.getPosition(anchorItem.sectionKey, anchorItem.graphObject); if (newPositionOfItem != -1) { listView.setSelectionFromTop(newPositionOfItem, top); } } if (dataChanged && onDataChangedListener != null) { onDataChangedListener.onDataChanged(PickerFragment.this); } } } private void reprioritizeDownloads() { int lastVisibleItem = listView.getLastVisiblePosition(); if (lastVisibleItem >= 0) { int firstVisibleItem = listView.getFirstVisiblePosition(); adapter.prioritizeViewRange(firstVisibleItem, lastVisibleItem, PROFILE_PICTURE_PREFETCH_BUFFER); } } private ListView.OnScrollListener onScrollListener = new ListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { reprioritizeDownloads(); } }; /** * Callback interface that will be called when a network or other error is encountered * while retrieving graph objects. */ public interface OnErrorListener { /** * Called when a network or other error is encountered. * * @param error a FacebookException representing the error that was encountered. */ void onError(PickerFragment<?> fragment, FacebookException error); } /** * Callback interface that will be called when the underlying data being displayed in the * picker has been updated. */ public interface OnDataChangedListener { /** * Called when the set of data being displayed in the picker has changed. */ void onDataChanged(PickerFragment<?> fragment); } /** * Callback interface that will be called when the user selects or unselects graph objects * in the picker. */ public interface OnSelectionChangedListener { /** * Called when the user selects or unselects graph objects in the picker. */ void onSelectionChanged(PickerFragment<?> fragment); } /** * Callback interface that will be called when the user clicks the Done button on the * title bar. */ public interface OnDoneButtonClickedListener { /** * Called when the user clicks the Done button. */ void onDoneButtonClicked(PickerFragment<?> fragment); } /** * Callback interface that will be called to determine if a graph object should be displayed. * * @param <T> */ public interface GraphObjectFilter<T> { /** * Called to determine if a graph object should be displayed. * * @param graphObject the graph object * @return true to display the graph object, false to hide it */ boolean includeItem(T graphObject); } abstract class LoadingStrategy { protected final static int CACHED_RESULT_REFRESH_DELAY = 2 * 1000; protected GraphObjectPagingLoader<T> loader; protected GraphObjectAdapter<T> adapter; public void attach(GraphObjectAdapter<T> adapter) { loader = (GraphObjectPagingLoader<T>) getLoaderManager().initLoader(0, null, new LoaderManager.LoaderCallbacks<SimpleGraphObjectCursor<T>>() { @Override public Loader<SimpleGraphObjectCursor<T>> onCreateLoader(int id, Bundle args) { return LoadingStrategy.this.onCreateLoader(); } @Override public void onLoadFinished(Loader<SimpleGraphObjectCursor<T>> loader, SimpleGraphObjectCursor<T> data) { if (loader != LoadingStrategy.this.loader) { throw new FacebookException("Received callback for unknown loader."); } LoadingStrategy.this.onLoadFinished((GraphObjectPagingLoader<T>) loader, data); } @Override public void onLoaderReset(Loader<SimpleGraphObjectCursor<T>> loader) { if (loader != LoadingStrategy.this.loader) { throw new FacebookException("Received callback for unknown loader."); } LoadingStrategy.this.onLoadReset((GraphObjectPagingLoader<T>) loader); } }); loader.setOnErrorListener(new GraphObjectPagingLoader.OnErrorListener() { @Override public void onError(FacebookException error, GraphObjectPagingLoader<?> loader) { hideActivityCircle(); if (onErrorListener != null) { onErrorListener.onError(PickerFragment.this, error); } } }); this.adapter = adapter; // Tell the adapter about any data we might already have. this.adapter.changeCursor(loader.getCursor()); } public void detach() { adapter.setDataNeededListener(null); loader.setOnErrorListener(null); loader = null; adapter = null; } public void clearResults() { if (loader != null) { loader.clearResults(); } } public void startLoading(Request request) { if (loader != null) { loader.startLoading(request, true); onStartLoading(loader, request); } } public boolean isDataPresentOrLoading() { return !adapter.isEmpty() || loader.isLoading(); } protected GraphObjectPagingLoader<T> onCreateLoader() { return new GraphObjectPagingLoader<T>(getActivity(), graphObjectClass); } protected void onStartLoading(GraphObjectPagingLoader<T> loader, Request request) { displayActivityCircle(); } protected void onLoadReset(GraphObjectPagingLoader<T> loader) { adapter.changeCursor(null); } protected void onLoadFinished(GraphObjectPagingLoader<T> loader, SimpleGraphObjectCursor<T> data) { updateAdapter(data); } } abstract class SelectionStrategy { abstract boolean isSelected(String id); abstract void toggleSelection(String id); abstract Collection<String> getSelectedIds(); abstract void clear(); abstract boolean isEmpty(); abstract boolean shouldShowCheckBoxIfUnselected(); abstract void saveSelectionToBundle(Bundle outBundle, String key); abstract void readSelectionFromBundle(Bundle inBundle, String key); } class SingleSelectionStrategy extends SelectionStrategy { private String selectedId; public Collection<String> getSelectedIds() { return Arrays.asList(new String[]{selectedId}); } @Override boolean isSelected(String id) { return selectedId != null && id != null && selectedId.equals(id); } @Override void toggleSelection(String id) { if (selectedId != null && selectedId.equals(id)) { selectedId = null; } else { selectedId = id; } } @Override void saveSelectionToBundle(Bundle outBundle, String key) { if (!TextUtils.isEmpty(selectedId)) { outBundle.putString(key, selectedId); } } @Override void readSelectionFromBundle(Bundle inBundle, String key) { if (inBundle != null) { selectedId = inBundle.getString(key); } } @Override public void clear() { selectedId = null; } @Override boolean isEmpty() { return selectedId == null; } @Override boolean shouldShowCheckBoxIfUnselected() { return false; } } class MultiSelectionStrategy extends SelectionStrategy { private Set<String> selectedIds = new HashSet<String>(); public Collection<String> getSelectedIds() { return selectedIds; } @Override boolean isSelected(String id) { return id != null && selectedIds.contains(id); } @Override void toggleSelection(String id) { if (id != null) { if (selectedIds.contains(id)) { selectedIds.remove(id); } else { selectedIds.add(id); } } } @Override void saveSelectionToBundle(Bundle outBundle, String key) { if (!selectedIds.isEmpty()) { String ids = TextUtils.join(",", selectedIds); outBundle.putString(key, ids); } } @Override void readSelectionFromBundle(Bundle inBundle, String key) { if (inBundle != null) { String ids = inBundle.getString(key); if (ids != null) { String[] splitIds = TextUtils.split(ids, ","); selectedIds.clear(); Collections.addAll(selectedIds, splitIds); } } } @Override public void clear() { selectedIds.clear(); } @Override boolean isEmpty() { return selectedIds.isEmpty(); } @Override boolean shouldShowCheckBoxIfUnselected() { return true; } } abstract class PickerFragmentAdapter<U extends GraphObject> extends GraphObjectAdapter<T> { public PickerFragmentAdapter(Context context) { super(context); } @Override boolean isGraphObjectSelected(String graphObjectId) { return selectionStrategy.isSelected(graphObjectId); } @Override void updateCheckboxState(CheckBox checkBox, boolean graphObjectSelected) { checkBox.setChecked(graphObjectSelected); int visible = (graphObjectSelected || selectionStrategy .shouldShowCheckBoxIfUnselected()) ? View.VISIBLE : View.GONE; checkBox.setVisibility(visible); } } }