Adopted SocialContract and first pass at fast-track.
Showing live data from SocialProvider through recently added
SocialContract constants, symlinked for now since the contract
isn't in the framework yet.
Added first pass at fast-track using edge-based triggering
from the social list. Wraps the ListView in a EdgeTriggerView
that watches for "pull" actions from a specific edge. Also adds
concept of a FloatyListView to keep a "floaty" window anchored
with respect to ListView scrolling.
The fast-track window summarizes contact methods based on
anyone system-wide who offers an icon for the mime-types. For
example, the testing app pushes app-specific contact methods
into the Data table, and then provides icons through its
RemoteViewsMapping XML resource.
Changed SHOW_OR_CREATE to accept Aggregate Uris and now shows
fast-track in cases where a single matching aggregate is found.
Abstracted AsyncQueryHandler to a QueryCompletedListener callback
interface to clean up code that uses it while still protecting
against leaked Contexts.
diff --git a/src/com/android/contacts/SocialStreamActivity.java b/src/com/android/contacts/SocialStreamActivity.java
index 8917fb4..e04128f 100644
--- a/src/com/android/contacts/SocialStreamActivity.java
+++ b/src/com/android/contacts/SocialStreamActivity.java
@@ -16,19 +16,447 @@
package com.android.contacts;
-import android.app.ListActivity;
-import android.os.Bundle;
-import android.widget.ArrayAdapter;
+import com.android.contacts.EdgeTriggerView.EdgeTriggerListener;
+import com.android.providers.contacts2.ContactsContract;
+import com.android.providers.contacts2.ContactsContract.Aggregates;
+import com.android.providers.contacts2.ContactsContract.Contacts;
+import com.android.providers.contacts2.ContactsContract.Data;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Photo;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.StructuredName;
+import com.android.providers.contacts2.SocialContract.Activities;
-public class SocialStreamActivity extends ListActivity {
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.ListActivity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.PaintDrawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+public class SocialStreamActivity extends ListActivity implements EdgeTriggerListener {
+ private static final String TAG = "SocialStreamActivity";
+
+ private static final String[] PROJ_ACTIVITIES = new String[] {
+ Activities._ID,
+ Activities.PACKAGE,
+ Activities.MIMETYPE,
+ Activities.AUTHOR_CONTACT_ID,
+ Contacts.AGGREGATE_ID,
+ Aggregates.DISPLAY_NAME,
+ Activities.PUBLISHED,
+ Activities.TITLE,
+ Activities.SUMMARY,
+ };
+
+ private static final int COL_ID = 0;
+ private static final int COL_PACKAGE = 1;
+ private static final int COL_MIMETYPE = 2;
+ private static final int COL_AUTHOR_CONTACT_ID = 3;
+ private static final int COL_AGGREGATE_ID = 4;
+ private static final int COL_DISPLAY_NAME = 5;
+ private static final int COL_PUBLISHED = 6;
+ private static final int COL_TITLE = 7;
+ private static final int COL_SUMMARY = 8;
+
+ public static final int PHOTO_SIZE = 58;
+
+ private ListAdapter mAdapter;
+
+ private FloatyListView mListView;
+ private EdgeTriggerView mEdgeTrigger;
+ private FastTrackWindow mFastTrack;
+
+ private ContactsCache mContactsCache;
+ private MappingCache mMappingCache;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
- setContentView(R.layout.social_list_content);
+ setContentView(R.layout.social_list);
- setListAdapter(new ArrayAdapter<String>(this,
- android.R.layout.simple_list_item_1));
+ mContactsCache = new ContactsCache(this);
+ mMappingCache = MappingCache.createAndFill(this);
+
+ Cursor cursor = managedQuery(Activities.CONTENT_URI, PROJ_ACTIVITIES, null, null);
+ mAdapter = new SocialAdapter(this, cursor, mContactsCache);
+
+// mAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_2, cursor,
+// new String[] { Activities.AUTHOR_CONTACT_ID, Activities.TITLE },
+// new int[] { android.R.id.text1, android.R.id.text2 });
+
+ setListAdapter(mAdapter);
+
+ mListView = (FloatyListView)findViewById(android.R.id.list);
+
+ // Find and listen for edge triggers
+ mEdgeTrigger = (EdgeTriggerView)findViewById(R.id.edge_trigger);
+ mEdgeTrigger.setOnEdgeTriggerListener(this);
+ }
+
+ /** {@inheritDoc} */
+ public void onTrigger(float downX, float downY, int edge) {
+ // Find list item user triggered over
+ int position = mListView.pointToPosition((int)downX, (int)downY);
+
+ Cursor cursor = (Cursor)mAdapter.getItem(position);
+ long aggId = cursor.getLong(COL_AGGREGATE_ID);
+
+ Log.d(TAG, "onTrigger found position=" + position + ", contactId=" + aggId);
+
+ Uri aggUri = ContentUris.withAppendedId(ContactsContract.Aggregates.CONTENT_URI, aggId);
+
+ // Dismiss any existing window first
+ if (mFastTrack != null) {
+ mFastTrack.dismiss();
+ }
+
+ mFastTrack = new FastTrackWindow(this, mListView, aggUri, mMappingCache);
+ mListView.setFloatyWindow(mFastTrack, position);
+
+ }
+
+ /**
+ * List adapter for social stream data queried from
+ * {@link Activities#CONTENT_URI}.
+ */
+ private static class SocialAdapter extends CursorAdapter {
+ private Context mContext;
+ private LayoutInflater mInflater;
+ private ContactsCache mContactsCache;
+
+ private static class SocialHolder {
+ ImageView photo;
+ TextView displayname;
+ TextView title;
+ TextView published;
+ }
+
+ public SocialAdapter(Context context, Cursor c, ContactsCache contactsCache) {
+ super(context, c, true);
+ mContext = context;
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mContactsCache = contactsCache;
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ SocialHolder holder = (SocialHolder)view.getTag();
+
+ long contactId = cursor.getLong(COL_AUTHOR_CONTACT_ID);
+
+ // TODO: trigger async query to find actual name and photo instead
+ // of using this lazy caching mechanism
+ holder.photo.setImageBitmap(mContactsCache.getPhoto(contactId));
+
+ holder.displayname.setText(cursor.getString(COL_DISPLAY_NAME));
+ holder.title.setText(cursor.getString(COL_TITLE));
+
+ long published = cursor.getLong(COL_PUBLISHED);
+ CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published, System
+ .currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
+ holder.published.setText(relativePublished);
+
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View view = mInflater.inflate(R.layout.social_list_item, parent, false);
+
+ SocialHolder holder = new SocialHolder();
+ holder.photo = (ImageView)view.findViewById(R.id.photo);
+ holder.displayname = (TextView)view.findViewById(R.id.displayname);
+ holder.title = (TextView)view.findViewById(R.id.title);
+ holder.published = (TextView)view.findViewById(R.id.published);
+ view.setTag(holder);
+
+ return view;
+ }
+ }
+
+ /**
+ * Keep a cache that maps from {@link Contacts#_ID} to {@link Photo#PHOTO}
+ * values.
+ */
+ private static class ContactsCache {
+ private static final String TAG = "ContactsCache";
+
+ private static final String[] PROJ_DETAILS = new String[] {
+ Data.MIMETYPE,
+ Data.CONTACT_ID,
+ Photo.PHOTO,
+ };
+
+ private static final int COL_MIMETYPE = 0;
+ private static final int COL_CONTACT_ID = 1;
+ private static final int COL_PHOTO = 3;
+
+ private HashMap<Long, Bitmap> mPhoto = new HashMap<Long, Bitmap>();
+
+ public ContactsCache(Context context) {
+ Log.d(TAG, "building ContactsCache...");
+
+ ContentResolver resolver = context.getContentResolver();
+ Cursor cursor = resolver.query(Data.CONTENT_URI, PROJ_DETAILS,
+ Data.MIMETYPE + "=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
+
+ while (cursor.moveToNext()) {
+ long contactId = cursor.getLong(COL_CONTACT_ID);
+ String mimeType = cursor.getString(COL_MIMETYPE);
+ if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ byte[] photoBlob = cursor.getBlob(COL_PHOTO);
+ Bitmap photo = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
+ photo = Utilities.createBitmapThumbnail(photo, context, PHOTO_SIZE);
+
+ mPhoto.put(contactId, photo);
+ }
+ }
+
+ cursor.close();
+ Log.d(TAG, "done building ContactsCache");
+ }
+
+ public Bitmap getPhoto(long contactId) {
+ return mPhoto.get(contactId);
+ }
+ }
+
+ /**
+ * Store a parsed <code>RemoteViewsMapping</code> object, which maps
+ * mime-types to <code>RemoteViews</code> XML resources and possible icons.
+ */
+ public static class MappingCache {
+ private static final String TAG = "MappingCache";
+
+ private static final String TAG_MAPPINGSET = "MappingSet";
+ private static final String TAG_MAPPING = "Mapping";
+
+ private static final String MAPPING_METADATA = "com.android.contacts.stylemap";
+
+ private LinkedList<Mapping> mappings = new LinkedList<Mapping>();
+
+ private MappingCache() {
+ }
+
+ public static class Mapping {
+ String packageName;
+ String mimeType;
+ int remoteViewsRes;
+ Bitmap icon;
+ }
+
+ public void addMapping(Mapping mapping) {
+ mappings.add(mapping);
+ }
+
+ /**
+ * Find matching <code>RemoteViews</code> XML resource for requested
+ * package and mime-type. Returns -1 if no mapping found.
+ */
+ public Mapping getMapping(String packageName, String mimeType) {
+ for (Mapping mapping : mappings) {
+ if (mapping.packageName.equals(packageName) && mapping.mimeType.equals(mimeType)) {
+ return mapping;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Create a new {@link MappingCache} object and fill by walking across
+ * all packages to find those that provide mappings.
+ */
+ public static MappingCache createAndFill(Context context) {
+ Log.d(TAG, "searching for mimetype mappings...");
+ final PackageManager pm = context.getPackageManager();
+ MappingCache building = new MappingCache();
+ List<ApplicationInfo> installed = pm
+ .getInstalledApplications(PackageManager.GET_META_DATA);
+ for (ApplicationInfo info : installed) {
+ if (info.metaData != null && info.metaData.containsKey(MAPPING_METADATA)) {
+ try {
+ // Found metadata, so clone into their context to
+ // inflate reference
+ Context theirContext = context.createPackageContext(info.packageName, 0);
+ XmlPullParser mappingParser = info.loadXmlMetaData(pm, MAPPING_METADATA);
+ building.inflateMappings(theirContext, info.uid, info.packageName,
+ mappingParser);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Problem creating context for remote package", e);
+ } catch (InflateException e) {
+ Log.w(TAG, "Problem inflating MappingSet from remote package", e);
+ }
+ }
+ }
+ return building;
+ }
+
+ public static class InflateException extends Exception {
+ public InflateException(String message) {
+ super(message);
+ }
+
+ public InflateException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+ }
+
+ /**
+ * Inflate a <code>MappingSet</code> from an XML resource, assuming the
+ * given package name as the source.
+ */
+ public void inflateMappings(Context context, int uid, String packageName,
+ XmlPullParser parser) throws InflateException {
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ try {
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Drain comments and whitespace
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new InflateException("No start tag found");
+ }
+
+ if (!TAG_MAPPINGSET.equals(parser.getName())) {
+ throw new InflateException("Top level element must be MappingSet");
+ }
+
+ // Parse all children actions
+ final int depth = parser.getDepth();
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+ if (type == XmlPullParser.END_TAG)
+ continue;
+
+ if (!TAG_MAPPING.equals(parser.getName())) {
+ throw new InflateException("Expected Mapping tag");
+ }
+
+ // Parse kind, mime-type, and RemoteViews reference
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Mapping);
+
+ Mapping mapping = new Mapping();
+ mapping.packageName = packageName;
+ mapping.mimeType = a.getString(R.styleable.Mapping_mimeType);
+ mapping.remoteViewsRes = a.getResourceId(R.styleable.Mapping_remoteViews, -1);
+
+ // Read and resize icon if provided
+ int iconRes = a.getResourceId(R.styleable.Mapping_icon, -1);
+ if (iconRes != -1) {
+ mapping.icon = BitmapFactory
+ .decodeResource(context.getResources(), iconRes);
+ mapping.icon = Utilities.createBitmapThumbnail(mapping.icon, context,
+ FastTrackWindow.ICON_SIZE);
+ }
+
+ addMapping(mapping);
+ Log.d(TAG, "Added mapping for packageName=" + mapping.packageName
+ + ", mimetype=" + mapping.mimeType);
+ }
+ } catch (XmlPullParserException e) {
+ throw new InflateException("Problem reading XML", e);
+ } catch (IOException e) {
+ throw new InflateException("Problem reading XML", e);
+ }
+ }
+ }
+
+ /**
+ * Borrowed from Launcher for {@link Bitmap} resizing.
+ */
+ static final class Utilities {
+ private static final Paint sPaint = new Paint();
+ private static final Rect sBounds = new Rect();
+ private static final Rect sOldBounds = new Rect();
+ private static Canvas sCanvas = new Canvas();
+
+ static {
+ sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
+ Paint.FILTER_BITMAP_FLAG));
+ }
+
+ /**
+ * Returns a Bitmap representing the thumbnail of the specified Bitmap.
+ * The size of the thumbnail is defined by the dimension
+ * android.R.dimen.launcher_application_icon_size. This method is not
+ * thread-safe and should be invoked on the UI thread only.
+ *
+ * @param bitmap The bitmap to get a thumbnail of.
+ * @param context The application's context.
+ * @return A thumbnail for the specified bitmap or the bitmap itself if
+ * the thumbnail could not be created.
+ */
+ static Bitmap createBitmapThumbnail(Bitmap bitmap, Context context, int size) {
+ int width = size;
+ int height = size;
+
+ final int bitmapWidth = bitmap.getWidth();
+ final int bitmapHeight = bitmap.getHeight();
+
+ if (width > 0 && height > 0 && (width < bitmapWidth || height < bitmapHeight)) {
+ final float ratio = (float)bitmapWidth / bitmapHeight;
+
+ if (bitmapWidth > bitmapHeight) {
+ height = (int)(width / ratio);
+ } else if (bitmapHeight > bitmapWidth) {
+ width = (int)(height * ratio);
+ }
+
+ final Bitmap.Config c = (width == size && height == size) ? bitmap.getConfig()
+ : Bitmap.Config.ARGB_8888;
+ final Bitmap thumb = Bitmap.createBitmap(size, size, c);
+ final Canvas canvas = sCanvas;
+ final Paint paint = sPaint;
+ canvas.setBitmap(thumb);
+ paint.setDither(false);
+ paint.setFilterBitmap(true);
+ sBounds.set((size - width) / 2, (size - height) / 2, width, height);
+ sOldBounds.set(0, 0, bitmapWidth, bitmapHeight);
+ canvas.drawBitmap(bitmap, sOldBounds, sBounds, paint);
+ return thumb;
+ }
+
+ return bitmap;
+ }
}
}