blob: 157100e295c3167eee3edb737730dfbe94a81fc8 [file] [log] [blame]
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -07001/*
2 * Copyright (C) 2009 The Android Open Source Project
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
17package com.android.contacts;
18
Jeff Sharkey3f177592009-05-18 15:23:12 -070019import com.android.contacts.EdgeTriggerView.EdgeTriggerListener;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -070020import com.android.contacts.SocialStreamActivity.MappingCache.Mapping;
Jeff Sharkey3f177592009-05-18 15:23:12 -070021import com.android.providers.contacts2.ContactsContract;
22import com.android.providers.contacts2.ContactsContract.Aggregates;
23import com.android.providers.contacts2.ContactsContract.Contacts;
24import com.android.providers.contacts2.ContactsContract.Data;
25import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Photo;
Jeff Sharkey3f177592009-05-18 15:23:12 -070026import com.android.providers.contacts2.SocialContract.Activities;
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -070027
Jeff Sharkey3f177592009-05-18 15:23:12 -070028import org.xmlpull.v1.XmlPullParser;
29import org.xmlpull.v1.XmlPullParserException;
30
31import android.app.ListActivity;
32import android.content.ContentResolver;
33import android.content.ContentUris;
34import android.content.Context;
35import android.content.pm.ApplicationInfo;
36import android.content.pm.PackageManager;
37import android.content.pm.PackageManager.NameNotFoundException;
Jeff Sharkey3f177592009-05-18 15:23:12 -070038import android.content.res.TypedArray;
39import android.database.Cursor;
40import android.graphics.Bitmap;
41import android.graphics.BitmapFactory;
42import android.graphics.Canvas;
43import android.graphics.Paint;
44import android.graphics.PaintFlagsDrawFilter;
Jeff Sharkey3f177592009-05-18 15:23:12 -070045import android.graphics.Rect;
Jeff Sharkey3f177592009-05-18 15:23:12 -070046import android.net.Uri;
47import android.os.Bundle;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -070048import android.text.SpannableStringBuilder;
Jeff Sharkey3f177592009-05-18 15:23:12 -070049import android.text.format.DateUtils;
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -070050import android.text.style.StyleSpan;
Jeff Sharkey3f177592009-05-18 15:23:12 -070051import android.util.AttributeSet;
52import android.util.Log;
53import android.util.Xml;
54import android.view.LayoutInflater;
55import android.view.View;
56import android.view.ViewGroup;
Jeff Sharkey3f177592009-05-18 15:23:12 -070057import android.widget.CursorAdapter;
58import android.widget.ImageView;
59import android.widget.ListAdapter;
Jeff Sharkey3f177592009-05-18 15:23:12 -070060import android.widget.TextView;
61
62import java.io.IOException;
63import java.util.HashMap;
64import java.util.LinkedList;
65import java.util.List;
66
67public class SocialStreamActivity extends ListActivity implements EdgeTriggerListener {
68 private static final String TAG = "SocialStreamActivity";
69
70 private static final String[] PROJ_ACTIVITIES = new String[] {
71 Activities._ID,
72 Activities.PACKAGE,
73 Activities.MIMETYPE,
74 Activities.AUTHOR_CONTACT_ID,
75 Contacts.AGGREGATE_ID,
76 Aggregates.DISPLAY_NAME,
77 Activities.PUBLISHED,
78 Activities.TITLE,
79 Activities.SUMMARY,
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -070080 Activities.THREAD_PUBLISHED,
Jeff Sharkey3f177592009-05-18 15:23:12 -070081 };
82
83 private static final int COL_ID = 0;
84 private static final int COL_PACKAGE = 1;
85 private static final int COL_MIMETYPE = 2;
86 private static final int COL_AUTHOR_CONTACT_ID = 3;
87 private static final int COL_AGGREGATE_ID = 4;
88 private static final int COL_DISPLAY_NAME = 5;
89 private static final int COL_PUBLISHED = 6;
90 private static final int COL_TITLE = 7;
91 private static final int COL_SUMMARY = 8;
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -070092 private static final int COL_THREAD_PUBLISHED = 9;
Jeff Sharkey3f177592009-05-18 15:23:12 -070093
94 public static final int PHOTO_SIZE = 58;
95
96 private ListAdapter mAdapter;
97
98 private FloatyListView mListView;
99 private EdgeTriggerView mEdgeTrigger;
100 private FastTrackWindow mFastTrack;
101
102 private ContactsCache mContactsCache;
103 private MappingCache mMappingCache;
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700104
105 @Override
106 protected void onCreate(Bundle icicle) {
107 super.onCreate(icicle);
108
Jeff Sharkey3f177592009-05-18 15:23:12 -0700109 setContentView(R.layout.social_list);
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700110
Jeff Sharkey3f177592009-05-18 15:23:12 -0700111 mContactsCache = new ContactsCache(this);
112 mMappingCache = MappingCache.createAndFill(this);
113
114 Cursor cursor = managedQuery(Activities.CONTENT_URI, PROJ_ACTIVITIES, null, null);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700115 mAdapter = new SocialAdapter(this, cursor, mContactsCache, mMappingCache);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700116
117 setListAdapter(mAdapter);
118
119 mListView = (FloatyListView)findViewById(android.R.id.list);
120
121 // Find and listen for edge triggers
122 mEdgeTrigger = (EdgeTriggerView)findViewById(R.id.edge_trigger);
123 mEdgeTrigger.setOnEdgeTriggerListener(this);
124 }
125
126 /** {@inheritDoc} */
127 public void onTrigger(float downX, float downY, int edge) {
128 // Find list item user triggered over
129 int position = mListView.pointToPosition((int)downX, (int)downY);
130
131 Cursor cursor = (Cursor)mAdapter.getItem(position);
132 long aggId = cursor.getLong(COL_AGGREGATE_ID);
133
134 Log.d(TAG, "onTrigger found position=" + position + ", contactId=" + aggId);
135
136 Uri aggUri = ContentUris.withAppendedId(ContactsContract.Aggregates.CONTENT_URI, aggId);
137
138 // Dismiss any existing window first
139 if (mFastTrack != null) {
140 mFastTrack.dismiss();
141 }
142
143 mFastTrack = new FastTrackWindow(this, mListView, aggUri, mMappingCache);
144 mListView.setFloatyWindow(mFastTrack, position);
145
146 }
147
148 /**
149 * List adapter for social stream data queried from
150 * {@link Activities#CONTENT_URI}.
151 */
152 private static class SocialAdapter extends CursorAdapter {
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700153 private final Context mContext;
154 private final LayoutInflater mInflater;
155 private final ContactsCache mContactsCache;
156 private final MappingCache mMappingCache;
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700157 private final StyleSpan mTextStyleName;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700158
159 private static class SocialHolder {
160 ImageView photo;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700161 ImageView sourceIcon;
162 TextView content;
163 SpannableStringBuilder contentBuilder = new SpannableStringBuilder();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700164 TextView published;
165 }
166
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700167 public SocialAdapter(Context context, Cursor c, ContactsCache contactsCache,
168 MappingCache mappingCache) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700169 super(context, c, true);
170 mContext = context;
171 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
172 mContactsCache = contactsCache;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700173 mMappingCache = mappingCache;
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700174 mTextStyleName = new StyleSpan(android.graphics.Typeface.BOLD);
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700175 }
176
177 @Override
178 public int getViewTypeCount() {
179 return 2;
180 }
181
182 @Override
183 public int getItemViewType(int position) {
184 Cursor cursor = (Cursor) getItem(position);
185 return isReply(cursor) ? 0 : 1;
186 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700187
188 @Override
189 public void bindView(View view, Context context, Cursor cursor) {
190 SocialHolder holder = (SocialHolder)view.getTag();
191
192 long contactId = cursor.getLong(COL_AUTHOR_CONTACT_ID);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700193 String name = cursor.getString(COL_DISPLAY_NAME);
194 String title = cursor.getString(COL_TITLE);
195 long published = cursor.getLong(COL_PUBLISHED);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700196
197 // TODO: trigger async query to find actual name and photo instead
198 // of using this lazy caching mechanism
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700199 Bitmap photo = mContactsCache.getPhoto(contactId);
200 if (photo != null) {
201 holder.photo.setImageBitmap(photo);
202 } else {
203 holder.photo.setImageResource(R.drawable.ic_contact_list_picture);
204 }
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700205 holder.contentBuilder.clear();
206 holder.contentBuilder.append(name);
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700207 holder.contentBuilder.append(" ");
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700208 holder.contentBuilder.append(title);
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700209 holder.contentBuilder.setSpan(mTextStyleName, 0, name.length(), 0);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700210 holder.content.setText(holder.contentBuilder);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700211
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700212 CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published,
213 System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700214 holder.published.setText(relativePublished);
215
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700216 if (holder.sourceIcon != null) {
217 String packageName = cursor.getString(COL_PACKAGE);
218 String mimeType = cursor.getString(COL_MIMETYPE);
219 Mapping mapping = mMappingCache.getMapping(packageName, mimeType);
220 if (mapping != null && mapping.icon != null) {
221 holder.sourceIcon.setImageBitmap(mapping.icon);
222 } else {
223 holder.sourceIcon.setImageDrawable(null);
224 }
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700225 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700226 }
227
228 @Override
229 public View newView(Context context, Cursor cursor, ViewGroup parent) {
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700230 View view = mInflater.inflate(
231 isReply(cursor) ? R.layout.social_list_item_reply : R.layout.social_list_item,
232 parent, false);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700233
234 SocialHolder holder = new SocialHolder();
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700235 holder.photo = (ImageView) view.findViewById(R.id.photo);
236 holder.sourceIcon = (ImageView) view.findViewById(R.id.sourceIcon);
237 holder.content = (TextView) view.findViewById(R.id.content);
238 holder.published = (TextView) view.findViewById(R.id.published);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700239 view.setTag(holder);
240
241 return view;
242 }
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700243
244 private boolean isReply(Cursor cursor) {
245
246 /*
247 * Comparing the message timestamp to the thread timestamp rather than checking the
248 * in_reply_to field. The rationale for this approach is that in the case when the
249 * original message to which the reply was posted is missing, we want to display
250 * the message as if it was an original; otherwise it would appear to be a reply
251 * to whatever message preceded it in the list. In the case when the original message
252 * of the thread is missing, the two timestamps will be the same.
253 */
254 long published = cursor.getLong(COL_PUBLISHED);
255 long threadPublished = cursor.getLong(COL_THREAD_PUBLISHED);
256 return published != threadPublished;
257 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700258 }
259
260 /**
261 * Keep a cache that maps from {@link Contacts#_ID} to {@link Photo#PHOTO}
262 * values.
263 */
264 private static class ContactsCache {
265 private static final String TAG = "ContactsCache";
266
267 private static final String[] PROJ_DETAILS = new String[] {
268 Data.MIMETYPE,
269 Data.CONTACT_ID,
270 Photo.PHOTO,
271 };
272
273 private static final int COL_MIMETYPE = 0;
274 private static final int COL_CONTACT_ID = 1;
Jeff Sharkey8da253a2009-05-18 21:23:19 -0700275 private static final int COL_PHOTO = 2;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700276
277 private HashMap<Long, Bitmap> mPhoto = new HashMap<Long, Bitmap>();
278
279 public ContactsCache(Context context) {
280 Log.d(TAG, "building ContactsCache...");
281
282 ContentResolver resolver = context.getContentResolver();
283 Cursor cursor = resolver.query(Data.CONTENT_URI, PROJ_DETAILS,
284 Data.MIMETYPE + "=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
285
286 while (cursor.moveToNext()) {
287 long contactId = cursor.getLong(COL_CONTACT_ID);
288 String mimeType = cursor.getString(COL_MIMETYPE);
289 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
290 byte[] photoBlob = cursor.getBlob(COL_PHOTO);
291 Bitmap photo = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
292 photo = Utilities.createBitmapThumbnail(photo, context, PHOTO_SIZE);
293
294 mPhoto.put(contactId, photo);
295 }
296 }
297
298 cursor.close();
299 Log.d(TAG, "done building ContactsCache");
300 }
301
302 public Bitmap getPhoto(long contactId) {
303 return mPhoto.get(contactId);
304 }
305 }
306
307 /**
308 * Store a parsed <code>RemoteViewsMapping</code> object, which maps
309 * mime-types to <code>RemoteViews</code> XML resources and possible icons.
310 */
311 public static class MappingCache {
312 private static final String TAG = "MappingCache";
313
314 private static final String TAG_MAPPINGSET = "MappingSet";
315 private static final String TAG_MAPPING = "Mapping";
316
317 private static final String MAPPING_METADATA = "com.android.contacts.stylemap";
318
319 private LinkedList<Mapping> mappings = new LinkedList<Mapping>();
320
321 private MappingCache() {
322 }
323
324 public static class Mapping {
325 String packageName;
326 String mimeType;
327 int remoteViewsRes;
328 Bitmap icon;
329 }
330
331 public void addMapping(Mapping mapping) {
332 mappings.add(mapping);
333 }
334
335 /**
336 * Find matching <code>RemoteViews</code> XML resource for requested
337 * package and mime-type. Returns -1 if no mapping found.
338 */
339 public Mapping getMapping(String packageName, String mimeType) {
340 for (Mapping mapping : mappings) {
341 if (mapping.packageName.equals(packageName) && mapping.mimeType.equals(mimeType)) {
342 return mapping;
343 }
344 }
345 return null;
346 }
347
348 /**
349 * Create a new {@link MappingCache} object and fill by walking across
350 * all packages to find those that provide mappings.
351 */
352 public static MappingCache createAndFill(Context context) {
353 Log.d(TAG, "searching for mimetype mappings...");
354 final PackageManager pm = context.getPackageManager();
355 MappingCache building = new MappingCache();
356 List<ApplicationInfo> installed = pm
357 .getInstalledApplications(PackageManager.GET_META_DATA);
358 for (ApplicationInfo info : installed) {
359 if (info.metaData != null && info.metaData.containsKey(MAPPING_METADATA)) {
360 try {
361 // Found metadata, so clone into their context to
362 // inflate reference
363 Context theirContext = context.createPackageContext(info.packageName, 0);
364 XmlPullParser mappingParser = info.loadXmlMetaData(pm, MAPPING_METADATA);
365 building.inflateMappings(theirContext, info.uid, info.packageName,
366 mappingParser);
367 } catch (NameNotFoundException e) {
368 Log.w(TAG, "Problem creating context for remote package", e);
369 } catch (InflateException e) {
370 Log.w(TAG, "Problem inflating MappingSet from remote package", e);
371 }
372 }
373 }
374 return building;
375 }
376
377 public static class InflateException extends Exception {
378 public InflateException(String message) {
379 super(message);
380 }
381
382 public InflateException(String message, Throwable throwable) {
383 super(message, throwable);
384 }
385 }
386
387 /**
388 * Inflate a <code>MappingSet</code> from an XML resource, assuming the
389 * given package name as the source.
390 */
391 public void inflateMappings(Context context, int uid, String packageName,
392 XmlPullParser parser) throws InflateException {
393 final AttributeSet attrs = Xml.asAttributeSet(parser);
394
395 try {
396 int type;
397 while ((type = parser.next()) != XmlPullParser.START_TAG
398 && type != XmlPullParser.END_DOCUMENT) {
399 // Drain comments and whitespace
400 }
401
402 if (type != XmlPullParser.START_TAG) {
403 throw new InflateException("No start tag found");
404 }
405
406 if (!TAG_MAPPINGSET.equals(parser.getName())) {
407 throw new InflateException("Top level element must be MappingSet");
408 }
409
410 // Parse all children actions
411 final int depth = parser.getDepth();
412 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
413 && type != XmlPullParser.END_DOCUMENT) {
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700414 if (type == XmlPullParser.END_TAG) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700415 continue;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700416 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700417
418 if (!TAG_MAPPING.equals(parser.getName())) {
419 throw new InflateException("Expected Mapping tag");
420 }
421
422 // Parse kind, mime-type, and RemoteViews reference
423 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Mapping);
424
425 Mapping mapping = new Mapping();
426 mapping.packageName = packageName;
427 mapping.mimeType = a.getString(R.styleable.Mapping_mimeType);
428 mapping.remoteViewsRes = a.getResourceId(R.styleable.Mapping_remoteViews, -1);
429
430 // Read and resize icon if provided
431 int iconRes = a.getResourceId(R.styleable.Mapping_icon, -1);
432 if (iconRes != -1) {
433 mapping.icon = BitmapFactory
434 .decodeResource(context.getResources(), iconRes);
435 mapping.icon = Utilities.createBitmapThumbnail(mapping.icon, context,
436 FastTrackWindow.ICON_SIZE);
437 }
438
439 addMapping(mapping);
440 Log.d(TAG, "Added mapping for packageName=" + mapping.packageName
441 + ", mimetype=" + mapping.mimeType);
442 }
443 } catch (XmlPullParserException e) {
444 throw new InflateException("Problem reading XML", e);
445 } catch (IOException e) {
446 throw new InflateException("Problem reading XML", e);
447 }
448 }
449 }
450
451 /**
452 * Borrowed from Launcher for {@link Bitmap} resizing.
453 */
454 static final class Utilities {
455 private static final Paint sPaint = new Paint();
456 private static final Rect sBounds = new Rect();
457 private static final Rect sOldBounds = new Rect();
458 private static Canvas sCanvas = new Canvas();
459
460 static {
461 sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
462 Paint.FILTER_BITMAP_FLAG));
463 }
464
465 /**
466 * Returns a Bitmap representing the thumbnail of the specified Bitmap.
467 * The size of the thumbnail is defined by the dimension
468 * android.R.dimen.launcher_application_icon_size. This method is not
469 * thread-safe and should be invoked on the UI thread only.
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700470 *
Jeff Sharkey3f177592009-05-18 15:23:12 -0700471 * @param bitmap The bitmap to get a thumbnail of.
472 * @param context The application's context.
473 * @return A thumbnail for the specified bitmap or the bitmap itself if
474 * the thumbnail could not be created.
475 */
476 static Bitmap createBitmapThumbnail(Bitmap bitmap, Context context, int size) {
477 int width = size;
478 int height = size;
479
480 final int bitmapWidth = bitmap.getWidth();
481 final int bitmapHeight = bitmap.getHeight();
482
483 if (width > 0 && height > 0 && (width < bitmapWidth || height < bitmapHeight)) {
484 final float ratio = (float)bitmapWidth / bitmapHeight;
485
486 if (bitmapWidth > bitmapHeight) {
487 height = (int)(width / ratio);
488 } else if (bitmapHeight > bitmapWidth) {
489 width = (int)(height * ratio);
490 }
491
492 final Bitmap.Config c = (width == size && height == size) ? bitmap.getConfig()
493 : Bitmap.Config.ARGB_8888;
494 final Bitmap thumb = Bitmap.createBitmap(size, size, c);
495 final Canvas canvas = sCanvas;
496 final Paint paint = sPaint;
497 canvas.setBitmap(thumb);
498 paint.setDither(false);
499 paint.setFilterBitmap(true);
500 sBounds.set((size - width) / 2, (size - height) / 2, width, height);
501 sOldBounds.set(0, 0, bitmapWidth, bitmapHeight);
502 canvas.drawBitmap(bitmap, sOldBounds, sBounds, paint);
503 return thumb;
504 }
505
506 return bitmap;
507 }
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700508 }
509}