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/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;
+ }
+}