blob: c3d8997b599b99ac688f0e4d4e8a27f92288d702 [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,
80 };
81
82 private static final int COL_ID = 0;
83 private static final int COL_PACKAGE = 1;
84 private static final int COL_MIMETYPE = 2;
85 private static final int COL_AUTHOR_CONTACT_ID = 3;
86 private static final int COL_AGGREGATE_ID = 4;
87 private static final int COL_DISPLAY_NAME = 5;
88 private static final int COL_PUBLISHED = 6;
89 private static final int COL_TITLE = 7;
90 private static final int COL_SUMMARY = 8;
91
92 public static final int PHOTO_SIZE = 58;
93
94 private ListAdapter mAdapter;
95
96 private FloatyListView mListView;
97 private EdgeTriggerView mEdgeTrigger;
98 private FastTrackWindow mFastTrack;
99
100 private ContactsCache mContactsCache;
101 private MappingCache mMappingCache;
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700102
103 @Override
104 protected void onCreate(Bundle icicle) {
105 super.onCreate(icicle);
106
Jeff Sharkey3f177592009-05-18 15:23:12 -0700107 setContentView(R.layout.social_list);
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700108
Jeff Sharkey3f177592009-05-18 15:23:12 -0700109 mContactsCache = new ContactsCache(this);
110 mMappingCache = MappingCache.createAndFill(this);
111
112 Cursor cursor = managedQuery(Activities.CONTENT_URI, PROJ_ACTIVITIES, null, null);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700113 mAdapter = new SocialAdapter(this, cursor, mContactsCache, mMappingCache);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700114
115 setListAdapter(mAdapter);
116
117 mListView = (FloatyListView)findViewById(android.R.id.list);
118
119 // Find and listen for edge triggers
120 mEdgeTrigger = (EdgeTriggerView)findViewById(R.id.edge_trigger);
121 mEdgeTrigger.setOnEdgeTriggerListener(this);
122 }
123
124 /** {@inheritDoc} */
125 public void onTrigger(float downX, float downY, int edge) {
126 // Find list item user triggered over
127 int position = mListView.pointToPosition((int)downX, (int)downY);
128
129 Cursor cursor = (Cursor)mAdapter.getItem(position);
130 long aggId = cursor.getLong(COL_AGGREGATE_ID);
131
132 Log.d(TAG, "onTrigger found position=" + position + ", contactId=" + aggId);
133
134 Uri aggUri = ContentUris.withAppendedId(ContactsContract.Aggregates.CONTENT_URI, aggId);
135
136 // Dismiss any existing window first
137 if (mFastTrack != null) {
138 mFastTrack.dismiss();
139 }
140
141 mFastTrack = new FastTrackWindow(this, mListView, aggUri, mMappingCache);
142 mListView.setFloatyWindow(mFastTrack, position);
143
144 }
145
146 /**
147 * List adapter for social stream data queried from
148 * {@link Activities#CONTENT_URI}.
149 */
150 private static class SocialAdapter extends CursorAdapter {
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700151 private final Context mContext;
152 private final LayoutInflater mInflater;
153 private final ContactsCache mContactsCache;
154 private final MappingCache mMappingCache;
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700155 private final StyleSpan mTextStyleName;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700156
157 private static class SocialHolder {
158 ImageView photo;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700159 ImageView sourceIcon;
160 TextView content;
161 SpannableStringBuilder contentBuilder = new SpannableStringBuilder();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700162 TextView published;
163 }
164
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700165 public SocialAdapter(Context context, Cursor c, ContactsCache contactsCache,
166 MappingCache mappingCache) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700167 super(context, c, true);
168 mContext = context;
169 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
170 mContactsCache = contactsCache;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700171 mMappingCache = mappingCache;
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700172 mTextStyleName = new StyleSpan(android.graphics.Typeface.BOLD);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700173 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700174
175 @Override
176 public void bindView(View view, Context context, Cursor cursor) {
177 SocialHolder holder = (SocialHolder)view.getTag();
178
179 long contactId = cursor.getLong(COL_AUTHOR_CONTACT_ID);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700180 String name = cursor.getString(COL_DISPLAY_NAME);
181 String title = cursor.getString(COL_TITLE);
182 long published = cursor.getLong(COL_PUBLISHED);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700183
184 // TODO: trigger async query to find actual name and photo instead
185 // of using this lazy caching mechanism
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700186 Bitmap photo = mContactsCache.getPhoto(contactId);
187 if (photo != null) {
188 holder.photo.setImageBitmap(photo);
189 } else {
190 holder.photo.setImageResource(R.drawable.ic_contact_list_picture);
191 }
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700192 holder.contentBuilder.clear();
193 holder.contentBuilder.append(name);
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700194 holder.contentBuilder.append(" ");
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700195 holder.contentBuilder.append(title);
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700196 holder.contentBuilder.setSpan(mTextStyleName, 0, name.length(), 0);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700197 holder.content.setText(holder.contentBuilder);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700198
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700199 CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published,
200 System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700201 holder.published.setText(relativePublished);
202
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700203 String packageName = cursor.getString(COL_PACKAGE);
204 String mimeType = cursor.getString(COL_MIMETYPE);
205 Mapping mapping = mMappingCache.getMapping(packageName, mimeType);
206 if (mapping != null && mapping.icon != null) {
207 holder.sourceIcon.setImageBitmap(mapping.icon);
208 } else {
209 holder.sourceIcon.setImageDrawable(null);
210 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700211 }
212
213 @Override
214 public View newView(Context context, Cursor cursor, ViewGroup parent) {
215 View view = mInflater.inflate(R.layout.social_list_item, parent, false);
216
217 SocialHolder holder = new SocialHolder();
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700218 holder.photo = (ImageView) view.findViewById(R.id.photo);
219 holder.sourceIcon = (ImageView) view.findViewById(R.id.sourceIcon);
220 holder.content = (TextView) view.findViewById(R.id.content);
221 holder.published = (TextView) view.findViewById(R.id.published);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700222 view.setTag(holder);
223
224 return view;
225 }
226 }
227
228 /**
229 * Keep a cache that maps from {@link Contacts#_ID} to {@link Photo#PHOTO}
230 * values.
231 */
232 private static class ContactsCache {
233 private static final String TAG = "ContactsCache";
234
235 private static final String[] PROJ_DETAILS = new String[] {
236 Data.MIMETYPE,
237 Data.CONTACT_ID,
238 Photo.PHOTO,
239 };
240
241 private static final int COL_MIMETYPE = 0;
242 private static final int COL_CONTACT_ID = 1;
Jeff Sharkey8da253a2009-05-18 21:23:19 -0700243 private static final int COL_PHOTO = 2;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700244
245 private HashMap<Long, Bitmap> mPhoto = new HashMap<Long, Bitmap>();
246
247 public ContactsCache(Context context) {
248 Log.d(TAG, "building ContactsCache...");
249
250 ContentResolver resolver = context.getContentResolver();
251 Cursor cursor = resolver.query(Data.CONTENT_URI, PROJ_DETAILS,
252 Data.MIMETYPE + "=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
253
254 while (cursor.moveToNext()) {
255 long contactId = cursor.getLong(COL_CONTACT_ID);
256 String mimeType = cursor.getString(COL_MIMETYPE);
257 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
258 byte[] photoBlob = cursor.getBlob(COL_PHOTO);
259 Bitmap photo = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
260 photo = Utilities.createBitmapThumbnail(photo, context, PHOTO_SIZE);
261
262 mPhoto.put(contactId, photo);
263 }
264 }
265
266 cursor.close();
267 Log.d(TAG, "done building ContactsCache");
268 }
269
270 public Bitmap getPhoto(long contactId) {
271 return mPhoto.get(contactId);
272 }
273 }
274
275 /**
276 * Store a parsed <code>RemoteViewsMapping</code> object, which maps
277 * mime-types to <code>RemoteViews</code> XML resources and possible icons.
278 */
279 public static class MappingCache {
280 private static final String TAG = "MappingCache";
281
282 private static final String TAG_MAPPINGSET = "MappingSet";
283 private static final String TAG_MAPPING = "Mapping";
284
285 private static final String MAPPING_METADATA = "com.android.contacts.stylemap";
286
287 private LinkedList<Mapping> mappings = new LinkedList<Mapping>();
288
289 private MappingCache() {
290 }
291
292 public static class Mapping {
293 String packageName;
294 String mimeType;
295 int remoteViewsRes;
296 Bitmap icon;
297 }
298
299 public void addMapping(Mapping mapping) {
300 mappings.add(mapping);
301 }
302
303 /**
304 * Find matching <code>RemoteViews</code> XML resource for requested
305 * package and mime-type. Returns -1 if no mapping found.
306 */
307 public Mapping getMapping(String packageName, String mimeType) {
308 for (Mapping mapping : mappings) {
309 if (mapping.packageName.equals(packageName) && mapping.mimeType.equals(mimeType)) {
310 return mapping;
311 }
312 }
313 return null;
314 }
315
316 /**
317 * Create a new {@link MappingCache} object and fill by walking across
318 * all packages to find those that provide mappings.
319 */
320 public static MappingCache createAndFill(Context context) {
321 Log.d(TAG, "searching for mimetype mappings...");
322 final PackageManager pm = context.getPackageManager();
323 MappingCache building = new MappingCache();
324 List<ApplicationInfo> installed = pm
325 .getInstalledApplications(PackageManager.GET_META_DATA);
326 for (ApplicationInfo info : installed) {
327 if (info.metaData != null && info.metaData.containsKey(MAPPING_METADATA)) {
328 try {
329 // Found metadata, so clone into their context to
330 // inflate reference
331 Context theirContext = context.createPackageContext(info.packageName, 0);
332 XmlPullParser mappingParser = info.loadXmlMetaData(pm, MAPPING_METADATA);
333 building.inflateMappings(theirContext, info.uid, info.packageName,
334 mappingParser);
335 } catch (NameNotFoundException e) {
336 Log.w(TAG, "Problem creating context for remote package", e);
337 } catch (InflateException e) {
338 Log.w(TAG, "Problem inflating MappingSet from remote package", e);
339 }
340 }
341 }
342 return building;
343 }
344
345 public static class InflateException extends Exception {
346 public InflateException(String message) {
347 super(message);
348 }
349
350 public InflateException(String message, Throwable throwable) {
351 super(message, throwable);
352 }
353 }
354
355 /**
356 * Inflate a <code>MappingSet</code> from an XML resource, assuming the
357 * given package name as the source.
358 */
359 public void inflateMappings(Context context, int uid, String packageName,
360 XmlPullParser parser) throws InflateException {
361 final AttributeSet attrs = Xml.asAttributeSet(parser);
362
363 try {
364 int type;
365 while ((type = parser.next()) != XmlPullParser.START_TAG
366 && type != XmlPullParser.END_DOCUMENT) {
367 // Drain comments and whitespace
368 }
369
370 if (type != XmlPullParser.START_TAG) {
371 throw new InflateException("No start tag found");
372 }
373
374 if (!TAG_MAPPINGSET.equals(parser.getName())) {
375 throw new InflateException("Top level element must be MappingSet");
376 }
377
378 // Parse all children actions
379 final int depth = parser.getDepth();
380 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
381 && type != XmlPullParser.END_DOCUMENT) {
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700382 if (type == XmlPullParser.END_TAG) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700383 continue;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700384 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700385
386 if (!TAG_MAPPING.equals(parser.getName())) {
387 throw new InflateException("Expected Mapping tag");
388 }
389
390 // Parse kind, mime-type, and RemoteViews reference
391 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Mapping);
392
393 Mapping mapping = new Mapping();
394 mapping.packageName = packageName;
395 mapping.mimeType = a.getString(R.styleable.Mapping_mimeType);
396 mapping.remoteViewsRes = a.getResourceId(R.styleable.Mapping_remoteViews, -1);
397
398 // Read and resize icon if provided
399 int iconRes = a.getResourceId(R.styleable.Mapping_icon, -1);
400 if (iconRes != -1) {
401 mapping.icon = BitmapFactory
402 .decodeResource(context.getResources(), iconRes);
403 mapping.icon = Utilities.createBitmapThumbnail(mapping.icon, context,
404 FastTrackWindow.ICON_SIZE);
405 }
406
407 addMapping(mapping);
408 Log.d(TAG, "Added mapping for packageName=" + mapping.packageName
409 + ", mimetype=" + mapping.mimeType);
410 }
411 } catch (XmlPullParserException e) {
412 throw new InflateException("Problem reading XML", e);
413 } catch (IOException e) {
414 throw new InflateException("Problem reading XML", e);
415 }
416 }
417 }
418
419 /**
420 * Borrowed from Launcher for {@link Bitmap} resizing.
421 */
422 static final class Utilities {
423 private static final Paint sPaint = new Paint();
424 private static final Rect sBounds = new Rect();
425 private static final Rect sOldBounds = new Rect();
426 private static Canvas sCanvas = new Canvas();
427
428 static {
429 sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
430 Paint.FILTER_BITMAP_FLAG));
431 }
432
433 /**
434 * Returns a Bitmap representing the thumbnail of the specified Bitmap.
435 * The size of the thumbnail is defined by the dimension
436 * android.R.dimen.launcher_application_icon_size. This method is not
437 * thread-safe and should be invoked on the UI thread only.
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700438 *
Jeff Sharkey3f177592009-05-18 15:23:12 -0700439 * @param bitmap The bitmap to get a thumbnail of.
440 * @param context The application's context.
441 * @return A thumbnail for the specified bitmap or the bitmap itself if
442 * the thumbnail could not be created.
443 */
444 static Bitmap createBitmapThumbnail(Bitmap bitmap, Context context, int size) {
445 int width = size;
446 int height = size;
447
448 final int bitmapWidth = bitmap.getWidth();
449 final int bitmapHeight = bitmap.getHeight();
450
451 if (width > 0 && height > 0 && (width < bitmapWidth || height < bitmapHeight)) {
452 final float ratio = (float)bitmapWidth / bitmapHeight;
453
454 if (bitmapWidth > bitmapHeight) {
455 height = (int)(width / ratio);
456 } else if (bitmapHeight > bitmapWidth) {
457 width = (int)(height * ratio);
458 }
459
460 final Bitmap.Config c = (width == size && height == size) ? bitmap.getConfig()
461 : Bitmap.Config.ARGB_8888;
462 final Bitmap thumb = Bitmap.createBitmap(size, size, c);
463 final Canvas canvas = sCanvas;
464 final Paint paint = sPaint;
465 canvas.setBitmap(thumb);
466 paint.setDither(false);
467 paint.setFilterBitmap(true);
468 sBounds.set((size - width) / 2, (size - height) / 2, width, height);
469 sOldBounds.set(0, 0, bitmapWidth, bitmapHeight);
470 canvas.drawBitmap(bitmap, sOldBounds, sBounds, paint);
471 return thumb;
472 }
473
474 return bitmap;
475 }
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700476 }
477}