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/AsyncQueryHandler.java b/src/com/android/contacts/AsyncQueryHandler.java
new file mode 100644
index 0000000..fa63ad4
--- /dev/null
+++ b/src/com/android/contacts/AsyncQueryHandler.java
@@ -0,0 +1,40 @@
+package com.android.contacts;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Slightly more abstract {@link android.content.AsyncQueryHandler} that helps
+ * keep a {@link WeakReference} back to a callback interface. Will properly
+ * close the completed query if the listener ceases to exist.
+ * <p>
+ * Using this pattern will help keep you from leaking a {@link Context}.
+ */
+public class AsyncQueryHandler extends android.content.AsyncQueryHandler {
+ private final WeakReference<QueryCompleteListener> mListener;
+
+ /**
+ * Interface to listen for completed queries.
+ */
+ public static interface QueryCompleteListener {
+ public void onQueryComplete(int token, Object cookie, Cursor cursor);
+ }
+
+ public AsyncQueryHandler(Context context, QueryCompleteListener listener) {
+ super(context.getContentResolver());
+ mListener = new WeakReference<QueryCompleteListener>(listener);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ final QueryCompleteListener listener = mListener.get();
+ if (listener != null) {
+ listener.onQueryComplete(token, cookie, cursor);
+ } else {
+ cursor.close();
+ }
+ }
+}
diff --git a/src/com/android/contacts/EdgeTriggerView.java b/src/com/android/contacts/EdgeTriggerView.java
new file mode 100644
index 0000000..d40dbad
--- /dev/null
+++ b/src/com/android/contacts/EdgeTriggerView.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * 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.android.contacts;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.widget.FrameLayout;
+
+/**
+ * Lightweight view that wraps around an existing view, watching and capturing
+ * sliding gestures from the left or right edges within a given tolerance.
+ */
+public class EdgeTriggerView extends FrameLayout {
+ public static final int FLAG_NONE = 0x00;
+ public static final int FLAG_LEFT = 0x01;
+ public static final int FLAG_RIGHT = 0x02;
+
+ private int mTouchSlop;
+
+ private int mEdgeWidth;
+ private int mListenEdges;
+
+ private boolean mListenLeft = false;
+ private boolean mListenRight = false;
+
+ private MotionEvent mDownStart;
+ private int mEdge = FLAG_NONE;
+
+ public static interface EdgeTriggerListener {
+ public void onTrigger(float downX, float downY, int edge);
+ }
+
+ private EdgeTriggerListener mListener;
+
+ /**
+ * Add a {@link EdgeTriggerListener} to watch for edge events.
+ */
+ public void setOnEdgeTriggerListener(EdgeTriggerListener listener) {
+ mListener = listener;
+ }
+
+ public EdgeTriggerView(Context context) {
+ this(context, null);
+ }
+
+ public EdgeTriggerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public EdgeTriggerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ setClickable(true);
+
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+
+ // TODO: enable reading these values again once we move away from symlinks
+// TypedArray a = context.obtainStyledAttributes(attrs,
+// R.styleable.EdgeTriggerView, defStyle, -1);
+// mEdgeWidth = a.getDimensionPixelSize(R.styleable.EdgeTriggerView_edgeWidth, mTouchSlop);
+// mListenEdges = a.getInt(R.styleable.EdgeTriggerView_listenEdges, FLAG_LEFT);
+
+ mEdgeWidth = 80;
+ mListenEdges = FLAG_LEFT;
+
+ mListenLeft = (mListenEdges & FLAG_LEFT) == FLAG_LEFT;
+ mListenRight = (mListenEdges & FLAG_RIGHT) == FLAG_RIGHT;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ // Consider watching this touch event based on listening flags
+ final float x = event.getX();
+ if (mListenLeft && x < mEdgeWidth) {
+ mEdge = FLAG_LEFT;
+ } else if (mListenRight && x > getWidth() - mEdgeWidth) {
+ mEdge = FLAG_RIGHT;
+ } else {
+ mEdge = FLAG_NONE;
+ }
+
+ if (mEdge != FLAG_NONE) {
+ mDownStart = MotionEvent.obtain(event);
+ } else {
+ mDownStart = null;
+ }
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (mEdge != FLAG_NONE) {
+ // If moved far enough, capture touch event for ourselves
+ float delta = event.getX() - mDownStart.getX();
+ if (mEdge == FLAG_LEFT && delta > mTouchSlop) {
+ return true;
+ } else if (mEdge == FLAG_RIGHT && delta < -mTouchSlop) {
+ return true;
+ }
+ }
+ break;
+ }
+ }
+
+ // Otherwise let the event slip through to children
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // Pass trigger event to listener and return true to consume
+ if (mEdge != FLAG_NONE && mListener != null) {
+ mListener.onTrigger(mDownStart.getX(), mDownStart.getY(), mEdge);
+
+ // Reset values so we don't sent twice
+ mEdge = FLAG_NONE;
+ mDownStart = null;
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/contacts/FastTrackWindow.java b/src/com/android/contacts/FastTrackWindow.java
new file mode 100644
index 0000000..b8d6ccd
--- /dev/null
+++ b/src/com/android/contacts/FastTrackWindow.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * 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.android.contacts;
+
+import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
+import com.android.contacts.FloatyListView.FloatyWindow;
+import com.android.contacts.SocialStreamActivity.MappingCache;
+import com.android.contacts.SocialStreamActivity.MappingCache.Mapping;
+import com.android.providers.contacts2.ContactsContract;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds;
+import com.android.providers.contacts2.ContactsContract.Data;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Email;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Im;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Phone;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Postal;
+
+import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.View.OnClickListener;
+import android.view.ViewTreeObserver.OnScrollChangedListener;
+import android.widget.AbsListView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.Gallery.LayoutParams;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.PriorityQueue;
+
+/**
+ * {@link PopupWindow} that shows fast-track details for a specific aggregate.
+ * This window implements {@link FloatyWindow} so that it can be quickly
+ * repositioned by someone like {@link FloatyListView}.
+ */
+public class FastTrackWindow extends PopupWindow implements QueryCompleteListener, FloatyWindow {
+ private static final String TAG = "FastTrackWindow";
+
+ private Context mContext;
+ private View mParent;
+
+ /** Mapping cache from mime-type to icons and actions */
+ private MappingCache mMappingCache;
+
+ private ViewGroup mContent;
+
+ private Uri mDataUri;
+ private NotifyingAsyncQueryHandler mHandler;
+
+ private boolean mShowing = false;
+ private boolean mHasPosition = false;
+ private boolean mHasData = false;
+
+ public static final int ICON_SIZE = 42;
+ public static final int ICON_PADDING = 3;
+ private static final int VERTICAL_OFFSET = 74;
+
+ private int mFirstX;
+ private int mFirstY;
+
+ private static final int TOKEN = 1;
+
+ private static final int GRAVITY = Gravity.LEFT | Gravity.TOP;
+
+ /** Message to show when no activity is found to perform an action */
+ // TODO: move this value into a resources string
+ private static final String NOT_FOUND = "Couldn't find an app to handle this action";
+
+ /** List of default mime-type icons */
+ private static HashMap<String, Integer> sMimeIcons = new HashMap<String, Integer>();
+
+ /** List of mime-type sorting scores */
+ private static HashMap<String, Integer> sMimeScores = new HashMap<String, Integer>();
+
+ static {
+ sMimeIcons.put(Phone.CONTENT_ITEM_TYPE, android.R.drawable.sym_action_call);
+ sMimeIcons.put(Email.CONTENT_ITEM_TYPE, android.R.drawable.sym_action_email);
+ sMimeIcons.put(Im.CONTENT_ITEM_TYPE, android.R.drawable.sym_action_chat);
+// sMimeIcons.put(Phone.CONTENT_ITEM_TYPE, R.drawable.sym_action_sms);
+ sMimeIcons.put(Postal.CONTENT_ITEM_TYPE, R.drawable.sym_action_map);
+
+ // For scoring, put phone numbers and E-mail up front, and addresses last
+ sMimeScores.put(Phone.CONTENT_ITEM_TYPE, -200);
+ sMimeScores.put(Email.CONTENT_ITEM_TYPE, -100);
+ sMimeScores.put(Postal.CONTENT_ITEM_TYPE, 100);
+ }
+
+ /**
+ * Create a new fast-track window for the given aggregate, using the
+ * provided {@link MappingCache} for icon as needed.
+ */
+ public FastTrackWindow(Context context, View parent, Uri aggUri, MappingCache mappingCache) {
+ super(context);
+
+ final Resources resources = context.getResources();
+
+ mContext = context;
+ mParent = parent;
+
+ mMappingCache = mappingCache;
+
+ // Inflate content view
+ LayoutInflater inflater = (LayoutInflater)context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mContent = (ViewGroup)inflater.inflate(R.layout.fasttrack, null, false);
+
+ setContentView(mContent);
+ setAnimationStyle(android.R.style.Animation_LeftEdge);
+
+ setBackgroundDrawable(resources.getDrawable(R.drawable.fasttrack));
+
+ setWidth(LayoutParams.WRAP_CONTENT);
+ setHeight(LayoutParams.WRAP_CONTENT);
+
+ setClippingEnabled(false);
+ setFocusable(false);
+
+ // Start data query in background
+ mDataUri = Uri.withAppendedPath(aggUri, ContactsContract.Aggregates.Data.CONTENT_DIRECTORY);
+
+ mHandler = new NotifyingAsyncQueryHandler(context, this);
+ mHandler.startQuery(TOKEN, null, mDataUri, null, null, null, null);
+
+ }
+
+ /**
+ * Consider showing this window, which requires both a given position and
+ * completed query results.
+ */
+ private synchronized void considerShowing() {
+ if (mHasData && mHasPosition && !mShowing) {
+ mShowing = true;
+ showAtLocation(mParent, GRAVITY, mFirstX, mFirstY);
+ }
+ }
+
+ /** {@inheritDoc} */
+ public void showAt(int x, int y) {
+ // Adjust vertical position by height
+ y -= VERTICAL_OFFSET;
+
+ // Show dialog or update existing location
+ if (!mShowing) {
+ mFirstX = x;
+ mFirstY = y;
+ mHasPosition = true;
+ considerShowing();
+ } else {
+ update(x, y, -1, -1, true);
+ }
+ }
+
+ /** {@inheritDoc} */
+ public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ final ViewGroup fastTrack = (ViewGroup)mContent.findViewById(R.id.fasttrack);
+
+ // Build list of actions for this contact, this could be done better in
+ // the future using an Adapter
+ ArrayList<ImageView> list = new ArrayList<ImageView>(cursor.getCount());
+
+ final int COL_ID = cursor.getColumnIndex(Data._ID);
+ final int COL_PACKAGE = cursor.getColumnIndex(Data.PACKAGE);
+ final int COL_MIMETYPE = cursor.getColumnIndex(Data.MIMETYPE);
+
+ while (cursor.moveToNext()) {
+ final long dataId = cursor.getLong(COL_ID);
+ final String packageName = cursor.getString(COL_PACKAGE);
+ final String mimeType = cursor.getString(COL_MIMETYPE);
+
+ ImageView action;
+
+ // First, try looking in mapping cache for possible icon match
+ Mapping mapping = mMappingCache.getMapping(packageName, mimeType);
+ if (mapping != null && mapping.icon != null) {
+ action = new ImageView(mContext);
+ action.setImageBitmap(mapping.icon);
+
+ } else if (sMimeIcons.containsKey(mimeType)) {
+ // Otherwise fall back to generic icons
+ int icon = sMimeIcons.get(mimeType);
+ action = new ImageView(mContext);
+ action.setImageResource(icon);
+
+ } else {
+ // No icon found, so don't insert any action button
+ break;
+
+ }
+
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ICON_SIZE, ICON_SIZE);
+ params.rightMargin = ICON_PADDING;
+ action.setLayoutParams(params);
+
+ // Find the sorting score for this mime-type, otherwise allocate a
+ // new one to make sure the same types are grouped together.
+ if (!sMimeScores.containsKey(mimeType)) {
+ sMimeScores.put(mimeType, sMimeScores.size());
+ }
+
+ int mimeScore = sMimeScores.get(mimeType);
+ action.setTag(mimeScore);
+
+ final Intent intent = buildIntentForMime(dataId, mimeType, cursor);
+ action.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ try {
+ mContext.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.w(TAG, NOT_FOUND, e);
+ Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ list.add(action);
+ }
+
+ cursor.close();
+
+ // Sort the final list based on mime-type scores
+ Collections.sort(list, new Comparator<ImageView>() {
+ public int compare(ImageView object1, ImageView object2) {
+ return (Integer)object1.getTag() - (Integer)object2.getTag();
+ }
+ });
+
+ for (ImageView action : list) {
+ fastTrack.addView(action);
+ }
+
+ mHasData = true;
+ considerShowing();
+ }
+
+ /**
+ * Build an {@link Intent} that will trigger the action described by the
+ * given {@link Cursor} and mime-type.
+ */
+ public Intent buildIntentForMime(long dataId, String mimeType, Cursor cursor) {
+ if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final String data = cursor.getString(cursor.getColumnIndex(Phone.NUMBER));
+ Uri callUri = Uri.parse("tel:" + Uri.encode(data));
+ return new Intent(Intent.ACTION_DIAL, callUri);
+
+ } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final String data = cursor.getString(cursor.getColumnIndex(Email.DATA));
+ return new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", data, null));
+
+// } else if (CommonDataKinds.Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
+// return new Intent(Intent.ACTION_SENDTO, constructImToUrl(host, data));
+
+ } else if (CommonDataKinds.Postal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final String data = cursor.getString(cursor.getColumnIndex(Postal.DATA));
+ Uri mapsUri = Uri.parse("geo:0,0?q=" + Uri.encode(data));
+ return new Intent(Intent.ACTION_VIEW, mapsUri);
+
+ }
+
+ // Otherwise fall back to default VIEW action
+ Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(dataUri);
+
+ return intent;
+ }
+}
diff --git a/src/com/android/contacts/FloatyListView.java b/src/com/android/contacts/FloatyListView.java
new file mode 100644
index 0000000..12a25e3
--- /dev/null
+++ b/src/com/android/contacts/FloatyListView.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * 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.android.contacts;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.ListView;
+
+/**
+ * Simple extension to {@link ListView} that internally keeps track of scrolling
+ * position and moves any currently displayed {@link FloatyWindow}.
+ */
+public class FloatyListView extends ListView {
+
+ /**
+ * Interface over to some sort of floating window that allows repositioning
+ * to a specific screen location.
+ */
+ public static interface FloatyWindow {
+ void showAt(int x, int y);
+ }
+
+ public FloatyListView(Context context) {
+ super(context);
+ }
+
+ public FloatyListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public FloatyListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ private FloatyWindow mFloaty;
+ private int mPosition;
+ private int mAnchorY;
+
+ /**
+ * Show the given {@link FloatyWindow} around the given {@link ListView}
+ * position. Specifically, it will keep the window aligned with the top of
+ * that list position.
+ */
+ public void setFloatyWindow(FloatyWindow floaty, int position) {
+ mFloaty = floaty;
+ mPosition = position;
+
+ if (mFloaty != null) {
+ mAnchorY = getPositionY();
+ mFloaty.showAt(0, mAnchorY);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void offsetChildrenTopAndBottom(int offset) {
+ super.offsetChildrenTopAndBottom(offset);
+
+ if (mFloaty != null) {
+ mAnchorY += offset;
+ mFloaty.showAt(0, mAnchorY);
+ }
+ }
+
+ private int[] mLocation = new int[2];
+
+ /**
+ * Calculate the current Y location of the internally tracked position.
+ */
+ public int getPositionY() {
+ return getPositionY(mPosition);
+ }
+
+ /**
+ * Calculate the current Y location of the given list position, or -1 if
+ * that position is offscreen.
+ */
+ public int getPositionY(int position) {
+ final int firstVis = getFirstVisiblePosition();
+ final int lastVis = getLastVisiblePosition();
+
+ if (position >= firstVis && position < lastVis) {
+ // Vertical position is just above position location
+ View childView = getChildAt(position - firstVis);
+ childView.getLocationOnScreen(mLocation);
+ return mLocation[1];
+ }
+ return -1;
+ }
+}
diff --git a/src/com/android/contacts/NotifyingAsyncQueryHandler.java b/src/com/android/contacts/NotifyingAsyncQueryHandler.java
new file mode 100644
index 0000000..2223e9c
--- /dev/null
+++ b/src/com/android/contacts/NotifyingAsyncQueryHandler.java
@@ -0,0 +1,41 @@
+package com.android.contacts;
+
+import android.content.AsyncQueryHandler;
+import android.content.Context;
+import android.database.Cursor;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Slightly more abstract {@link android.content.AsyncQueryHandler} that helps
+ * keep a {@link WeakReference} back to a callback interface. Will properly
+ * close the completed query if the listener ceases to exist.
+ * <p>
+ * Using this pattern will help keep you from leaking a {@link Context}.
+ */
+public class NotifyingAsyncQueryHandler extends AsyncQueryHandler {
+ private final WeakReference<QueryCompleteListener> mListener;
+
+ /**
+ * Interface to listen for completed queries.
+ */
+ public static interface QueryCompleteListener {
+ public void onQueryComplete(int token, Object cookie, Cursor cursor);
+ }
+
+ public NotifyingAsyncQueryHandler(Context context, QueryCompleteListener listener) {
+ super(context.getContentResolver());
+ mListener = new WeakReference<QueryCompleteListener>(listener);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ final QueryCompleteListener listener = mListener.get();
+ if (listener != null) {
+ listener.onQueryComplete(token, cookie, cursor);
+ } else {
+ cursor.close();
+ }
+ }
+}
diff --git a/src/com/android/contacts/ShowOrCreateActivity.java b/src/com/android/contacts/ShowOrCreateActivity.java
index 75af4ae..56aef26 100755
--- a/src/com/android/contacts/ShowOrCreateActivity.java
+++ b/src/com/android/contacts/ShowOrCreateActivity.java
@@ -16,16 +16,22 @@
package com.android.contacts;
+import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
+import com.android.contacts.SocialStreamActivity.MappingCache;
+import com.android.providers.contacts2.ContactsContract;
+import com.android.providers.contacts2.ContactsContract.Aggregates;
+
import android.app.Activity;
import android.app.AlertDialog;
-import android.content.AsyncQueryHandler;
import android.content.ComponentName;
import android.content.ContentUris;
+import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
+import android.os.IBinder;
import android.provider.Contacts;
import android.provider.Contacts.ContactMethods;
import android.provider.Contacts.ContactMethodsColumns;
@@ -33,6 +39,7 @@
import android.provider.Contacts.People;
import android.provider.Contacts.Phones;
import android.util.Log;
+import android.view.View;
import java.lang.ref.WeakReference;
@@ -52,7 +59,7 @@
* {@link Intent#ACTION_SEARCH}.
* </ul>
*/
-public final class ShowOrCreateActivity extends Activity {
+public final class ShowOrCreateActivity extends Activity implements QueryCompleteListener {
static final String TAG = "ShowOrCreateActivity";
static final boolean LOGD = false;
@@ -60,14 +67,15 @@
Phones.PERSON_ID,
};
- static final String[] PEOPLE_PROJECTION = new String[] {
- People._ID,
+ static final String[] CONTACTS_PROJECTION = new String[] {
+ ContactsContract.Contacts.AGGREGATE_ID,
+// People._ID,
};
static final String SCHEME_MAILTO = "mailto";
static final String SCHEME_TEL = "tel";
- static final int PERSON_ID_INDEX = 0;
+ static final int AGGREGATE_ID_INDEX = 0;
/**
* Query clause to filter {@link ContactMethods#CONTENT_URI} to only search
@@ -76,24 +84,39 @@
static final String QUERY_KIND_EMAIL_OR_IM = ContactMethodsColumns.KIND +
" IN (" + Contacts.KIND_EMAIL + "," + Contacts.KIND_IM + ")";
+ /**
+ * Extra used to request a specific {@link FastTrackWindow} position.
+ */
+ private static final String EXTRA_Y = "pixel_y";
+ private static final int DEFAULT_Y = 90;
+
static final int QUERY_TOKEN = 42;
- private QueryHandler mQueryHandler;
+ private NotifyingAsyncQueryHandler mQueryHandler;
+
+ private Bundle mCreateExtras;
+ private String mCreateDescrip;
+ private boolean mCreateForce;
+
+ private FastTrackWindow mFastTrack;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
-
+
+ // Throw an empty layout up so we have a window token later
+ setContentView(R.layout.empty);
+
// Create handler if doesn't exist, otherwise cancel any running
if (mQueryHandler == null) {
- mQueryHandler = new QueryHandler(this);
+ mQueryHandler = new NotifyingAsyncQueryHandler(this, this);
} else {
mQueryHandler.cancelOperation(QUERY_TOKEN);
}
final Intent intent = getIntent();
final Uri data = intent.getData();
-
+
// Unpack scheme and target data from intent
String scheme = null;
String ssp = null;
@@ -101,52 +124,143 @@
scheme = data.getScheme();
ssp = data.getSchemeSpecificPart();
}
-
+
// Build set of extras for possible use when creating contact
- Bundle createExtras = new Bundle();
+ mCreateExtras = new Bundle();
Bundle originalExtras = intent.getExtras();
if (originalExtras != null) {
- createExtras.putAll(originalExtras);
+ mCreateExtras.putAll(originalExtras);
}
- mQueryHandler.setCreateExtras(createExtras);
-
+
// Read possible extra with specific title
- String createDescrip = intent.getStringExtra(Intents.EXTRA_CREATE_DESCRIPTION);
- if (createDescrip == null) {
- createDescrip = ssp;
+ String mCreateDescrip = intent.getStringExtra(Intents.EXTRA_CREATE_DESCRIPTION);
+ if (mCreateDescrip == null) {
+ mCreateDescrip = ssp;
}
- mQueryHandler.setCreateDescription(createDescrip);
-
+
// Allow caller to bypass dialog prompt
- boolean createForce = intent.getBooleanExtra(Intents.EXTRA_FORCE_CREATE, false);
- mQueryHandler.setCreateForce(createForce);
-
+ mCreateForce = intent.getBooleanExtra(Intents.EXTRA_FORCE_CREATE, false);
+
// Handle specific query request
if (SCHEME_MAILTO.equals(scheme)) {
- createExtras.putString(Intents.Insert.EMAIL, ssp);
- Uri uri = Uri.withAppendedPath(People.WITH_EMAIL_OR_IM_FILTER_URI, Uri.encode(ssp));
+ mCreateExtras.putString(Intents.Insert.EMAIL, ssp);
+// Uri uri = Uri.withAppendedPath(People.WITH_EMAIL_OR_IM_FILTER_URI, Uri.encode(ssp));
+// mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
+// PEOPLE_PROJECTION, null, null, null);
+
+ Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_EMAIL_FILTER_URI, Uri.encode(ssp));
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
- PEOPLE_PROJECTION, null, null, null);
+ CONTACTS_PROJECTION, null, null, null);
+
} else if (SCHEME_TEL.equals(scheme)) {
- createExtras.putString(Intents.Insert.PHONE, ssp);
- mQueryHandler.startQuery(QUERY_TOKEN, null,
- Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, ssp),
- PHONES_PROJECTION, null, null, null);
-
+ mCreateExtras.putString(Intents.Insert.PHONE, ssp);
+// mQueryHandler.startQuery(QUERY_TOKEN, null,
+// Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, ssp),
+// PHONES_PROJECTION, null, null, null);
+
} else {
- Log.w(TAG, "Invalid intent:" + getIntent());
- finish();
+ // Otherwise assume incoming aggregate Uri
+ final int y = getIntent().getExtras().getInt(EXTRA_Y, DEFAULT_Y);
+ showFastTrack(data, y);
+
}
}
-
+
@Override
protected void onStop() {
super.onStop();
if (mQueryHandler != null) {
mQueryHandler.cancelOperation(QUERY_TOKEN);
}
+ if (mFastTrack != null) {
+ mFastTrack.dismiss();
+ }
}
-
+
+ /**
+ * Show a {@link FastTrackWindow} for the given aggregate at the requested
+ * screen location.
+ */
+ private void showFastTrack(Uri aggUri, int y) {
+ // Use our local window token for now
+ final IBinder windowToken = findViewById(android.R.id.empty).getWindowToken();
+ FakeView fakeView = new FakeView(this, windowToken);
+
+ final MappingCache mappingCache = MappingCache.createAndFill(this);
+
+ mFastTrack = new FastTrackWindow(this, fakeView, aggUri, mappingCache);
+ mFastTrack.showAt(0, y);
+ }
+
+ public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (cursor == null) {
+ return;
+ }
+
+ // Count contacts found by query
+ int count = 0;
+ long aggId = -1;
+ try {
+ count = cursor.getCount();
+ if (count == 1 && cursor.moveToFirst()) {
+ // Try reading ID if only one contact returned
+ aggId = cursor.getLong(AGGREGATE_ID_INDEX);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ if (count == 1 && aggId != -1) {
+ // If we only found one item, jump right to viewing it
+ final Uri aggUri = ContentUris.withAppendedId(Aggregates.CONTENT_URI, aggId);
+ final int y = getIntent().getExtras().getInt(EXTRA_Y, DEFAULT_Y);
+ showFastTrack(aggUri, y);
+
+// Intent viewIntent = new Intent(Intent.ACTION_VIEW,
+// ContentUris.withAppendedId(People.CONTENT_URI, personId));
+// activity.startActivity(viewIntent);
+// activity.finish();
+
+ } else if (count > 1) {
+ // If more than one, show pick list
+ Intent listIntent = new Intent(Intent.ACTION_SEARCH);
+ listIntent.setComponent(new ComponentName(this, ContactsListActivity.class));
+ listIntent.putExtras(mCreateExtras);
+ startActivity(listIntent);
+ finish();
+
+ } else {
+ // No matching contacts found
+ if (mCreateForce) {
+ // Forced to create new contact
+ Intent createIntent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
+ createIntent.putExtras(mCreateExtras);
+ createIntent.setType(People.CONTENT_TYPE);
+
+ startActivity(createIntent);
+ finish();
+
+ } else {
+ // Prompt user to insert or edit contact
+ Intent createIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+ createIntent.putExtras(mCreateExtras);
+ createIntent.setType(People.CONTENT_ITEM_TYPE);
+
+ CharSequence message = getResources().getString(
+ R.string.add_contact_dlg_message_fmt, mCreateDescrip);
+
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.add_contact_dlg_title)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok,
+ new IntentClickListener(this, createIntent))
+ .setNegativeButton(android.R.string.cancel,
+ new IntentClickListener(this, null))
+ .show();
+ }
+ }
+ }
+
/**
* Listener for {@link DialogInterface} that launches a given {@link Intent}
* when clicked. When clicked, this also closes the parent using
@@ -174,101 +288,20 @@
}
/**
- * Handle asynchronous query to find matching contacts. When query finishes,
- * will handle based on number of matching contacts found.
+ * Fake view that simply exists to pass through a specific {@link IBinder}
+ * window token.
*/
- private static final class QueryHandler extends AsyncQueryHandler {
- private final WeakReference<Activity> mActivity;
- private Bundle mCreateExtras;
- private String mCreateDescrip;
- private boolean mCreateForce;
+ private static class FakeView extends View {
+ private IBinder mWindowToken;
- public QueryHandler(Activity activity) {
- super(activity.getContentResolver());
- mActivity = new WeakReference<Activity>(activity);
- }
-
- public void setCreateExtras(Bundle createExtras) {
- mCreateExtras = createExtras;
- }
-
- public void setCreateDescription(String createDescrip) {
- mCreateDescrip = createDescrip;
- }
-
- public void setCreateForce(boolean createForce) {
- mCreateForce = createForce;
+ public FakeView(Context context, IBinder windowToken) {
+ super(context);
+ mWindowToken = windowToken;
}
@Override
- protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
- Activity activity = mActivity.get();
- if (activity == null) {
- return;
- }
-
- // Count contacts found by query
- int count = 0;
- long personId = -1;
- if (cursor != null) {
- try {
- count = cursor.getCount();
- if (count == 1 && cursor.moveToFirst()) {
- // Try reading ID if only one contact returned
- personId = cursor.getLong(PERSON_ID_INDEX);
- }
- } finally {
- cursor.close();
- }
- }
-
- if (LOGD) Log.d(TAG, "onQueryComplete count=" + count);
-
- if (count == 1) {
- // If we only found one item, jump right to viewing it
- Intent viewIntent = new Intent(Intent.ACTION_VIEW,
- ContentUris.withAppendedId(People.CONTENT_URI, personId));
- activity.startActivity(viewIntent);
- activity.finish();
-
- } else if (count > 1) {
- // If more than one, show pick list
- Intent listIntent = new Intent(Intent.ACTION_SEARCH);
- listIntent.setComponent(new ComponentName(activity, ContactsListActivity.class));
- listIntent.putExtras(mCreateExtras);
- activity.startActivity(listIntent);
- activity.finish();
-
- } else {
- // No matching contacts found
- if (mCreateForce) {
- // Forced to create new contact
- Intent createIntent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
- createIntent.putExtras(mCreateExtras);
- createIntent.setType(People.CONTENT_TYPE);
-
- activity.startActivity(createIntent);
- activity.finish();
-
- } else {
- // Prompt user to insert or edit contact
- Intent createIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
- createIntent.putExtras(mCreateExtras);
- createIntent.setType(People.CONTENT_ITEM_TYPE);
-
- CharSequence message = activity.getResources().getString(
- R.string.add_contact_dlg_message_fmt, mCreateDescrip);
-
- new AlertDialog.Builder(activity)
- .setTitle(R.string.add_contact_dlg_title)
- .setMessage(message)
- .setPositiveButton(android.R.string.ok,
- new IntentClickListener(activity, createIntent))
- .setNegativeButton(android.R.string.cancel,
- new IntentClickListener(activity, null))
- .show();
- }
- }
+ public IBinder getWindowToken() {
+ return mWindowToken;
}
}
}
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;
+ }
}
}
diff --git a/src/com/android/providers/contacts2/ContactsContract.java b/src/com/android/providers/contacts2/ContactsContract.java
new file mode 120000
index 0000000..206b35a
--- /dev/null
+++ b/src/com/android/providers/contacts2/ContactsContract.java
@@ -0,0 +1 @@
+../../../../../../../providers/ContactsProvider2/src/com/android/providers/contacts2/ContactsContract.java
\ No newline at end of file
diff --git a/src/com/android/providers/contacts2/SocialContract.java b/src/com/android/providers/contacts2/SocialContract.java
new file mode 120000
index 0000000..40cafe1
--- /dev/null
+++ b/src/com/android/providers/contacts2/SocialContract.java
@@ -0,0 +1 @@
+../../../../../../../providers/ContactsProvider2/src/com/android/providers/contacts2/SocialContract.java
\ No newline at end of file