| 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 |
}
|